Skip to content

Commit

Permalink
feat: Add support for operators without arguments =null=, =notnull=.
Browse files Browse the repository at this point in the history
This commit introduces mor flexible way of parsing and validating operators and it's arguments.
Two new operators `=null=` and `=notnull=` were added which wasn't possible previously due to grammar and API level limitations.

Closes: gh-8
  • Loading branch information
nstdio committed May 1, 2024
1 parent 5b385c0 commit 85b0763
Show file tree
Hide file tree
Showing 14 changed files with 341 additions and 24 deletions.
7 changes: 5 additions & 2 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ Comparison operators are in FIQL notation and some of them has an alternative sy
* Greater than or equal to : `=ge=` or `>=`
* In : `=in=`
* Not in : `=out=`
* Null: `=null=`
* Not Null: `=notnull=`

You can also simply extend this parser with your own operators (see the <<How to add custom operators, next section>>).

Expand All @@ -94,11 +96,11 @@ comp-fiql = ( ( "=", { ALPHA } ) | "!" ), "=";
comp-alt = ( ">" | "<" ), [ "=" ];
----

Argument can be a single value, or multiple values in parenthesis separated by comma.
Argument can be no value, single value, or multiple values in parenthesis separated by comma.
Value that doesn’t contain any reserved character or a white space can be unquoted, other arguments must be enclosed in single or double quotes.

----
arguments = ( "(", value, { "," , value }, ")" ) | value;
arguments = ( "(", value, { "," , value }, ")" ) | ( value )?;
value = unreserved-str | double-quoted | single-quoted;
unreserved-str = unreserved, { unreserved }
Expand Down Expand Up @@ -130,6 +132,7 @@ Examples of RSQL expressions in both FIQL-like and alternative notation:
- director.lastName==Nolan and year>=2000 and year<2010
- genres=in=(sci-fi,action);genres=out=(romance,animated,horror),director==Que*Tarantino
- genres=in=(sci-fi,action) and genres=out=(romance,animated,horror) or director==Que*Tarantino
- year=notnull= and director.lastName=isnull=
----

== How to use
Expand Down
47 changes: 47 additions & 0 deletions src/main/java/cz/jirutka/rsql/parser/ast/Arity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cz.jirutka.rsql.parser.ast;

/**
* The arity of an operator.
*
* @since 2.3.0
*/
public interface Arity {

/**
* The minimum number of arguments operator can receive.
*
* @return The minimum number of arguments operator can receive. Positive or zero.
* @apiNote The minimum values is always less than or equal to {@linkplain #max()}
*/
int min();

/**
* The maximum number of arguments operator can receive.
*
* @return The maximum number of arguments operator can receive. Positive or zero.
* @apiNote The maximum values is always greater than or equal to {@linkplain #min()}. For practically unlimited
* arity the implementations should return {@link Integer#MAX_VALUE}.
*/
int max();

/**
* Creates arity with given {@code min} and {@code max}.
*
* @param min The minimum number of arguments. Must be zero or positive.
* @param max The maximum number of arguments. Must be zero or positive and greater than or equal to {@code min}.
* @return the created arity
*/
static Arity of(int min, int max) {
return new DynamicArity(min, max);
}

/**
* Creates N-ary object.
*
* @param n The N.
* @return the created arity
*/
static Arity nary(int n) {
return new NAry(n);
}
}
31 changes: 24 additions & 7 deletions src/main/java/cz/jirutka/rsql/parser/ast/ComparisonNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,11 @@
*/
package cz.jirutka.rsql.parser.ast;

import net.jcip.annotations.Immutable;
import static cz.jirutka.rsql.parser.ast.StringUtils.join;

import java.util.ArrayList;
import java.util.List;

import static cz.jirutka.rsql.parser.ast.StringUtils.join;
import net.jcip.annotations.Immutable;

