From 4791037455978e7702afc3b9850b3b9b72e6459d Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Thu, 6 Feb 2025 15:50:10 +0000 Subject: [PATCH 1/7] Clean up path validation Abstract regex patterns behind static acceptance method --- .../sdk/spring/metadata/Metadata.java | 18 ++++++------------ .../sdk/spring/path/DataFieldPath.java | 12 ++++++++++-- .../quickcase/sdk/spring/path/FieldPath.java | 6 +----- .../sdk/spring/path/MetadataFieldPath.java | 7 ++++--- 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/main/java/app/quickcase/sdk/spring/metadata/Metadata.java b/src/main/java/app/quickcase/sdk/spring/metadata/Metadata.java index a5670b1..8bac301 100644 --- a/src/main/java/app/quickcase/sdk/spring/metadata/Metadata.java +++ b/src/main/java/app/quickcase/sdk/spring/metadata/Metadata.java @@ -29,20 +29,14 @@ public static Metadata fromPath(@NonNull String path) { var name = path.substring(1, path.length() - 1).toLowerCase(); return switch (name) { - case "workspace", "organisation", "jurisdiction" -> - WORKSPACE; - case "type", "case_type" -> - TYPE; - case "id", "reference", "case_reference" -> - ID; + case "workspace", "organisation", "jurisdiction" -> WORKSPACE; + case "type", "case_type" -> TYPE; + case "id", "reference", "case_reference" -> ID; case "title" -> TITLE; case "state" -> STATE; - case "classification", "security_classification" -> - CLASSIFICATION; - case "createdat", "created", "created_date" -> - CREATED_AT; - case "lastmodifiedat", "modified", "last_modified", "last_modified_date" -> - LAST_MODIFIED_AT; + case "classification", "security_classification" -> CLASSIFICATION; + case "createdat", "created", "created_date" -> CREATED_AT; + case "lastmodifiedat", "modified", "last_modified", "last_modified_date" -> LAST_MODIFIED_AT; default -> throw new IllegalArgumentException("Invalid metadata path: " + path); }; } diff --git a/src/main/java/app/quickcase/sdk/spring/path/DataFieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/DataFieldPath.java index 127de5f..d853959 100644 --- a/src/main/java/app/quickcase/sdk/spring/path/DataFieldPath.java +++ b/src/main/java/app/quickcase/sdk/spring/path/DataFieldPath.java @@ -8,11 +8,15 @@ import lombok.NonNull; public final class DataFieldPath extends FieldPath { - public static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(?:\\[(?:(?:id:[a-zA-Z0-9_]+)|(?:[0-9]+))?])?(?:\\.[a-zA-Z0-9_]+(?:\\[(?:(?:id:[a-zA-Z0-9_]+)|(?:[0-9]+))?])?)*$"); - public static final Pattern COLLECTION_ITEM_PATTERN = Pattern.compile("^(?[a-zA-Z0-9_]+)\\[(?:(?:id:(?[a-zA-Z0-9_]+))|(?[0-9]+))?]$"); + private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(?:\\[(?:(?:id:[a-zA-Z0-9_]+)|(?:[0-9]+))?])?(?:\\.[a-zA-Z0-9_]+(?:\\[(?:(?:id:[a-zA-Z0-9_]+)|(?:[0-9]+))?])?)*$"); + private static final Pattern COLLECTION_ITEM_PATTERN = Pattern.compile("^(?[a-zA-Z0-9_]+)\\[(?:(?:id:(?[a-zA-Z0-9_]+))|(?[0-9]+))?]$"); DataFieldPath(@NonNull String path) { super(path); + + if (!accepts(path)) { + throw new IllegalArgumentException("Invalid data field path: " + path); + } } public List elements() { @@ -21,6 +25,10 @@ public List elements() { .toList(); } + public static boolean accepts(String path) { + return PATTERN.matcher(path).matches(); + } + @Getter public static class Element { private final String identifier; diff --git a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java index ca37cb6..ece985c 100644 --- a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java +++ b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java @@ -12,7 +12,7 @@ protected FieldPath(@NonNull String path) { } public static FieldPath of(@NonNull String path) { - if (MetadataFieldPath.PATTERN.matcher(path).matches()) { + if (MetadataFieldPath.accepts(path)) { return ofMetadata(path); } @@ -24,10 +24,6 @@ public static MetadataFieldPath ofMetadata(@NonNull String path) { } public static DataFieldPath ofData(@NonNull String path) { - if (!DataFieldPath.PATTERN.matcher(path).matches()) { - throw new IllegalArgumentException("Invalid data field path: " + path); - } - return new DataFieldPath(path); } diff --git a/src/main/java/app/quickcase/sdk/spring/path/MetadataFieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/MetadataFieldPath.java index 75dbcc2..a7dc1ae 100644 --- a/src/main/java/app/quickcase/sdk/spring/path/MetadataFieldPath.java +++ b/src/main/java/app/quickcase/sdk/spring/path/MetadataFieldPath.java @@ -1,6 +1,6 @@ package app.quickcase.sdk.spring.path; -import java.util.regex.Pattern; +import java.util.Arrays; import app.quickcase.sdk.spring.metadata.Metadata; import lombok.Getter; @@ -8,8 +8,6 @@ @Getter public final class MetadataFieldPath extends FieldPath { - public static final Pattern PATTERN = Pattern.compile("^\\[[a-zA-Z_]+]$"); - final private Metadata metadata; MetadataFieldPath(@NonNull String path) { @@ -17,4 +15,7 @@ public final class MetadataFieldPath extends FieldPath { metadata = Metadata.fromPath(path); } + public static boolean accepts(@NonNull String path) { + return Arrays.stream(Metadata.values()).anyMatch(meta -> meta.getPath().equalsIgnoreCase(path)); + } } From 2c865d0b4af0ce9f554792ded3f2b301ad9a9fd2 Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Thu, 6 Feb 2025 16:13:16 +0000 Subject: [PATCH 2/7] Add `ComputedFieldPath` --- .../sdk/spring/path/ComputedFieldPath.java | 25 ++++++++++++++++ .../quickcase/sdk/spring/path/FieldPath.java | 8 +++++ .../sdk/spring/path/FieldPathTest.java | 30 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 src/main/java/app/quickcase/sdk/spring/path/ComputedFieldPath.java diff --git a/src/main/java/app/quickcase/sdk/spring/path/ComputedFieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/ComputedFieldPath.java new file mode 100644 index 0000000..d30c17e --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/path/ComputedFieldPath.java @@ -0,0 +1,25 @@ +package app.quickcase.sdk.spring.path; + +import java.util.regex.Pattern; + +import lombok.NonNull; + +public class ComputedFieldPath extends FieldPath { + public static final Pattern PATTERN = Pattern.compile("^:[a-zA-Z0-9_]+$"); + + protected ComputedFieldPath(@NonNull String path) { + super(path); + + if (!accepts(path)) { + throw new IllegalArgumentException("Invalid computed field path: " + path); + } + } + + public static boolean accepts(String path) { + return PATTERN.matcher(path).matches(); + } + + public String getIdentifier() { + return path.substring(1); + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java index ece985c..8c9b975 100644 --- a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java +++ b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java @@ -16,6 +16,10 @@ public static FieldPath of(@NonNull String path) { return ofMetadata(path); } + if (ComputedFieldPath.accepts(path)) { + return ofComputed(path); + } + return ofData(path); } @@ -27,6 +31,10 @@ public static DataFieldPath ofData(@NonNull String path) { return new DataFieldPath(path); } + public static ComputedFieldPath ofComputed(@NonNull String path) { + return new ComputedFieldPath(path); + } + @Override public String toString() { return path; diff --git a/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java b/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java index b4ce3ed..ee5894e 100644 --- a/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java +++ b/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java @@ -49,6 +49,25 @@ void shouldReturnDataFieldPath() { } } + @Nested + class OfComputed { + @Test + @DisplayName("should throw error when path is not for computed field") + void shouldThrowErrorWhenNotMetadata() { + var error = assertThrows(IllegalArgumentException.class, () -> FieldPath.ofComputed("field1")); + assertThat(error.getMessage(), equalTo("Invalid computed field path: field1")); + } + + @Test + @DisplayName("should return computed field path") + void shouldReturnComputedFieldPath() { + var path = FieldPath.ofComputed(":linkedRecordsCount"); + + assertThat(path.toString(), equalTo(":linkedRecordsCount")); + assertThat(path.getIdentifier(), equalTo("linkedRecordsCount")); + } + } + @Nested class Of { @Test @@ -62,6 +81,17 @@ void shouldReturnMetadataFieldPath() { ); } + @Test + @DisplayName("should return computed field path") + void shouldReturnComputedFieldPath() { + var path = FieldPath.of(":linkedRecordsCount"); + + assertAll( + () -> assertThat(path.toString(), equalTo(":linkedRecordsCount")), + () -> assertThat(path, is(instanceOf(ComputedFieldPath.class))) + ); + } + @Test @DisplayName("should return data field path") void shouldReturnDataFieldPath() { From 70e5fe181de5303d2f6673f01c8e9ea68fbe651a Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Fri, 7 Feb 2025 09:45:22 +0000 Subject: [PATCH 3/7] Add global field path validation Defers validation to all possible sub-classes --- .../quickcase/sdk/spring/path/FieldPath.java | 4 ++ .../sdk/spring/path/FieldPathTest.java | 51 +++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java index 8c9b975..98e4005 100644 --- a/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java +++ b/src/main/java/app/quickcase/sdk/spring/path/FieldPath.java @@ -11,6 +11,10 @@ protected FieldPath(@NonNull String path) { this.path = path; } + public static boolean accepts(@NonNull String path) { + return MetadataFieldPath.accepts(path) || ComputedFieldPath.accepts(path) || DataFieldPath.accepts(path); + } + public static FieldPath of(@NonNull String path) { if (MetadataFieldPath.accepts(path)) { return ofMetadata(path); diff --git a/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java b/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java index ee5894e..9f654ad 100644 --- a/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java +++ b/src/test/java/app/quickcase/sdk/spring/path/FieldPathTest.java @@ -4,6 +4,8 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -12,6 +14,55 @@ class FieldPathTest { + @Nested + class Accepts { + @ParameterizedTest + @ValueSource(strings = { + // metadata + "[type]", + "[state]", + // data field + "field1", + "another_field", + "complex1.member1", + "collection1[].value.member1", + "collection1[0].value.member1", + "collection1[id:item_1].value.member1", + // computed field + ":backlinksCount", + ":another_computed_field", + }) + @DisplayName("should accept valid field path") + void shouldAcceptValidFieldPath(String path) { + assertThat(FieldPath.accepts(path), is(true)); + } + + @ParameterizedTest + @ValueSource(strings = { + // metadata + "[invalidMeta]", + "[state", + "state]", + // data field + "fie:ld1", + "another field", + ".field2", + "complex1..member1", + "collection1[abc].value.member1", + "[0]collection1.value.member1", + "collection1[id:item_1]value.member1", + // computed field + ":", + ":computed:", + ":com:puted", + ":computed[0]", + }) + @DisplayName("should reject invalid field path") + void shouldRejectInvalidFieldPath(String path) { + assertThat(FieldPath.accepts(path), is(false)); + } + } + @Nested class OfMetadata { @Test From a529ddf1ed8718889aa6741c15b4dba585264ee9 Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Fri, 7 Feb 2025 10:48:01 +0000 Subject: [PATCH 4/7] Extend field path syntax support in condition Add support for: - metadata - collection items - computed fields --- .../tokens/extract/TokenCharacter.java | 7 ++++-- .../parse/state/FieldPathStateHandler.java | 5 ++-- .../tokens/extract/TokensExtractorTest.java | 15 +++++++++++ .../tokens/parse/TokensParserTest.java | 25 +++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java index 8de23f7..afa64ae 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java @@ -16,8 +16,11 @@ public enum TokenCharacter { DOUBLE_QUOTE('"'), PARENTHESIS_OPEN('('), PARENTHESIS_CLOSE(')'), + COLON(':'), DOT('.'), EQUAL('='), + SQUARE_BRACKET_OPEN('['), + SQUARE_BRACKET_CLOSE(']'), UNDERSCORE('_'); // ASCII code @@ -49,8 +52,8 @@ public static Boolean isText(int code) { // a-z return true; } - if (code == DOT.code || code == UNDERSCORE.code) { - // ._ + if (code == COLON.code || code == DOT.code || code == UNDERSCORE.code || code == SQUARE_BRACKET_OPEN.code || code == SQUARE_BRACKET_CLOSE.code) { + // :._[] return true; } diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/FieldPathStateHandler.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/FieldPathStateHandler.java index a5b779c..61d577f 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/FieldPathStateHandler.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/FieldPathStateHandler.java @@ -5,13 +5,12 @@ import app.quickcase.sdk.spring.condition.tokens.TextToken; import app.quickcase.sdk.spring.condition.tokens.Token; import app.quickcase.sdk.spring.condition.tokens.parse.ParsingContext; +import app.quickcase.sdk.spring.path.FieldPath; class FieldPathStateHandler implements ParsingStateHandler { - private static final Pattern REGEX = Pattern.compile("^[a-zA-Z0-9._]+$"); - @Override public Boolean accept(Token token) { - return token instanceof TextToken && REGEX.matcher(token.value()).matches(); + return token instanceof TextToken && FieldPath.accepts(token.value()); } @Override diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java index bc9beee..6b88951 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java @@ -84,6 +84,21 @@ private static Stream provideHappyTestCases() { text("OR"), text("NOT"), groupDelimiter("("), text("c"), operator("==="), number("3"), groupDelimiter(")") ) + ), + args( + "Support for all field path syntaxes", + "[state] == \"active\" " + + "AND :computedField == 2" + + "AND complexField.member1 == \"test\" " + + "AND collectionField[0].value == \"itemValue\" " + + "AND collectionField[id:item1] == \"itemValue\"", + array( + text("[state]"), operator("=="), quotedString("active"), + text("AND"), text(":computedField"), operator("=="), number("2"), + text("AND"), text("complexField.member1"), operator("=="), quotedString("test"), + text("AND"), text("collectionField[0].value"), operator("=="), quotedString("itemValue"), + text("AND"), text("collectionField[id:item1]"), operator("=="), quotedString("itemValue") + ) ) ); } diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java index ad3f4ac..8e02f24 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java @@ -203,6 +203,31 @@ void shouldParseNegatedCondition() { })); } + @Test + @DisplayName("should parse all valid field path syntaxes") + void shouldParseAllValidFieldPathSyntaxes() { + var tokens = condition( + text("[state]"), operator("==="), quotedString("active"), + text("AND"), text(":computedField"), operator("==="), number("2"), + text("AND"), text("complexField.member1"), operator("==="), quotedString("test"), + text("AND"), text("collectionField[0].value"), operator("==="), quotedString("itemValue"), + text("AND"), text("collectionField[id:item1]"), operator("==="), quotedString("itemValue") + ); + + final TokensParser parser = new TokensParser(); + assertThat(parser.parse(tokens), equalTo(new ConditionNode[]{ + criteria("[state]", "EQUALS", "active").build(), + AND, + criteria(":computedField", "EQUALS", 2).build(), + AND, + criteria("complexField.member1", "EQUALS", "test").build(), + AND, + criteria("collectionField[0].value", "EQUALS", "itemValue").build(), + AND, + criteria("collectionField[id:item1]", "EQUALS", "itemValue").build(), + })); + } + @Nested @DisplayName("EQUALS") class EqualsOperator { From 66b394cbba3d600e8ef6ef2cc784f203acbeec47 Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Fri, 7 Feb 2025 13:29:09 +0000 Subject: [PATCH 5/7] Add greater/less than condition operators --- .../tokens/extract/TokenCharacter.java | 4 + .../parse/state/OperatorStateHandler.java | 8 +- .../tokens/parse/state/ParsingState.java | 24 ++ .../tokens/extract/TokensExtractorTest.java | 19 ++ .../tokens/parse/TokensParserTest.java | 208 ++++++++++++++++++ 5 files changed, 262 insertions(+), 1 deletion(-) diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java index afa64ae..8f23d24 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/extract/TokenCharacter.java @@ -19,6 +19,8 @@ public enum TokenCharacter { COLON(':'), DOT('.'), EQUAL('='), + GREATER_THAN('>'), + LESS_THAN('<'), SQUARE_BRACKET_OPEN('['), SQUARE_BRACKET_CLOSE(']'), UNDERSCORE('_'); @@ -32,6 +34,8 @@ public enum TokenCharacter { public static final TokenCharacter[] OPERATOR_SYMBOLS = new TokenCharacter[]{ EQUAL, + GREATER_THAN, + LESS_THAN, }; public static final TokenCharacter[] GROUP_DELIMITERS = new TokenCharacter[]{ diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/OperatorStateHandler.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/OperatorStateHandler.java index 7cb466b..0691f95 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/OperatorStateHandler.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/OperatorStateHandler.java @@ -8,10 +8,16 @@ import app.quickcase.sdk.spring.condition.tokens.parse.ParsingContext; class OperatorStateHandler implements ParsingStateHandler { + private final String targetToken; private final String[] acceptTokens; private final ParsingState[] nextStates; public OperatorStateHandler(String[] acceptTokens, ParsingState[] nextStates) { + this(null, acceptTokens, nextStates); + } + + public OperatorStateHandler(String targetToken, String[] acceptTokens, ParsingState[] nextStates) { + this.targetToken = targetToken; this.acceptTokens = acceptTokens; this.nextStates = nextStates; } @@ -29,6 +35,6 @@ public ParsingState[] nextStates(ParsingContext context) { @Override public void apply(ParsingContext context, Token token) { - context.getCriteriaBuilder().operator(token.value()); + context.getCriteriaBuilder().operator(targetToken != null ? targetToken : token.value()); } } diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/ParsingState.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/ParsingState.java index 960daaa..cb4d7ff 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/ParsingState.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/state/ParsingState.java @@ -28,10 +28,30 @@ public enum ParsingState { new String[] {"ENDS_WITH_IC"}, new ParsingState[]{VALUE_STRING} )), + COMP_GREATER_THAN(new OperatorStateHandler( + "GREATER_THAN", + new String[] {"GREATER_THAN", ">"}, + new ParsingState[]{VALUE_NUMBER} + )), + COMP_GREATER_OR_EQUALS(new OperatorStateHandler( + "GREATER_OR_EQUALS", + new String[] {"GREATER_OR_EQUALS", ">="}, + new ParsingState[]{VALUE_NUMBER} + )), COMP_HAS_LENGTH(new OperatorStateHandler( new String[]{"HAS_LENGTH"}, new ParsingState[]{VALUE_NUMBER} )), + COMP_LESS_THAN(new OperatorStateHandler( + "LESS_THAN", + new String[] {"LESS_THAN", "<"}, + new ParsingState[]{VALUE_NUMBER} + )), + COMP_LESS_OR_EQUALS(new OperatorStateHandler( + "LESS_OR_EQUALS", + new String[] {"LESS_OR_EQUALS", "<="}, + new ParsingState[]{VALUE_NUMBER} + )), COMP_MATCHES(new OperatorStateHandler( new String[]{"MATCHES"}, new ParsingState[]{VALUE_STRING} @@ -53,7 +73,11 @@ public enum ParsingState { COMP_CONTAINS, COMP_EQUALS, COMP_ENDS_WITH, + COMP_GREATER_THAN, + COMP_GREATER_OR_EQUALS, COMP_HAS_LENGTH, + COMP_LESS_THAN, + COMP_LESS_OR_EQUALS, COMP_MATCHES, COMP_STARTS_WITH, }; diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java index 6b88951..32bbe1d 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/extract/TokensExtractorTest.java @@ -99,6 +99,25 @@ private static Stream provideHappyTestCases() { text("AND"), text("collectionField[0].value"), operator("=="), quotedString("itemValue"), text("AND"), text("collectionField[id:item1]"), operator("=="), quotedString("itemValue") ) + ), + args( + "Support for all operator symbols", + "field1 = 0 " + + "AND field2 == 0 " + + "AND field3 === 0 " + + "AND field4 > 0 " + + "AND field5 >= 0 " + + "AND field6 < 0 " + + "AND field7 <= 0", + array( + text("field1"), operator("="), number("0"), + text("AND"), text("field2"), operator("=="), number("0"), + text("AND"), text("field3"), operator("==="), number("0"), + text("AND"), text("field4"), operator(">"), number("0"), + text("AND"), text("field5"), operator(">="), number("0"), + text("AND"), text("field6"), operator("<"), number("0"), + text("AND"), text("field7"), operator("<="), number("0") + ) ) ); } diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java index 8e02f24..27a56f4 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java @@ -456,4 +456,212 @@ void shouldRejectQuotedStringValues() { ); } } + + @Nested + @DisplayName("GREATER_THAN") + class GreaterThanOperator { + @Test + @DisplayName("should parse all forms of GREATER_THAN operator") + void shouldParseAllEqualsOperator() { + var tokens = condition( + text("field1"), operator(">"), number("2"), text("AND"), + text("field2"), text("GREATER_THAN"), number("2") + ); + + final TokensParser parser = new TokensParser(); + assertThat(parser.parse(tokens), equalTo(new ConditionNode[]{ + criteria("field1", "GREATER_THAN", 2).build(), + AND, + criteria("field2", "GREATER_THAN", 2).build(), + })); + } + + @Test + @DisplayName("should reject text values") + void shouldRejectTextValues() { + var tokens = condition( + text("field1"), operator(">"), text("abc") // Non-quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token TextToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + + @Test + @DisplayName("should reject quoted string values") + void shouldRejectQuotedStringValues() { + var tokens = condition( + text("field2"), operator(">"), quotedString("abc") // Quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token QuotedStringToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + } + + @Nested + @DisplayName("GREATER_OR_EQUALS") + class GreaterOrEqualsOperator { + @Test + @DisplayName("should parse all forms of GREATER_OR_EQUALS operator") + void shouldParseAllEqualsOperator() { + var tokens = condition( + text("field1"), operator(">="), number("2"), text("AND"), + text("field2"), text("GREATER_OR_EQUALS"), number("2") + ); + + final TokensParser parser = new TokensParser(); + assertThat(parser.parse(tokens), equalTo(new ConditionNode[]{ + criteria("field1", "GREATER_OR_EQUALS", 2).build(), + AND, + criteria("field2", "GREATER_OR_EQUALS", 2).build(), + })); + } + + @Test + @DisplayName("should reject text values") + void shouldRejectTextValues() { + var tokens = condition( + text("field1"), operator(">="), text("abc") // Non-quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token TextToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + + @Test + @DisplayName("should reject quoted string values") + void shouldRejectQuotedStringValues() { + var tokens = condition( + text("field2"), operator(">="), quotedString("abc") // Quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token QuotedStringToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + } + + @Nested + @DisplayName("LESS_THAN") + class LessThanOperator { + @Test + @DisplayName("should parse all forms of LESS_THAN operator") + void shouldParseAllEqualsOperator() { + var tokens = condition( + text("field1"), operator("<"), number("2"), text("AND"), + text("field2"), text("LESS_THAN"), number("2") + ); + + final TokensParser parser = new TokensParser(); + assertThat(parser.parse(tokens), equalTo(new ConditionNode[]{ + criteria("field1", "LESS_THAN", 2).build(), + AND, + criteria("field2", "LESS_THAN", 2).build(), + })); + } + + @Test + @DisplayName("should reject text values") + void shouldRejectTextValues() { + var tokens = condition( + text("field1"), operator("<"), text("abc") // Non-quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token TextToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + + @Test + @DisplayName("should reject quoted string values") + void shouldRejectQuotedStringValues() { + var tokens = condition( + text("field2"), operator("<"), quotedString("abc") // Quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token QuotedStringToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + } + + @Nested + @DisplayName("LESS_OR_EQUALS") + class LessOrEqualsOperator { + @Test + @DisplayName("should parse all forms of LESS_OR_EQUALS operator") + void shouldParseAllEqualsOperator() { + var tokens = condition( + text("field1"), operator("<="), number("2"), text("AND"), + text("field2"), text("LESS_OR_EQUALS"), number("2") + ); + + final TokensParser parser = new TokensParser(); + assertThat(parser.parse(tokens), equalTo(new ConditionNode[]{ + criteria("field1", "LESS_OR_EQUALS", 2).build(), + AND, + criteria("field2", "LESS_OR_EQUALS", 2).build(), + })); + } + + @Test + @DisplayName("should reject text values") + void shouldRejectTextValues() { + var tokens = condition( + text("field1"), operator("<="), text("abc") // Non-quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token TextToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + + @Test + @DisplayName("should reject quoted string values") + void shouldRejectQuotedStringValues() { + var tokens = condition( + text("field2"), operator("<="), quotedString("abc") // Quoted string + ); + + final TokensParser parser = new TokensParser(); + final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + () -> parser.parse(tokens)); + assertThat( + exception.getMessage(), + equalTo("Unexpected token QuotedStringToken[value=abc], expected one of: VALUE_NUMBER") + ); + } + } } From c8cbf61e2f35354e44d19c676ffb754ee5a9ae1c Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Wed, 12 Feb 2025 17:16:09 +0000 Subject: [PATCH 6/7] Move and rename `ConditionSyntaxException` Surface the exception under a more explicit name as this is an exception which is excepted to be handled by consumers of the SDK --- .../condition/ConditionSyntaxException.java | 7 ++++ .../condition/tokens/parse/TokensParser.java | 6 ++-- .../tokens/parse/error/SyntaxException.java | 7 ---- .../tokens/parse/TokensParserTest.java | 34 +++++++++---------- 4 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java delete mode 100644 src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/error/SyntaxException.java diff --git a/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java b/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java new file mode 100644 index 0000000..eb5fa11 --- /dev/null +++ b/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java @@ -0,0 +1,7 @@ +package app.quickcase.sdk.spring.condition; + +public class ConditionSyntaxException extends RuntimeException { + public ConditionSyntaxException(String message) { + super(message); + } +} diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java index 2ad4929..853e4fd 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java @@ -6,7 +6,7 @@ import app.quickcase.sdk.spring.condition.ConditionNode; import app.quickcase.sdk.spring.condition.tokens.Token; -import app.quickcase.sdk.spring.condition.tokens.parse.error.SyntaxException; +import app.quickcase.sdk.spring.condition.ConditionSyntaxException; import app.quickcase.sdk.spring.condition.tokens.parse.state.ParsingState; /** @@ -28,7 +28,7 @@ public ConditionNode[] parse(Token[] tokens) { .filter((posState) -> posState.accept(token)) .findFirst(); if (nextState.isEmpty()) { - throw new SyntaxException(String.format( + throw new ConditionSyntaxException(String.format( "Unexpected token %s, expected one of: %s", token, formatStates(nextPossibleStates) @@ -42,7 +42,7 @@ public ConditionNode[] parse(Token[] tokens) { // Validate final state final ParsingState[] endStates = state.nextStates(context); if (!Arrays.asList(endStates).contains(ParsingState.END)) { - throw new SyntaxException("Unexpected end of condition, expected one of: " + formatStates(endStates)); + throw new ConditionSyntaxException("Unexpected end of condition, expected one of: " + formatStates(endStates)); } return context.rootNodes(); diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/error/SyntaxException.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/error/SyntaxException.java deleted file mode 100644 index 95d7cb4..0000000 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/error/SyntaxException.java +++ /dev/null @@ -1,7 +0,0 @@ -package app.quickcase.sdk.spring.condition.tokens.parse.error; - -public class SyntaxException extends RuntimeException { - public SyntaxException(String message) { - super(message); - } -} diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java index 27a56f4..d2a5fb0 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java @@ -6,7 +6,7 @@ import app.quickcase.sdk.spring.condition.Criteria; import app.quickcase.sdk.spring.condition.Group; import app.quickcase.sdk.spring.condition.tokens.Token; -import app.quickcase.sdk.spring.condition.tokens.parse.error.SyntaxException; +import app.quickcase.sdk.spring.condition.ConditionSyntaxException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -134,7 +134,7 @@ void shouldRejectGroupEndOutsideOfGroup() { var tokens = condition(text("field1"), operator("==="), quotedString("value1"), groupDelimiter(")")); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -148,7 +148,7 @@ void shouldRejectConditionMissingValue() { var tokens = condition(text("field1"), operator("===")); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -162,7 +162,7 @@ void shouldRejectConditionGroupNotClosed() { var tokens = condition(groupDelimiter("("), text("field1"), operator("==="), quotedString("value1")); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -277,7 +277,7 @@ void shouldRejectOtherValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -315,7 +315,7 @@ void shouldRejectNumericValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -353,7 +353,7 @@ void shouldRejectNumericValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -419,7 +419,7 @@ void shouldRejectNumericValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -448,7 +448,7 @@ void shouldRejectQuotedStringValues() { var tokens = condition(text("field1"), text("HAS_LENGTH"), quotedString("abc")); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -484,7 +484,7 @@ void shouldRejectTextValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -500,7 +500,7 @@ void shouldRejectQuotedStringValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -536,7 +536,7 @@ void shouldRejectTextValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -552,7 +552,7 @@ void shouldRejectQuotedStringValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -588,7 +588,7 @@ void shouldRejectTextValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -604,7 +604,7 @@ void shouldRejectQuotedStringValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -640,7 +640,7 @@ void shouldRejectTextValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), @@ -656,7 +656,7 @@ void shouldRejectQuotedStringValues() { ); final TokensParser parser = new TokensParser(); - final SyntaxException exception = Assertions.assertThrows(SyntaxException.class, + final ConditionSyntaxException exception = Assertions.assertThrows(ConditionSyntaxException.class, () -> parser.parse(tokens)); assertThat( exception.getMessage(), From af6ceaf919889f342626974c7be4a357a7d3545b Mon Sep 17 00:00:00 2001 From: Valentin Laurin Date: Wed, 12 Feb 2025 17:33:08 +0000 Subject: [PATCH 7/7] Make `ConditionSyntaxException` checked Force handling of condition syntax exceptions by consumers --- .../sdk/spring/condition/ConditionParser.java | 2 +- .../condition/ConditionSyntaxException.java | 2 +- .../condition/tokens/parse/TokensParser.java | 2 +- .../spring/condition/ConditionParserTest.java | 6 +++- .../tokens/parse/TokensParserTest.java | 36 +++++++++---------- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/main/java/app/quickcase/sdk/spring/condition/ConditionParser.java b/src/main/java/app/quickcase/sdk/spring/condition/ConditionParser.java index 40a78e9..0cdbee6 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/ConditionParser.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/ConditionParser.java @@ -9,7 +9,7 @@ public class ConditionParser { private final TokensParser parser = new TokensParser(); private final ConditionNormaliser normaliser = new ConditionNormaliser(); - public Condition parse(String conditionString) { + public Condition parse(String conditionString) throws ConditionSyntaxException { return new Condition(normaliser.normalise(parser.parse(extractor.extract(conditionString)))); } } diff --git a/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java b/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java index eb5fa11..09c79cc 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/ConditionSyntaxException.java @@ -1,6 +1,6 @@ package app.quickcase.sdk.spring.condition; -public class ConditionSyntaxException extends RuntimeException { +public class ConditionSyntaxException extends Exception { public ConditionSyntaxException(String message) { super(message); } diff --git a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java index 853e4fd..e7d0d39 100644 --- a/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java +++ b/src/main/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParser.java @@ -18,7 +18,7 @@ */ public class TokensParser { - public ConditionNode[] parse(Token[] tokens) { + public ConditionNode[] parse(Token[] tokens) throws ConditionSyntaxException { ParsingState state = ParsingState.START; ParsingContext context = new ParsingContext(); diff --git a/src/test/java/app/quickcase/sdk/spring/condition/ConditionParserTest.java b/src/test/java/app/quickcase/sdk/spring/condition/ConditionParserTest.java index d70d604..16673e7 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/ConditionParserTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/ConditionParserTest.java @@ -154,6 +154,10 @@ void nLevelGrouping() { private void assertCondition(String conditionString, Condition expected) { final ConditionParser parser = new ConditionParser(); - assertThat(parser.parse(conditionString), equalTo(expected)); + try { + assertThat(parser.parse(conditionString), equalTo(expected)); + } catch (ConditionSyntaxException e) { + throw new RuntimeException(e); + } } } diff --git a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java index d2a5fb0..2c51be4 100644 --- a/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java +++ b/src/test/java/app/quickcase/sdk/spring/condition/tokens/parse/TokensParserTest.java @@ -38,7 +38,7 @@ private static Group negatedGroup(ConditionNode...members) { @Test @DisplayName("should parse simple conjunction condition without grouping") - void shouldParseSimpleConjunctionNoGrouping() { + void shouldParseSimpleConjunctionNoGrouping() throws ConditionSyntaxException { var tokens = condition( text("field1"), operator("==="), quotedString("value1"), text("AND"), @@ -55,7 +55,7 @@ void shouldParseSimpleConjunctionNoGrouping() { @Test @DisplayName("should parse simple condition with redundant grouping") - void shouldParseSimpleConditionWithRedundantGrouping() { + void shouldParseSimpleConditionWithRedundantGrouping() throws ConditionSyntaxException { var tokens = condition( groupDelimiter("("), text("field1"), operator("==="), quotedString("value1"), @@ -72,7 +72,7 @@ void shouldParseSimpleConditionWithRedundantGrouping() { @Test @DisplayName("should parse composed condition with one level of grouping") - void shouldParseComposedConditionWithSingleLevelGrouping() { + void shouldParseComposedConditionWithSingleLevelGrouping() throws ConditionSyntaxException { var tokens = condition( groupDelimiter("("), text("a"), operator("==="), quotedString("1"), text("AND"), text("b"), operator("==="), quotedString("2"), @@ -101,7 +101,7 @@ void shouldParseComposedConditionWithSingleLevelGrouping() { @Test @DisplayName("should parse conditions with nested groups") - void shouldParseConditionWithNestedGrouping() { + void shouldParseConditionWithNestedGrouping() throws ConditionSyntaxException { var tokens = condition( groupDelimiter("("), groupDelimiter("("), text("a"), operator("==="), quotedString("1"), groupDelimiter(")"), @@ -172,7 +172,7 @@ void shouldRejectConditionGroupNotClosed() { @Test @DisplayName("should parse negated condition") - void shouldParseNegatedCondition() { + void shouldParseNegatedCondition() throws ConditionSyntaxException { var tokens = condition( groupDelimiter("("), text("a"), operator("==="), quotedString("1"), text("AND"), text("NOT"), text("b"), operator("==="), quotedString("2"), @@ -205,7 +205,7 @@ void shouldParseNegatedCondition() { @Test @DisplayName("should parse all valid field path syntaxes") - void shouldParseAllValidFieldPathSyntaxes() { + void shouldParseAllValidFieldPathSyntaxes() throws ConditionSyntaxException { var tokens = condition( text("[state]"), operator("==="), quotedString("active"), text("AND"), text(":computedField"), operator("==="), number("2"), @@ -233,7 +233,7 @@ void shouldParseAllValidFieldPathSyntaxes() { class EqualsOperator { @Test @DisplayName("should parse all forms of EQUALS operator") - void shouldParseAllEqualsOperator() { + void shouldParseAllEqualsOperator() throws ConditionSyntaxException { var tokens = condition( // Case insensitive text("field1"), operator("="), quotedString("a"), text("AND"), @@ -260,7 +260,7 @@ void shouldParseAllEqualsOperator() { @Test @DisplayName("should accept numeric criteria value") - void shouldAcceptNumericCriteriaValue() { + void shouldAcceptNumericCriteriaValue() throws ConditionSyntaxException { var tokens = condition(text("field1"), text("EQUALS"), number("1")); final TokensParser parser = new TokensParser(); @@ -291,7 +291,7 @@ void shouldRejectOtherValues() { class StartsWithOperator { @Test @DisplayName("should parse all forms of STARTS_WITH operator") - void shouldParseAllStartsWithOperator() { + void shouldParseAllStartsWithOperator() throws ConditionSyntaxException { var tokens = condition( // Case insensitive text("field1"), text("STARTS_WITH_IC"), quotedString("a"), text("AND"), @@ -329,7 +329,7 @@ void shouldRejectNumericValues() { class EndsWithOperator { @Test @DisplayName("should parse all forms of ENDS_WITH operator") - void shouldParseAllEndsWithOperator() { + void shouldParseAllEndsWithOperator() throws ConditionSyntaxException { var tokens = condition( // Case insensitive text("field1"), text("ENDS_WITH_IC"), quotedString("a"), text("AND"), @@ -367,7 +367,7 @@ void shouldRejectNumericValues() { class ContainsOperator { @Test @DisplayName("should parse all forms of CONTAINS operator") - void shouldParseAllContainsOperator() { + void shouldParseAllContainsOperator() throws ConditionSyntaxException { var tokens = condition( // Case insensitive text("field1"), text("CONTAINS_IC"), quotedString("a"), text("AND"), @@ -385,7 +385,7 @@ void shouldParseAllContainsOperator() { @Test @DisplayName("should accept numeric criteria value") - void shouldAcceptNumericCriteriaValue() { + void shouldAcceptNumericCriteriaValue() throws ConditionSyntaxException { var tokens = condition(text("field1"), text("CONTAINS"), number("1")); final TokensParser parser = new TokensParser(); @@ -400,7 +400,7 @@ void shouldAcceptNumericCriteriaValue() { class MatchesOperator { @Test @DisplayName("should parse MATCHES operator") - void shouldParseMatchesOperator() { + void shouldParseMatchesOperator() throws ConditionSyntaxException { var tokens = condition( text("field1"), text("MATCHES"), quotedString("^[a-z]{3}$") ); @@ -433,7 +433,7 @@ void shouldRejectNumericValues() { class HasLengthOperator { @Test @DisplayName("should parse HAS_LENGTH operator") - void shouldParseAllContainsOperator() { + void shouldParseAllContainsOperator() throws ConditionSyntaxException { var tokens = condition(text("field1"), text("HAS_LENGTH"), number("3")); final TokensParser parser = new TokensParser(); @@ -462,7 +462,7 @@ void shouldRejectQuotedStringValues() { class GreaterThanOperator { @Test @DisplayName("should parse all forms of GREATER_THAN operator") - void shouldParseAllEqualsOperator() { + void shouldParseAllEqualsOperator() throws ConditionSyntaxException { var tokens = condition( text("field1"), operator(">"), number("2"), text("AND"), text("field2"), text("GREATER_THAN"), number("2") @@ -514,7 +514,7 @@ void shouldRejectQuotedStringValues() { class GreaterOrEqualsOperator { @Test @DisplayName("should parse all forms of GREATER_OR_EQUALS operator") - void shouldParseAllEqualsOperator() { + void shouldParseAllEqualsOperator() throws ConditionSyntaxException { var tokens = condition( text("field1"), operator(">="), number("2"), text("AND"), text("field2"), text("GREATER_OR_EQUALS"), number("2") @@ -566,7 +566,7 @@ void shouldRejectQuotedStringValues() { class LessThanOperator { @Test @DisplayName("should parse all forms of LESS_THAN operator") - void shouldParseAllEqualsOperator() { + void shouldParseAllEqualsOperator() throws ConditionSyntaxException { var tokens = condition( text("field1"), operator("<"), number("2"), text("AND"), text("field2"), text("LESS_THAN"), number("2") @@ -618,7 +618,7 @@ void shouldRejectQuotedStringValues() { class LessOrEqualsOperator { @Test @DisplayName("should parse all forms of LESS_OR_EQUALS operator") - void shouldParseAllEqualsOperator() { + void shouldParseAllEqualsOperator() throws ConditionSyntaxException { var tokens = condition( text("field1"), operator("<="), number("2"), text("AND"), text("field2"), text("LESS_OR_EQUALS"), number("2")