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 e638a08
Show file tree
Hide file tree
Showing 15 changed files with 420 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
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ tasks {

withType<Javadoc>().configureEach {
options.encoding = "UTF-8"
(options as? StandardJavadocDocletOptions)?.tags(
"apiNote:a:API Note:"
)
}

withType<AbstractArchiveTask>().configureEach {
Expand Down
70 changes: 70 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,70 @@
/*
* The MIT License
*
* Copyright 2024 Edgar Asatryan <nstdio@gmail.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
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);
}
}
32 changes: 25 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 @@ -2,6 +2,7 @@
* The MIT License
*
* Copyright 2013-2014 Jakub Jirutka <jakub@jirutka.cz>.
* Copyright 2024 Edgar Asatryan <nstdio@gmail.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand All @@ -23,12 +24,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 +54,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 +117,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
62 changes: 58 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 @@ -2,6 +2,7 @@
* The MIT License
*
* Copyright 2013-2014 Jakub Jirutka <jakub@jirutka.cz>.
* Copyright 2024 Edgar Asatryan <nstdio@gmail.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -36,8 +37,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 +47,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 +80,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 +155,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
55 changes: 55 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,55 @@
/*
* The MIT License
*
* Copyright 2024 Edgar Asatryan <nstdio@gmail.com>.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
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;
}
}
Loading

0 comments on commit e638a08

Please sign in to comment.