/**
* This node represents a comparison with operator, selector and arguments,
Expand All @@ -54,9 +53,8 @@ public final class ComparisonNode extends AbstractNode {
public ComparisonNode(ComparisonOperator operator, String selector, List<String> arguments) {
Assert.notNull(operator, "operator must not be null");
Assert.notBlank(selector, "selector must not be blank");
Assert.notEmpty(arguments, "arguments list must not be empty");
Assert.isTrue(operator.isMultiValue() || arguments.size() == 1,
"operator %s expects single argument, but multiple values given", operator);
Assert.notNull(arguments, "arguments must not be null");
validate(operator, arguments.size());

this.operator = operator;
this.selector = selector;
Expand Down Expand Up @@ -118,9 +116,28 @@ public ComparisonNode withArguments(List<String> newArguments) {
return new ComparisonNode(operator, selector, newArguments);
}

private static void validate(ComparisonOperator operator, int argc) {
Arity arity = operator.getArity();
int min = arity.min();
int max = arity.max();

if (argc < min || argc > max) {
final String message;
if (min == max) {
message = String.format("operator '%s' can have exactly %d argument(s), but got %d",
operator.getSymbol(), max, argc);
} else {
message = String.format("operator '%s' can have from %d to %d argument(s), but got %d",
operator.getSymbol(), min, max, argc);
}

throw new IllegalArgumentException(message);
}
}

@Override
public String toString() {
String args = operator.isMultiValue()
String args = operator.getArity().max() > 1
? join(arguments, "','", "('", "')")
: "'" + arguments.get(0) + "'";
return selector + operator + args;
Expand Down
61 changes: 57 additions & 4 deletions src/main/java/cz/jirutka/rsql/parser/ast/ComparisonOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,7 @@ public final class ComparisonOperator {

private final String[] symbols;

private final boolean multiValue;

private final Arity arity;

/**
* @param symbols Textual representation of this operator (e.g. <tt>=gt=</tt>); the first item
Expand All @@ -47,13 +46,30 @@ public final class ComparisonOperator {
* validated in {@link NodesFactory}.
* @throws IllegalArgumentException If the {@code symbols} is either <tt>null</tt>, empty,
* or contain illegal symbols.
* @see #ComparisonOperator(String[], Arity)
* @deprecated in favor of {@linkplain #ComparisonOperator(String[], Arity)}
*/
@Deprecated
public ComparisonOperator(String[] symbols, boolean multiValue) {
this(symbols, multiValue ? Arity.of(1, Integer.MAX_VALUE) : Arity.nary(1));
}

/**
* @param symbols Textual representation of this operator (e.g. <tt>=gt=</tt>); the first item is primary
* representation, any others are alternatives. Must match {@literal =[a-zA-Z]*=|[><]=?|!=}.
* @param arity Arity of this operator.
* @throws IllegalArgumentException If the {@code symbols} is either <tt>null</tt>, empty, or contain illegal
* symbols.
* @since 2.3.0
*/
public ComparisonOperator(String[] symbols, Arity arity) {
Assert.notEmpty(symbols, "symbols must not be null or empty");
Assert.notNull(arity, "arity must not be null");
for (String sym : symbols) {
Assert.isTrue(isValidOperatorSymbol(sym), "symbol must match: %s", SYMBOL_PATTERN);
}
this.multiValue = multiValue;

this.arity = arity;
this.symbols = symbols.clone();
}

Expand All @@ -63,22 +79,48 @@ public ComparisonOperator(String[] symbols, boolean multiValue) {
* @param multiValue Whether this operator may be used with multiple arguments. This is then
* validated in {@link NodesFactory}.
* @see #ComparisonOperator(String[], boolean)
* @deprecated in favor of {@linkplain #ComparisonOperator(String, Arity)}
*/
@Deprecated
public ComparisonOperator(String symbol, boolean multiValue) {
this(new String[]{symbol}, multiValue);
}

/**
* @param symbol Textual representation of this operator (e.g. <tt>=gt=</tt>); Must match
* {@literal =[a-zA-Z]*=|[><]=?|!=}.
* @param arity Arity of this operator.
* @see #ComparisonOperator(String[], boolean)
* @since 2.3.0
*/
public ComparisonOperator(String symbol, Arity arity) {
this(new String[]{symbol}, arity);
}

/**
* @param symbol Textual representation of this operator (e.g. <tt>=gt=</tt>); Must match
* {@literal =[a-zA-Z]*=|[><]=?|!=}.
* @param altSymbol Alternative representation for {@code symbol}.
* @param multiValue Whether this operator may be used with multiple arguments. This is then
* @see #ComparisonOperator(String[], boolean)
* @deprecated in favor of {@linkplain #ComparisonOperator(String, String, Arity)}
*/
public ComparisonOperator(String symbol, String altSymbol, boolean multiValue) {
this(new String[]{symbol, altSymbol}, multiValue);
}

/**
* @param symbol Textual representation of this operator (e.g. <tt>=gt=</tt>); Must match
* {@literal =[a-zA-Z]*=|[><]=?|!=}.
* @param altSymbol Alternative representation for {@code symbol}.
* @param arity Arity of this operator.
* @see #ComparisonOperator(String[], boolean)
* @since 2.3.0
*/
public ComparisonOperator(String symbol, String altSymbol, Arity arity) {
this(new String[]{symbol, altSymbol}, arity);
}

/**
* @param symbols Textual representation of this operator (e.g. <tt>=gt=</tt>); the first item
* is primary representation, any others are alternatives. Must match {@literal =[a-zA-Z]*=|[><]=?|!=}.
Expand Down Expand Up @@ -112,11 +154,22 @@ public String[] getSymbols() {
* Whether this operator may be used with multiple arguments.
*
* @return Whether this operator may be used with multiple arguments.
* @deprecated use {@linkplain #getArity()}
*/
@Deprecated
public boolean isMultiValue() {
return multiValue;
return arity.max() > 1;
}

/**
* Returns the arity of this operator.
*
* @return the arity of this operator.
* @since 2.3.0
*/
public Arity getArity() {
return arity;
}

/**
* Whether the given string can represent an operator.
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/cz/jirutka/rsql/parser/ast/DynamicArity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package cz.jirutka.rsql.parser.ast;

final class DynamicArity implements Arity {

private final int min;
private final int max;

DynamicArity(int min, int max) {
if (min < 0) {
throw new IllegalArgumentException("min must be positive or zero");
}
if (max < 0) {
throw new IllegalArgumentException("max must be positive or zero");
}
if (min > max) {
throw new IllegalArgumentException("min must be less than or equal to max");
}

this.min = min;
this.max = max;
}

@Override
public int min() {
return min;
}

@Override
public int max() {
return max;
}
}
24 changes: 24 additions & 0 deletions src/main/java/cz/jirutka/rsql/parser/ast/NAry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cz.jirutka.rsql.parser.ast;

final class NAry implements Arity {

private final int n;

NAry(int n) {
if (n < 0) {
throw new IllegalArgumentException("n must be positive or zero");
}

this.n = n;
}

@Override
public int min() {
return n;
}

@Override
public int max() {
return n;
}
}
20 changes: 11 additions & 9 deletions src/main/java/cz/jirutka/rsql/parser/ast/RSQLOperators.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,20 @@
public abstract class RSQLOperators {

public static final ComparisonOperator
EQUAL = new ComparisonOperator("=="),
NOT_EQUAL = new ComparisonOperator("!="),
GREATER_THAN = new ComparisonOperator("=gt=", ">"),
GREATER_THAN_OR_EQUAL = new ComparisonOperator("=ge=", ">="),
LESS_THAN = new ComparisonOperator("=lt=", "<"),
LESS_THAN_OR_EQUAL = new ComparisonOperator("=le=", "<="),
IN = new ComparisonOperator("=in=", true),
NOT_IN = new ComparisonOperator("=out=", true);
EQUAL = new ComparisonOperator("==", Arity.nary(1)),
NOT_EQUAL = new ComparisonOperator("!=", Arity.nary(1)),
GREATER_THAN = new ComparisonOperator("=gt=", ">", Arity.nary(1)),
GREATER_THAN_OR_EQUAL = new ComparisonOperator("=ge=", ">=", Arity.nary(1)),
LESS_THAN = new ComparisonOperator("=lt=", "<", Arity.nary(1)),
LESS_THAN_OR_EQUAL = new ComparisonOperator("=le=", "<=", Arity.nary(1)),
IN = new ComparisonOperator("=in=", Arity.of(1, Integer.MAX_VALUE)),
NOT_IN = new ComparisonOperator("=out=", Arity.of(1, Integer.MAX_VALUE)),
IS_NULL = new ComparisonOperator("=null=", Arity.nary(0)),
NOT_NULL = new ComparisonOperator("=notnull=", Arity.nary(0));


public static Set<ComparisonOperator> defaultOperators() {
return new HashSet<>(asList(EQUAL, NOT_EQUAL, GREATER_THAN, GREATER_THAN_OR_EQUAL,
LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN));
LESS_THAN, LESS_THAN_OR_EQUAL, IN, NOT_IN, IS_NULL, NOT_NULL));
}
}
6 changes: 5 additions & 1 deletion src/main/javacc/RSQLParser.jj
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

final class Parser {
Expand Down Expand Up @@ -206,7 +207,10 @@ List<String> Arguments():
{
( <LPAREN> value = CommaSepArguments() <RPAREN> ) { return (List) value; }
|
value = Argument() { return Arrays.asList((String) value); }
(value = Argument() { return Arrays.asList((String) value); })?
{
return Collections.emptyList();
}
}

List<String> CommaSepArguments():
Expand Down
Loading

0 comments on commit 85b0763

Please sign in to comment.