Expression language (#363)
* Configured the sql package Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Bootstrap implementation Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Literal done Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * More progress Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Progress Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Sync contract Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fix type cohercion for event type system Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * In expression + sync grammar Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Implemented binary expressions Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Implemented Like expression Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Big refactor Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * More testing Fix math Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Implemented all the functions! Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Refactored logical expressions implementation More testing Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * More coverage and tests Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fixed ConcatFunction and added ConcatWSFunction Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fixed IN type casting Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Added ABS function Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fix SUBSTRING implementation Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * More nits Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * WIP Javadoc-ing Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fix division by 0 Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Bootstrapped TCK Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Added comparison operators to tck Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Added logical operators, case sensitivity and casting functions Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Copied all the tests to the tck Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Removed Java tests now covered by the TCK Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Added integer builtin test case Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Added fail fast evaluation mode Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * More changes More Javadoc Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Typo Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Fix bad javadoc Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Another CONCAT_WS test case Signed-off-by: Francesco Guardiani <francescoguard@gmail.com> * Import yaml just for testing Signed-off-by: Francesco Guardiani <francescoguard@gmail.com>
This commit is contained in:
parent
30fd6769eb
commit
2730ae4a13
1
pom.xml
1
pom.xml
|
@ -76,6 +76,7 @@
|
|||
<module>http/restful-ws</module>
|
||||
<module>kafka</module>
|
||||
<module>spring</module>
|
||||
<module>sql</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>cloudevents-parent</artifactId>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<version>2.1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>cloudevents-sql</artifactId>
|
||||
|
||||
<properties>
|
||||
<module-name>io.cloudevents.sql</module-name>
|
||||
<antlr.version>4.9.2</antlr.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-api</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr4-runtime</artifactId>
|
||||
<version>${antlr.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test deps -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit-jupiter.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.assertj</groupId>
|
||||
<artifactId>assertj-core</artifactId>
|
||||
<version>${assertj-core.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-json-jackson</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>2.11.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.cloudevents</groupId>
|
||||
<artifactId>cloudevents-core</artifactId>
|
||||
<classifier>tests</classifier>
|
||||
<type>test-jar</type>
|
||||
<version>${project.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.antlr</groupId>
|
||||
<artifactId>antlr4-maven-plugin</artifactId>
|
||||
<version>${antlr.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>antlr4</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<visitor>true</visitor>
|
||||
<listener>true</listener> <!-- TODO do we need the listener? -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,79 @@
|
|||
lexer grammar CESQLLexer;
|
||||
|
||||
// NOTE:
|
||||
// This grammar is case-sensitive, although CESQL keywords are case-insensitive.
|
||||
// In order to implement case-insensitivity, check out
|
||||
// https://github.com/antlr/antlr4/blob/master/doc/case-insensitive-lexing.md#custom-character-streams-approach
|
||||
|
||||
// Skip tab, carriage return and newlines
|
||||
|
||||
SPACE: [ \t\r\n]+ -> skip;
|
||||
|
||||
// Fragments for Literal primitives
|
||||
|
||||
fragment ID_LITERAL: [a-zA-Z0-9]+;
|
||||
fragment DQUOTA_STRING: '"' ( '\\'. | '""' | ~('"'| '\\') )* '"';
|
||||
fragment SQUOTA_STRING: '\'' ('\\'. | '\'\'' | ~('\'' | '\\'))* '\'';
|
||||
fragment INT_DIGIT: [0-9];
|
||||
fragment FN_LITERAL: [A-Z] [A-Z_]*;
|
||||
|
||||
// Constructors symbols
|
||||
|
||||
LR_BRACKET: '(';
|
||||
RR_BRACKET: ')';
|
||||
COMMA: ',';
|
||||
SINGLE_QUOTE_SYMB: '\'';
|
||||
DOUBLE_QUOTE_SYMB: '"';
|
||||
|
||||
fragment QUOTE_SYMB
|
||||
: SINGLE_QUOTE_SYMB | DOUBLE_QUOTE_SYMB
|
||||
;
|
||||
|
||||
// Operators
|
||||
// - Logic
|
||||
|
||||
AND: 'AND';
|
||||
OR: 'OR';
|
||||
XOR: 'XOR';
|
||||
NOT: 'NOT';
|
||||
|
||||
// - Arithmetics
|
||||
|
||||
STAR: '*';
|
||||
DIVIDE: '/';
|
||||
MODULE: '%';
|
||||
PLUS: '+';
|
||||
MINUS: '-';
|
||||
|
||||
// - Comparison
|
||||
|
||||
EQUAL: '=';
|
||||
NOT_EQUAL: '!=';
|
||||
GREATER: '>';
|
||||
GREATER_OR_EQUAL: '>=';
|
||||
LESS: '<';
|
||||
LESS_GREATER: '<>';
|
||||
LESS_OR_EQUAL: '<=';
|
||||
|
||||
// Like, exists, in
|
||||
|
||||
LIKE: 'LIKE';
|
||||
EXISTS: 'EXISTS';
|
||||
IN: 'IN';
|
||||
|
||||
// Booleans
|
||||
|
||||
TRUE: 'TRUE';
|
||||
FALSE: 'FALSE';
|
||||
|
||||
// Literals
|
||||
|
||||
DQUOTED_STRING_LITERAL: DQUOTA_STRING;
|
||||
SQUOTED_STRING_LITERAL: SQUOTA_STRING;
|
||||
INTEGER_LITERAL: INT_DIGIT+;
|
||||
|
||||
// Identifiers
|
||||
|
||||
IDENTIFIER: [a-zA-Z]+;
|
||||
IDENTIFIER_WITH_NUMBER: [a-zA-Z0-9]+;
|
||||
FUNCTION_IDENTIFIER_WITH_UNDERSCORE: [A-Z] [A-Z_]*;
|
|
@ -0,0 +1,62 @@
|
|||
grammar CESQLParser;
|
||||
|
||||
import CESQLLexer;
|
||||
|
||||
// Entrypoint
|
||||
cesql: expression EOF;
|
||||
|
||||
// Structure of operations, function invocations and expression
|
||||
expression
|
||||
: functionIdentifier functionParameterList #functionInvocationExpression
|
||||
// unary operators are the highest priority
|
||||
| NOT expression #unaryLogicExpression
|
||||
| MINUS expression # unaryNumericExpression
|
||||
// LIKE, EXISTS and IN takes precedence over all the other binary operators
|
||||
| expression NOT? LIKE stringLiteral #likeExpression
|
||||
| EXISTS identifier #existsExpression
|
||||
| expression NOT? IN setExpression #inExpression
|
||||
// Numeric operations
|
||||
| expression (STAR | DIVIDE | MODULE) expression #binaryMultiplicativeExpression
|
||||
| expression (PLUS | MINUS) expression #binaryAdditiveExpression
|
||||
// Comparison operations
|
||||
| expression (EQUAL | NOT_EQUAL | LESS_GREATER | GREATER_OR_EQUAL | LESS_OR_EQUAL | LESS | GREATER) expression #binaryComparisonExpression
|
||||
// Logic operations
|
||||
|<assoc=right> expression (AND | OR | XOR) expression #binaryLogicExpression
|
||||
// Subexpressions and atoms
|
||||
| LR_BRACKET expression RR_BRACKET #subExpression
|
||||
| atom #atomExpression
|
||||
;
|
||||
|
||||
atom
|
||||
: booleanLiteral #booleanAtom
|
||||
| integerLiteral #integerAtom
|
||||
| stringLiteral #stringAtom
|
||||
| identifier #identifierAtom
|
||||
;
|
||||
|
||||
// Identifiers
|
||||
|
||||
identifier
|
||||
: (IDENTIFIER | IDENTIFIER_WITH_NUMBER)
|
||||
;
|
||||
functionIdentifier
|
||||
: (IDENTIFIER | FUNCTION_IDENTIFIER_WITH_UNDERSCORE)
|
||||
;
|
||||
|
||||
// Literals
|
||||
|
||||
booleanLiteral: (TRUE | FALSE);
|
||||
stringLiteral: (DQUOTED_STRING_LITERAL | SQUOTED_STRING_LITERAL);
|
||||
integerLiteral: INTEGER_LITERAL;
|
||||
|
||||
// Functions
|
||||
|
||||
functionParameterList
|
||||
: LR_BRACKET ( expression ( COMMA expression )* )? RR_BRACKET
|
||||
;
|
||||
|
||||
// Sets
|
||||
|
||||
setExpression
|
||||
: LR_BRACKET expression ( COMMA expression )* RR_BRACKET // Empty sets are not allowed
|
||||
;
|
|
@ -0,0 +1,36 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
/**
|
||||
* This class wraps some elements of the evaluation context,
|
||||
* required to throw {@link EvaluationException} when an error occurs while evaluating a function.
|
||||
*/
|
||||
public interface EvaluationContext {
|
||||
|
||||
/**
|
||||
* @return the interval of the original expression string.
|
||||
*/
|
||||
Interval expressionInterval();
|
||||
|
||||
/**
|
||||
* @return the text of the original expression string.
|
||||
*/
|
||||
String expressionText();
|
||||
|
||||
/**
|
||||
* Append a new exception to the evaluation context.
|
||||
* This exception will be propagated back in the evaluation result.
|
||||
*
|
||||
* @param exception exception to append
|
||||
*/
|
||||
void appendException(EvaluationException exception);
|
||||
|
||||
/**
|
||||
* Append a new exception to the evaluation context.
|
||||
* This exception will be propagated back in the evaluation result.
|
||||
*
|
||||
* @param exceptionFactory exception factory, which will automatically include expression interval and text
|
||||
*/
|
||||
void appendException(EvaluationException.EvaluationExceptionFactory exceptionFactory);
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
/**
|
||||
* This exception represents an evaluation exception when evaluating an {@link Expression}.
|
||||
* Using {@link #getKind()} you can inspect what kind of failure happened, implementing proper failure handling.
|
||||
*/
|
||||
public class EvaluationException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* Interface to simplify the construction of an {@link EvaluationException}.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EvaluationExceptionFactory {
|
||||
EvaluationException create(Interval interval, String expression);
|
||||
}
|
||||
|
||||
public enum ErrorKind {
|
||||
/**
|
||||
* An implicit or an explicit casting failed.
|
||||
*/
|
||||
INVALID_CAST,
|
||||
/**
|
||||
* An event attribute was addressed, but missing.
|
||||
*/
|
||||
MISSING_ATTRIBUTE,
|
||||
/**
|
||||
* Error happened while dispatching a function invocation. Reasons may be invalid function name or invalid arguments number.
|
||||
*/
|
||||
FUNCTION_DISPATCH,
|
||||
/**
|
||||
* Error happened while executing a function. This usually contains a non null cause.
|
||||
*/
|
||||
FUNCTION_EXECUTION,
|
||||
/**
|
||||
* Error happened while executing a math operation. Reason may be a division by zero.
|
||||
*/
|
||||
MATH
|
||||
}
|
||||
|
||||
private final ErrorKind errorKind;
|
||||
private final Interval interval;
|
||||
private final String expression;
|
||||
|
||||
protected EvaluationException(ErrorKind errorKind, Interval interval, String expression, String message, Throwable cause) {
|
||||
super(String.format("%s at %s `%s`: %s", errorKind.name(), interval.toString(), expression, message), cause);
|
||||
this.errorKind = errorKind;
|
||||
this.interval = interval;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
public ErrorKind getKind() {
|
||||
return errorKind;
|
||||
}
|
||||
|
||||
public Interval getExpressionInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public String getExpressionText() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
public static EvaluationExceptionFactory invalidCastTarget(Class<?> from, Class<?> to) {
|
||||
return (interval, expression) -> new EvaluationException(
|
||||
ErrorKind.INVALID_CAST,
|
||||
interval,
|
||||
expression,
|
||||
"Cannot cast " + from + " to " + to + ": no cast defined.",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static EvaluationExceptionFactory castError(Class<?> from, Class<?> to, Throwable cause) {
|
||||
return (interval, expression) -> new EvaluationException(
|
||||
ErrorKind.INVALID_CAST,
|
||||
interval,
|
||||
expression,
|
||||
"Cannot cast " + from + " to " + to + ": " + cause.getMessage(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
public static EvaluationException missingAttribute(Interval interval, String expression, String key) {
|
||||
return new EvaluationException(
|
||||
ErrorKind.MISSING_ATTRIBUTE,
|
||||
interval,
|
||||
expression,
|
||||
"Missing attribute " + key + " in the input event. Perhaps you should check with 'EXISTS " + key + "' if the input contains the provided key?",
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
public static EvaluationException cannotDispatchFunction(Interval interval, String expression, String functionName, Throwable cause) {
|
||||
return new EvaluationException(
|
||||
ErrorKind.FUNCTION_DISPATCH,
|
||||
interval,
|
||||
expression,
|
||||
"Cannot dispatch function invocation to function " + functionName + ": " + cause.getMessage(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
public static EvaluationExceptionFactory functionExecutionError(String functionName, Throwable cause) {
|
||||
return (interval, expression) -> new EvaluationException(
|
||||
ErrorKind.FUNCTION_EXECUTION,
|
||||
interval,
|
||||
expression,
|
||||
"Error while executing " + functionName + ": " + cause.getMessage(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
public static EvaluationException divisionByZero(Interval interval, String expression, Integer dividend) {
|
||||
return new EvaluationException(
|
||||
ErrorKind.MATH,
|
||||
interval,
|
||||
expression,
|
||||
"Division by zero: " + dividend + " / 0",
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.sql.impl.EvaluationRuntimeBuilder;
|
||||
import io.cloudevents.sql.impl.EvaluationRuntimeImpl;
|
||||
|
||||
/**
|
||||
* The evaluation runtime takes care of the function resolution, casting and other core functionalities to execute an expression.
|
||||
*/
|
||||
public interface EvaluationRuntime {
|
||||
|
||||
/**
|
||||
* Check if the cast can be executed from {@code value} to the {@code target} type.
|
||||
*
|
||||
* @param value the value to cast
|
||||
* @param target the type cast target
|
||||
* @return false if the cast trigger an error, true otherwise.
|
||||
*/
|
||||
boolean canCast(Object value, Type target);
|
||||
|
||||
/**
|
||||
* Return the {@code value} casted to the {@code target} type.
|
||||
*
|
||||
* @param ctx the evaluation context
|
||||
* @param value the value to cast
|
||||
* @param target the type cast target
|
||||
* @return the casted value, if the cast succeeds, otherwise the default value of the target type
|
||||
*/
|
||||
Object cast(EvaluationContext ctx, Object value, Type target);
|
||||
|
||||
/**
|
||||
* Resolve a {@link Function} starting from its name and the concrete number of arguments.
|
||||
*
|
||||
* @param name the name of the function
|
||||
* @param args the number of arguments passed to the function
|
||||
* @return the resolved function
|
||||
* @throws IllegalStateException if the function cannot be resolved
|
||||
*/
|
||||
Function resolveFunction(String name, int args) throws IllegalStateException;
|
||||
|
||||
/**
|
||||
* @return a new builder to create a custom evaluation runtime
|
||||
*/
|
||||
static EvaluationRuntimeBuilder builder() {
|
||||
return new EvaluationRuntimeBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the default evaluation runtime
|
||||
*/
|
||||
static EvaluationRuntime getDefault() {
|
||||
return EvaluationRuntimeImpl.getInstance();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
|
||||
/**
|
||||
* This class represents a parsed expression, ready to be executed.
|
||||
* <p>
|
||||
* You can execute the Expression in one of the two modes, depending on your use case:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Use {@link #evaluate(EvaluationRuntime, CloudEvent)} to evaluate the expression without interrupting on the first error. The returned {@link Result} will contain a non-null evaluation result and, eventually, one or more failures</li>
|
||||
* <li>Use {@link #tryEvaluate(EvaluationRuntime, CloudEvent)} to evaluate the expression, failing as soon as an error happens. This function either returns the evaluation result, or throws an exception with the first evaluation error.</li>
|
||||
* </ul>
|
||||
*
|
||||
* The former approach gives more flexibility and allows to implement proper error handling,
|
||||
* while the latter is generally faster, because as soon as a failure happens the execution is interrupted.
|
||||
* <p>
|
||||
* The execution of an {@link Expression} is thread safe, in the sense that no state is shared with other {@link Expression} and it doesn't mutate the state of {@link EvaluationRuntime}.
|
||||
*/
|
||||
public interface Expression {
|
||||
|
||||
/**
|
||||
* Evaluate the expression.
|
||||
*
|
||||
* @param evaluationRuntime the runtime instance to use to run the evaluation
|
||||
* @param event the input event
|
||||
* @return the evaluation result, encapsulated in a {@link Result} type
|
||||
*/
|
||||
Result evaluate(EvaluationRuntime evaluationRuntime, CloudEvent event);
|
||||
|
||||
/**
|
||||
* Evaluate the expression, but throw an {@link EvaluationException} as soon as the evaluation fails.
|
||||
*
|
||||
* @param evaluationRuntime the runtime instance to use to run the evaluation
|
||||
* @param event the input event
|
||||
* @return the evaluation result
|
||||
* @throws EvaluationException the first evaluation failure that happened
|
||||
*/
|
||||
Object tryEvaluate(EvaluationRuntime evaluationRuntime, CloudEvent event) throws EvaluationException;
|
||||
|
||||
/**
|
||||
* Like {@link #evaluate(EvaluationRuntime, CloudEvent)}, but using the default runtime instance.
|
||||
*/
|
||||
default Result evaluate(CloudEvent event) {
|
||||
return evaluate(EvaluationRuntime.getDefault(), event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #tryEvaluate(EvaluationRuntime, CloudEvent)}, but using the default runtime instance.
|
||||
*/
|
||||
default Object tryEvaluate(CloudEvent event) throws EvaluationException {
|
||||
return tryEvaluate(EvaluationRuntime.getDefault(), event);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Function is a CloudEvents Expression Language function definition and implementation.
|
||||
*/
|
||||
public interface Function extends FunctionSignature {
|
||||
|
||||
/**
|
||||
* Invoke the function logic.
|
||||
*
|
||||
* @param ctx the evaluation context
|
||||
* @param evaluationRuntime the evaluation runtime
|
||||
* @param event the expression input event
|
||||
* @param arguments the arguments passed to this function. Note: the arguments are already casted to the appropriate type declared in the signature
|
||||
* @return the return value of the function
|
||||
*/
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments);
|
||||
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
/**
|
||||
* Function is a CloudEvents Expression Language function definition.
|
||||
* <p>
|
||||
* This class' methods are used to perform the function dispatch and to cast the arguments to the appropriate values.
|
||||
*/
|
||||
public interface FunctionSignature {
|
||||
|
||||
/**
|
||||
* @return uppercase name of the function
|
||||
*/
|
||||
String name();
|
||||
|
||||
/**
|
||||
* @return the type of the parameter at index {@code i}. If the function is variadic and if {@code i >= arity()}, this function returns the vararg type
|
||||
* @throws IllegalArgumentException if {@code i} is greater or equal to the arity and the function is not variadic
|
||||
*/
|
||||
Type typeOfParameter(int i) throws IllegalArgumentException;
|
||||
|
||||
/**
|
||||
* @return the arity, excluding the vararg parameter if {@code isVariadic() == true}
|
||||
*/
|
||||
int arity();
|
||||
|
||||
/**
|
||||
* @return true is the function is variadic
|
||||
*/
|
||||
boolean isVariadic();
|
||||
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import org.antlr.v4.runtime.RecognitionException;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
|
||||
/**
|
||||
* This exception represents an error occurred during parsing.
|
||||
*/
|
||||
public class ParseException extends RuntimeException {
|
||||
|
||||
public enum ErrorKind {
|
||||
RECOGNITION_ERROR,
|
||||
PARSE_VALUE
|
||||
}
|
||||
|
||||
private final ErrorKind errorKind;
|
||||
private final Interval interval;
|
||||
private final String expression;
|
||||
|
||||
protected ParseException(ErrorKind errorKind, Interval interval, String expression, String message, Throwable cause) {
|
||||
super(String.format("[%s at %d:%d `%s`] %s", errorKind.name(), interval.a, interval.b, expression, message), cause);
|
||||
this.errorKind = errorKind;
|
||||
this.interval = interval;
|
||||
this.expression = expression;
|
||||
}
|
||||
|
||||
public ErrorKind getKind() {
|
||||
return errorKind;
|
||||
}
|
||||
|
||||
public Interval getInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
public String getExpression() {
|
||||
return expression;
|
||||
}
|
||||
|
||||
public static ParseException cannotParseValue(ParseTree node, Type target, Throwable cause) {
|
||||
return new ParseException(
|
||||
ErrorKind.PARSE_VALUE,
|
||||
node.getSourceInterval(),
|
||||
node.getText(),
|
||||
"Cannot parse to " + target.name() + ": " + cause.getMessage(),
|
||||
cause
|
||||
);
|
||||
}
|
||||
|
||||
public static ParseException recognitionError(RecognitionException e, String msg) {
|
||||
return new ParseException(
|
||||
ErrorKind.RECOGNITION_ERROR,
|
||||
new Interval(e.getOffendingToken().getStartIndex(), e.getOffendingToken().getStopIndex()),
|
||||
e.getOffendingToken().getText(),
|
||||
"Cannot parse: " + msg,
|
||||
e
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.sql.impl.ParserImpl;
|
||||
|
||||
public interface Parser {
|
||||
|
||||
/**
|
||||
* Parse the expression.
|
||||
*
|
||||
* @param inputExpression input expression
|
||||
* @return the parsed expression
|
||||
* @throws ParseException if the expression cannot be parsed
|
||||
*/
|
||||
Expression parse(String inputExpression) throws ParseException;
|
||||
|
||||
/**
|
||||
* Parse the expression with default parser instance.
|
||||
*
|
||||
* @param inputExpression input expression
|
||||
* @return the parsed expression
|
||||
* @throws ParseException if the expression cannot be parsed
|
||||
*/
|
||||
static Expression parseDefault(String inputExpression) throws ParseException {
|
||||
return ParserImpl.getInstance().parse(inputExpression);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Result of an expression evaluation.
|
||||
*/
|
||||
public interface Result {
|
||||
|
||||
/**
|
||||
* @return the result of the expression evaluation, which could be a {@link String}, a {@link Integer} or a {@link Boolean}.
|
||||
*/
|
||||
Object value();
|
||||
|
||||
/**
|
||||
* @return true if the causes collection is not empty.
|
||||
*/
|
||||
boolean isFailed();
|
||||
|
||||
/**
|
||||
* @return the list of evaluation exceptions happened while evaluating the expression.
|
||||
*/
|
||||
Collection<EvaluationException> causes();
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Type represents any of the types supported by the CloudEvents Expression Language and their relative Java classes.
|
||||
*/
|
||||
public enum Type {
|
||||
/**
|
||||
* The <i>Integer</i> type
|
||||
*/
|
||||
INTEGER(Integer.class),
|
||||
/**
|
||||
* The <i>String</i> type
|
||||
*/
|
||||
STRING(String.class),
|
||||
/**
|
||||
* The <i>Boolean</i> type
|
||||
*/
|
||||
BOOLEAN(Boolean.class),
|
||||
/**
|
||||
* Any is a catch-all type that can be used to represent any of the above types in a function signature.
|
||||
*/
|
||||
ANY(Object.class);
|
||||
|
||||
private final Class<?> clazz;
|
||||
|
||||
Type(Class<?> clazz) {
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Java class corresponding to the CloudEvents Expression Language type.
|
||||
*/
|
||||
public Class<?> valueClass() {
|
||||
return clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the CloudEvents Expression Language type from a value.
|
||||
*
|
||||
* @param value the value to use
|
||||
* @return the type, or any if the value class is unrecognized.
|
||||
*/
|
||||
public static Type fromValue(Object value) {
|
||||
Objects.requireNonNull(value);
|
||||
return fromClass(value.getClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the CloudEvents Expression Language type from a Java class.
|
||||
*
|
||||
* @param clazz the class to use
|
||||
* @return the type, or any if the class is unrecognized.
|
||||
*/
|
||||
public static Type fromClass(Class<?> clazz) {
|
||||
Objects.requireNonNull(clazz);
|
||||
if (Integer.class.equals(clazz)) {
|
||||
return INTEGER;
|
||||
} else if (String.class.equals(clazz)) {
|
||||
return STRING;
|
||||
} else if (Boolean.class.equals(clazz)) {
|
||||
return BOOLEAN;
|
||||
}
|
||||
return ANY;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import org.antlr.v4.runtime.CharStream;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
/**
|
||||
* This class supports case-insensitive lexing by wrapping an existing
|
||||
* {@link CharStream} and forcing the lexer to see either upper or
|
||||
* lowercase characters. Grammar literals should then be either upper or
|
||||
* lower case such as 'BEGIN' or 'begin'. The text of the character
|
||||
* stream is unaffected. Example: input 'BeGiN' would match lexer rule
|
||||
* 'BEGIN' if constructor parameter upper=true but getText() would return
|
||||
* 'BeGiN'.
|
||||
*/
|
||||
public class CaseChangingCharStream implements CharStream {
|
||||
|
||||
final CharStream stream;
|
||||
final boolean upper;
|
||||
|
||||
/**
|
||||
* Constructs a new CaseChangingCharStream wrapping the given {@link CharStream} forcing
|
||||
* all characters to upper case or lower case.
|
||||
*
|
||||
* @param stream The stream to wrap.
|
||||
* @param upper If true force each symbol to upper case, otherwise force to lower.
|
||||
*/
|
||||
public CaseChangingCharStream(CharStream stream, boolean upper) {
|
||||
this.stream = stream;
|
||||
this.upper = upper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getText(Interval interval) {
|
||||
return stream.getText(interval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consume() {
|
||||
stream.consume();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int LA(int i) {
|
||||
int c = stream.LA(i);
|
||||
if (c <= 0) {
|
||||
return c;
|
||||
}
|
||||
if (upper) {
|
||||
return Character.toUpperCase(c);
|
||||
}
|
||||
return Character.toLowerCase(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int mark() {
|
||||
return stream.mark();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void release(int marker) {
|
||||
stream.release(marker);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int index() {
|
||||
return stream.index();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seek(int index) {
|
||||
stream.seek(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return stream.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSourceName() {
|
||||
return stream.getSourceName();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CloudEventUtils {
|
||||
|
||||
private CloudEventUtils() {
|
||||
}
|
||||
|
||||
public static boolean hasContextAttribute(CloudEvent event, String key) {
|
||||
return event.getAttributeNames().contains(key) || event.getExtensionNames().contains(key);
|
||||
}
|
||||
|
||||
public static Object accessContextAttribute(ExceptionThrower exceptions, Interval interval, String expression, CloudEvent event, String key) {
|
||||
// TODO do we have a better solution to access attributes here?
|
||||
Object value;
|
||||
try {
|
||||
value = event.getAttribute(key);
|
||||
} catch (IllegalArgumentException e) {
|
||||
value = event.getExtension(key);
|
||||
}
|
||||
if (value == null) {
|
||||
exceptions.throwException(
|
||||
EvaluationException.missingAttribute(interval, expression, key)
|
||||
);
|
||||
value = "";
|
||||
} else {
|
||||
// Because the CESQL type system is smaller than the CE type system,
|
||||
// we need to coherce some values to string
|
||||
value = coherceTypes(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static Object coherceTypes(Object value) {
|
||||
if (value instanceof Boolean || value instanceof String || value instanceof Integer) {
|
||||
// No casting required
|
||||
return value;
|
||||
}
|
||||
if (value instanceof byte[]) {
|
||||
return Base64.getEncoder().encodeToString((byte[]) value);
|
||||
}
|
||||
return Objects.toString(value);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class EvaluationContextImpl implements EvaluationContext {
|
||||
|
||||
private final Interval expressionInterval;
|
||||
private final String expressionText;
|
||||
private final ExceptionThrower exceptionThrower;
|
||||
|
||||
public EvaluationContextImpl(Interval expressionInterval, String expressionText, ExceptionThrower exceptionThrower) {
|
||||
this.expressionInterval = expressionInterval;
|
||||
this.expressionText = expressionText;
|
||||
this.exceptionThrower = exceptionThrower;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interval expressionInterval() {
|
||||
return this.expressionInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String expressionText() {
|
||||
return this.expressionText;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendException(EvaluationException exception) {
|
||||
this.exceptionThrower.throwException(exception);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void appendException(EvaluationException.EvaluationExceptionFactory exceptionFactory) {
|
||||
this.exceptionThrower.throwException(exceptionFactory.create(expressionInterval(), expressionText()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.Result;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class EvaluationResult implements Result {
|
||||
|
||||
private final Object value;
|
||||
private final List<EvaluationException> exceptions;
|
||||
|
||||
public EvaluationResult(Object value, List<EvaluationException> exceptions) {
|
||||
this.value = value;
|
||||
this.exceptions = exceptions == null ? Collections.emptyList() : Collections.unmodifiableList(exceptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the raw result of the evaluation
|
||||
*/
|
||||
@Override
|
||||
public Object value() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true if the evaluation failed, false otherwise
|
||||
*/
|
||||
@Override
|
||||
public boolean isFailed() {
|
||||
return !exceptions.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the causes of the failure, if {@link #isFailed()} returns {@code true}
|
||||
*/
|
||||
@Override
|
||||
public Collection<EvaluationException> causes() {
|
||||
return exceptions != null ? exceptions : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
EvaluationResult that = (EvaluationResult) o;
|
||||
return Objects.equals(value, that.value) && Objects.equals(exceptions, that.exceptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(value, exceptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EvaluationResult{" +
|
||||
" value=" + value +
|
||||
"\n" +
|
||||
" , exceptions=" + exceptions +
|
||||
"\n" +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Function;
|
||||
|
||||
public class EvaluationRuntimeBuilder {
|
||||
|
||||
private final FunctionTable functionTable;
|
||||
|
||||
public EvaluationRuntimeBuilder() {
|
||||
this.functionTable = new FunctionTable(FunctionTable.getDefaultInstance());
|
||||
}
|
||||
|
||||
public EvaluationRuntimeBuilder addFunction(Function function) throws IllegalArgumentException {
|
||||
this.functionTable.addFunction(function);
|
||||
return this;
|
||||
}
|
||||
|
||||
public EvaluationRuntime build() {
|
||||
return new EvaluationRuntimeImpl(
|
||||
new TypeCastingProvider(),
|
||||
functionTable
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Function;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class EvaluationRuntimeImpl implements EvaluationRuntime {
|
||||
|
||||
private static class SingletonContainer {
|
||||
private final static EvaluationRuntimeImpl INSTANCE = new EvaluationRuntimeImpl(new TypeCastingProvider(), FunctionTable.getDefaultInstance());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return instance of {@link EvaluationRuntimeImpl}
|
||||
*/
|
||||
public static EvaluationRuntime getInstance() {
|
||||
return EvaluationRuntimeImpl.SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
private final TypeCastingProvider typeCastingProvider;
|
||||
private final FunctionTable functionTable;
|
||||
|
||||
public EvaluationRuntimeImpl(TypeCastingProvider typeCastingProvider, FunctionTable functionTable) {
|
||||
this.typeCastingProvider = typeCastingProvider;
|
||||
this.functionTable = functionTable;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canCast(Object value, Type target) {
|
||||
return this.typeCastingProvider.canCast(value, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object cast(EvaluationContext ctx, Object value, Type target) {
|
||||
return this.typeCastingProvider.cast(ctx, value, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function resolveFunction(String name, int args) throws IllegalStateException {
|
||||
return functionTable.resolve(name, args);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
|
||||
public interface ExceptionThrower {
|
||||
|
||||
/**
|
||||
* This method might block the execution or not, depending on its implementation
|
||||
*
|
||||
* @param exception the exception to throw
|
||||
*/
|
||||
void throwException(EvaluationException exception);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class ExceptionsStore implements ExceptionThrower {
|
||||
|
||||
private List<EvaluationException> exceptions;
|
||||
|
||||
ExceptionsStore() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void throwException(EvaluationException exception) {
|
||||
if (this.exceptions == null) {
|
||||
this.exceptions = new ArrayList<>();
|
||||
}
|
||||
this.exceptions.add(exception);
|
||||
}
|
||||
|
||||
List<EvaluationException> getExceptions() {
|
||||
return exceptions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Expression;
|
||||
import io.cloudevents.sql.Result;
|
||||
|
||||
public class ExpressionImpl implements Expression {
|
||||
|
||||
private final ExpressionInternal expressionInternal;
|
||||
|
||||
public ExpressionImpl(ExpressionInternal expressionInternal) {
|
||||
this.expressionInternal = expressionInternal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result evaluate(EvaluationRuntime evaluationRuntime, CloudEvent event) {
|
||||
ExceptionsStore exceptions = new ExceptionsStore();
|
||||
Object value = this.expressionInternal.evaluate(evaluationRuntime, event, exceptions);
|
||||
return new EvaluationResult(value, exceptions.getExceptions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object tryEvaluate(EvaluationRuntime evaluationRuntime, CloudEvent event) throws EvaluationException {
|
||||
return this.expressionInternal.evaluate(evaluationRuntime, event, FailFastExceptionThrower.getInstance());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public interface ExpressionInternal {
|
||||
|
||||
Interval expressionInterval();
|
||||
|
||||
String expressionText();
|
||||
|
||||
Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower);
|
||||
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.ParseException;
|
||||
import io.cloudevents.sql.Type;
|
||||
import io.cloudevents.sql.generated.CESQLParserBaseVisitor;
|
||||
import io.cloudevents.sql.generated.CESQLParserParser;
|
||||
import io.cloudevents.sql.impl.expressions.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ExpressionTranslatorVisitor extends CESQLParserBaseVisitor<ExpressionInternal> {
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitCesql(CESQLParserParser.CesqlContext ctx) {
|
||||
return visit(ctx.expression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitBooleanLiteral(CESQLParserParser.BooleanLiteralContext ctx) {
|
||||
if (ctx.TRUE() != null) {
|
||||
return new ValueExpression(ctx.getSourceInterval(), ctx.getText(), true);
|
||||
} else {
|
||||
return new ValueExpression(ctx.getSourceInterval(), ctx.getText(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitIntegerLiteral(CESQLParserParser.IntegerLiteralContext ctx) {
|
||||
try {
|
||||
return ValueExpression.fromIntegerLiteral(ctx.INTEGER_LITERAL());
|
||||
} catch (RuntimeException e) {
|
||||
throw ParseException.cannotParseValue(ctx, Type.INTEGER, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitStringLiteral(CESQLParserParser.StringLiteralContext ctx) {
|
||||
try {
|
||||
if (ctx.DQUOTED_STRING_LITERAL() != null) {
|
||||
return ValueExpression.fromDQuotedStringLiteral(ctx.DQUOTED_STRING_LITERAL());
|
||||
} else {
|
||||
return ValueExpression.fromSQuotedStringLiteral(ctx.SQUOTED_STRING_LITERAL());
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
throw ParseException.cannotParseValue(ctx, Type.STRING, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitSubExpression(CESQLParserParser.SubExpressionContext ctx) {
|
||||
return visit(ctx.expression());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitExistsExpression(CESQLParserParser.ExistsExpressionContext ctx) {
|
||||
return new ExistsExpression(ctx.getSourceInterval(), ctx.getText(), ctx.identifier().getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitUnaryLogicExpression(CESQLParserParser.UnaryLogicExpressionContext ctx) {
|
||||
// Only 'not' is a valid unary logic expression
|
||||
ExpressionInternal internal = visit(ctx.expression());
|
||||
return new NotExpression(ctx.getSourceInterval(), ctx.getText(), internal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitUnaryNumericExpression(CESQLParserParser.UnaryNumericExpressionContext ctx) {
|
||||
// Only 'negate' is a valid unary logic expression
|
||||
ExpressionInternal internal = visit(ctx.expression());
|
||||
return new NegateExpression(ctx.getSourceInterval(), ctx.getText(), internal);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitIdentifier(CESQLParserParser.IdentifierContext ctx) {
|
||||
return new AccessAttributeExpression(ctx.getSourceInterval(), ctx.getText(), ctx.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitInExpression(CESQLParserParser.InExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression());
|
||||
List<ExpressionInternal> setExpressions = ctx.setExpression().expression().stream()
|
||||
.map(this::visit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
ExpressionInternal inExpression = new InExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, setExpressions);
|
||||
return (ctx.NOT() != null) ? new NotExpression(ctx.getSourceInterval(), ctx.getText(), inExpression) : inExpression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitBinaryMultiplicativeExpression(CESQLParserParser.BinaryMultiplicativeExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression(0));
|
||||
ExpressionInternal rightExpression = visit(ctx.expression(1));
|
||||
|
||||
if (ctx.STAR() != null) {
|
||||
return new MultiplicationExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
} else if (ctx.DIVIDE() != null) {
|
||||
return new DivisionExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
} else {
|
||||
return new ModuleExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitBinaryAdditiveExpression(CESQLParserParser.BinaryAdditiveExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression(0));
|
||||
ExpressionInternal rightExpression = visit(ctx.expression(1));
|
||||
|
||||
if (ctx.PLUS() != null) {
|
||||
return new SumExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
} else {
|
||||
return new DifferenceExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitBinaryComparisonExpression(CESQLParserParser.BinaryComparisonExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression(0));
|
||||
ExpressionInternal rightExpression = visit(ctx.expression(1));
|
||||
|
||||
if (ctx.EQUAL() != null) {
|
||||
// Equality operation is ambiguous, we have a specific implementation for it
|
||||
return new EqualExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
}
|
||||
if (ctx.NOT_EQUAL() != null || ctx.LESS_GREATER() != null) {
|
||||
// Equality operation is ambiguous, we have a specific implementation for it
|
||||
return new NotExpression(ctx.getSourceInterval(), ctx.getText(), new EqualExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression));
|
||||
}
|
||||
|
||||
// From this onward, just operators defined on integers
|
||||
IntegerComparisonBinaryExpression.Operation op;
|
||||
if (ctx.LESS() != null) {
|
||||
op = IntegerComparisonBinaryExpression.Operation.LESS;
|
||||
} else if (ctx.LESS_OR_EQUAL() != null) {
|
||||
op = IntegerComparisonBinaryExpression.Operation.LESS_OR_EQUAL;
|
||||
} else if (ctx.GREATER() != null) {
|
||||
op = IntegerComparisonBinaryExpression.Operation.GREATER;
|
||||
} else {
|
||||
op = IntegerComparisonBinaryExpression.Operation.GREATER_OR_EQUAL;
|
||||
}
|
||||
|
||||
return new IntegerComparisonBinaryExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression, op);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitBinaryLogicExpression(CESQLParserParser.BinaryLogicExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression(0));
|
||||
ExpressionInternal rightExpression = visit(ctx.expression(1));
|
||||
|
||||
if (ctx.AND() != null) {
|
||||
return new AndExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
} else if (ctx.OR() != null) {
|
||||
return new OrExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
} else {
|
||||
return new XorExpression(ctx.getSourceInterval(), ctx.getText(), leftExpression, rightExpression);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitLikeExpression(CESQLParserParser.LikeExpressionContext ctx) {
|
||||
ExpressionInternal leftExpression = visit(ctx.expression());
|
||||
ExpressionInternal likeExpression = new LikeExpression(
|
||||
ctx.getSourceInterval(),
|
||||
ctx.getText(),
|
||||
leftExpression,
|
||||
(ctx.stringLiteral().DQUOTED_STRING_LITERAL() != null) ?
|
||||
LiteralUtils.parseDQuotedStringLiteral(ctx.stringLiteral().DQUOTED_STRING_LITERAL()) :
|
||||
LiteralUtils.parseSQuotedStringLiteral(ctx.stringLiteral().SQUOTED_STRING_LITERAL())
|
||||
);
|
||||
return (ctx.NOT() != null) ? new NotExpression(ctx.getSourceInterval(), ctx.getText(), likeExpression) : likeExpression;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExpressionInternal visitFunctionInvocationExpression(CESQLParserParser.FunctionInvocationExpressionContext ctx) {
|
||||
List<ExpressionInternal> parameters = ctx.functionParameterList().expression().stream()
|
||||
.map(this::visit)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new FunctionInvocationExpression(ctx.getSourceInterval(), ctx.getText(), ctx.functionIdentifier().getText(), parameters);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
|
||||
class FailFastExceptionThrower implements ExceptionThrower {
|
||||
|
||||
private static class SingletonContainer {
|
||||
private final static FailFastExceptionThrower INSTANCE = new FailFastExceptionThrower();
|
||||
}
|
||||
|
||||
static FailFastExceptionThrower getInstance() {
|
||||
return FailFastExceptionThrower.SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void throwException(EvaluationException exception) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.Function;
|
||||
import io.cloudevents.sql.impl.functions.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class FunctionTable {
|
||||
|
||||
private static class SingletonContainer {
|
||||
private final static FunctionTable INSTANCE = new FunctionTable(
|
||||
Stream.of(
|
||||
new InfallibleOneArgumentFunction<>("ABS", Integer.class, Math::abs),
|
||||
new IntFunction(),
|
||||
new BoolFunction(),
|
||||
new StringFunction(),
|
||||
new IsBoolFunction(),
|
||||
new IsIntFunction(),
|
||||
new InfallibleOneArgumentFunction<>("LENGTH", String.class, String::length),
|
||||
new ConcatFunction(),
|
||||
new ConcatWSFunction(),
|
||||
new InfallibleOneArgumentFunction<>("LOWER", String.class, String::toLowerCase),
|
||||
new InfallibleOneArgumentFunction<>("UPPER", String.class, String::toUpperCase),
|
||||
new InfallibleOneArgumentFunction<>("TRIM", String.class, String::trim),
|
||||
new LeftFunction(),
|
||||
new RightFunction(),
|
||||
new SubstringFunction(),
|
||||
new SubstringWithLengthFunction()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return instance of {@link FunctionTable}
|
||||
*/
|
||||
public static FunctionTable getDefaultInstance() {
|
||||
return SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
private final Map<String, Functions> functions;
|
||||
|
||||
private FunctionTable(Stream<Function> functions) {
|
||||
this.functions = new HashMap<>();
|
||||
functions.forEach(this::addFunction);
|
||||
}
|
||||
|
||||
protected FunctionTable(FunctionTable functionTable) {
|
||||
this(functionTable.getFunctions());
|
||||
}
|
||||
|
||||
protected Function resolve(String name, int args) throws IllegalStateException {
|
||||
Functions fns = functions.get(name);
|
||||
if (fns == null) {
|
||||
throw new IllegalStateException(
|
||||
"No function named '" + name + "' found. Available function names: " + functions.keySet()
|
||||
);
|
||||
}
|
||||
|
||||
return fns.resolve(args);
|
||||
}
|
||||
|
||||
protected void addFunction(Function function) throws IllegalArgumentException {
|
||||
Functions fns = this.functions.computeIfAbsent(function.name(), v -> new Functions());
|
||||
fns.addFunction(function);
|
||||
}
|
||||
|
||||
private Stream<Function> getFunctions() {
|
||||
return functions.values()
|
||||
.stream()
|
||||
.flatMap(Functions::getFunctions);
|
||||
}
|
||||
|
||||
private static class Functions {
|
||||
private final Map<Integer, Function> fixedArgsNumberFunctions;
|
||||
private Function variadicFunction;
|
||||
|
||||
private Functions() {
|
||||
this.fixedArgsNumberFunctions = new HashMap<>();
|
||||
}
|
||||
|
||||
public void addFunction(Function function) {
|
||||
if (function.isVariadic()) {
|
||||
if (
|
||||
fixedArgsNumberFunctions
|
||||
.keySet()
|
||||
.stream()
|
||||
.max(Integer::compareTo)
|
||||
.map(maxArity -> maxArity >= function.arity())
|
||||
.orElse(false)
|
||||
) {
|
||||
throw new IllegalArgumentException(
|
||||
"You're trying to add a variadic function, but one function with the same name and arity greater or equal is already defined: " + function.name()
|
||||
);
|
||||
}
|
||||
if (this.variadicFunction != null) {
|
||||
throw new IllegalArgumentException("You're trying to add a variadic function, but one is already defined for this function name: " + function.name());
|
||||
}
|
||||
this.variadicFunction = function;
|
||||
} else {
|
||||
Function old = this.fixedArgsNumberFunctions.put(function.arity(), function);
|
||||
if (old != null) {
|
||||
throw new IllegalArgumentException("You're trying to add a function, but one with the same arity is already defined: " + function.name() + " with arity " + function.arity());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Function resolve(int args) {
|
||||
Function fn = fixedArgsNumberFunctions.get(args);
|
||||
if (fn != null) {
|
||||
return fn;
|
||||
}
|
||||
// Let's try with the variadic functions
|
||||
if (variadicFunction == null) {
|
||||
// This shouldn't really happen, since this object should not exist in that case
|
||||
throw createMissingFunctionException(args);
|
||||
}
|
||||
if (variadicFunction.arity() > args) {
|
||||
throw createMissingFunctionException(args);
|
||||
}
|
||||
return variadicFunction;
|
||||
}
|
||||
|
||||
private RuntimeException createMissingFunctionException(int args) {
|
||||
return new IllegalStateException(
|
||||
"No functions with arity " + args + " found. Available functions: " +
|
||||
fixedArgsNumberFunctions.values() + ((variadicFunction != null) ? " and variadic " + variadicFunction : "")
|
||||
);
|
||||
}
|
||||
|
||||
private Stream<Function> getFunctions() {
|
||||
if (variadicFunction == null) {
|
||||
return fixedArgsNumberFunctions.values().stream();
|
||||
}
|
||||
return Stream.concat(fixedArgsNumberFunctions.values().stream(), Stream.of(variadicFunction));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class LiteralUtils {
|
||||
|
||||
public static String parseSQuotedStringLiteral(TerminalNode node) {
|
||||
String val = node.getText();
|
||||
val = val.substring(1, val.length() - 1);
|
||||
val = val.replaceAll(Pattern.quote("\\'"), "'");
|
||||
return val;
|
||||
}
|
||||
|
||||
public static String parseDQuotedStringLiteral(TerminalNode node) {
|
||||
String val = node.getText();
|
||||
val = val.substring(1, val.length() - 1);
|
||||
val = val.replaceAll(Pattern.quote("\\\""), "\"");
|
||||
return val;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.Expression;
|
||||
import io.cloudevents.sql.ParseException;
|
||||
import io.cloudevents.sql.Parser;
|
||||
import io.cloudevents.sql.generated.CESQLParserLexer;
|
||||
import io.cloudevents.sql.generated.CESQLParserParser;
|
||||
import org.antlr.v4.runtime.*;
|
||||
import org.antlr.v4.runtime.atn.ATNConfigSet;
|
||||
import org.antlr.v4.runtime.dfa.DFA;
|
||||
import org.antlr.v4.runtime.tree.ParseTree;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
|
||||
public class ParserImpl implements Parser {
|
||||
|
||||
private static class SingletonContainer {
|
||||
private final static ParserImpl INSTANCE = new ParserImpl();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return instance of {@link ParserImpl}
|
||||
*/
|
||||
public static Parser getInstance() {
|
||||
return ParserImpl.SingletonContainer.INSTANCE;
|
||||
}
|
||||
|
||||
public ParserImpl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Expression parse(String inputExpression) {
|
||||
CharStream s = CharStreams.fromString(inputExpression);
|
||||
CaseChangingCharStream upperInput = new CaseChangingCharStream(s, true);
|
||||
CESQLParserLexer lexer = new CESQLParserLexer(upperInput);
|
||||
|
||||
CommonTokenStream tokens = new CommonTokenStream(lexer);
|
||||
|
||||
CESQLParserParser parser = new CESQLParserParser(tokens);
|
||||
|
||||
List<ParseException> parseExceptionList = new ArrayList<>();
|
||||
parser.removeErrorListeners();
|
||||
parser.addErrorListener(new ANTLRErrorListener() {
|
||||
@Override
|
||||
public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) {
|
||||
parseExceptionList.add(
|
||||
ParseException.recognitionError(e, msg)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAmbiguity(org.antlr.v4.runtime.Parser recognizer, DFA dfa, int startIndex, int stopIndex, boolean exact, BitSet ambigAlts, ATNConfigSet configs) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportAttemptingFullContext(org.antlr.v4.runtime.Parser recognizer, DFA dfa, int startIndex, int stopIndex, BitSet conflictingAlts, ATNConfigSet configs) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reportContextSensitivity(org.antlr.v4.runtime.Parser recognizer, DFA dfa, int startIndex, int stopIndex, int prediction, ATNConfigSet configs) {
|
||||
}
|
||||
});
|
||||
|
||||
// Start parsing from cesql rule
|
||||
ParseTree tree = parser.cesql();
|
||||
|
||||
if (!parseExceptionList.isEmpty()) {
|
||||
throw parseExceptionList.get(0);
|
||||
}
|
||||
|
||||
ExpressionInternal internal = new ExpressionTranslatorVisitor().visit(tree);
|
||||
|
||||
return new ExpressionImpl(internal);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package io.cloudevents.sql.impl;
|
||||
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class TypeCastingProvider {
|
||||
|
||||
boolean canCast(Object value, Type target) {
|
||||
if (target.valueClass().equals(value.getClass())) {
|
||||
return true;
|
||||
}
|
||||
switch (target) {
|
||||
case INTEGER:
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
Integer.parseInt((String) value);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
case BOOLEAN:
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
parseBool((String) value);
|
||||
return true;
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Object cast(EvaluationContext ctx, Object value, Type target) {
|
||||
Objects.requireNonNull(value);
|
||||
if (target.valueClass().equals(value.getClass())) {
|
||||
return value;
|
||||
}
|
||||
switch (target) {
|
||||
case ANY:
|
||||
return value;
|
||||
case STRING:
|
||||
return Objects.toString(value);
|
||||
case INTEGER:
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
return Integer.parseInt((String) value);
|
||||
} catch (NumberFormatException e) {
|
||||
ctx.appendException(
|
||||
EvaluationException.castError(String.class, Integer.class, e)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ctx.appendException(
|
||||
EvaluationException.invalidCastTarget(value.getClass(), target.valueClass())
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
case BOOLEAN:
|
||||
if (value instanceof String) {
|
||||
try {
|
||||
return parseBool((String) value);
|
||||
} catch (IllegalArgumentException e) {
|
||||
ctx.appendException(
|
||||
EvaluationException.castError(String.class, Boolean.class, e)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
ctx.appendException(
|
||||
EvaluationException.invalidCastTarget(value.getClass(), target.valueClass())
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// This should never happen
|
||||
throw new IllegalArgumentException("target type doesn't correspond to a known type");
|
||||
}
|
||||
|
||||
private boolean parseBool(String val) {
|
||||
switch (val.toLowerCase()) {
|
||||
case "true":
|
||||
return true;
|
||||
case "false":
|
||||
return false;
|
||||
default:
|
||||
throw new IllegalArgumentException("Cannot cast '" + val + "' to boolean. Allowed values: ['true', 'false']");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.CloudEventUtils;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class AccessAttributeExpression extends BaseExpression {
|
||||
|
||||
private final String key;
|
||||
|
||||
public AccessAttributeExpression(Interval expressionInterval, String expressionText, String key) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.key = key.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
return CloudEventUtils.accessContextAttribute(thrower, expressionInterval(), expressionText(), event, key);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class AndExpression extends BaseBinaryExpression {
|
||||
|
||||
public AndExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
boolean x = castToBoolean(runtime, exceptions, left);
|
||||
if (!x) {
|
||||
// Short circuit
|
||||
return false;
|
||||
}
|
||||
return castToBoolean(runtime, exceptions, right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public abstract class BaseBinaryExpression extends BaseExpression {
|
||||
|
||||
protected final ExpressionInternal leftOperand;
|
||||
protected final ExpressionInternal rightOperand;
|
||||
|
||||
protected BaseBinaryExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.leftOperand = leftOperand;
|
||||
this.rightOperand = rightOperand;
|
||||
}
|
||||
|
||||
abstract Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions);
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
Object left = leftOperand.evaluate(runtime, event, thrower);
|
||||
Object right = rightOperand.evaluate(runtime, event, thrower);
|
||||
return evaluate(runtime, left, right, thrower);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
import io.cloudevents.sql.impl.EvaluationContextImpl;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public abstract class BaseExpression implements ExpressionInternal {
|
||||
|
||||
private final String expressionText;
|
||||
private final Interval expressionInterval;
|
||||
|
||||
protected BaseExpression(Interval expressionInterval, String expressionText) {
|
||||
this.expressionText = expressionText;
|
||||
this.expressionInterval = expressionInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Interval expressionInterval() {
|
||||
return this.expressionInterval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String expressionText() {
|
||||
return this.expressionText;
|
||||
}
|
||||
|
||||
public Boolean castToBoolean(EvaluationRuntime runtime, ExceptionThrower exceptions, Object value) {
|
||||
return (Boolean) runtime.cast(
|
||||
new EvaluationContextImpl(expressionInterval(), expressionText(), exceptions),
|
||||
value,
|
||||
Type.BOOLEAN
|
||||
);
|
||||
}
|
||||
|
||||
public Integer castToInteger(EvaluationRuntime runtime, ExceptionThrower exceptions, Object value) {
|
||||
return (Integer) runtime.cast(
|
||||
new EvaluationContextImpl(expressionInterval(), expressionText(), exceptions),
|
||||
value,
|
||||
Type.INTEGER
|
||||
);
|
||||
}
|
||||
|
||||
public String castToString(EvaluationRuntime runtime, ExceptionThrower exceptions, Object value) {
|
||||
return (String) runtime.cast(
|
||||
new EvaluationContextImpl(expressionInterval(), expressionText(), exceptions),
|
||||
value,
|
||||
Type.STRING
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public abstract class BaseIntegerBinaryExpression extends BaseBinaryExpression {
|
||||
|
||||
public BaseIntegerBinaryExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
abstract Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions);
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
return this.evaluate(
|
||||
runtime,
|
||||
castToInteger(runtime, exceptions, left).intValue(),
|
||||
castToInteger(runtime, exceptions, right).intValue(),
|
||||
exceptions
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class DifferenceExpression extends BaseIntegerBinaryExpression {
|
||||
|
||||
public DifferenceExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions) {
|
||||
return left - right;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class DivisionExpression extends BaseIntegerBinaryExpression {
|
||||
|
||||
public DivisionExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions) {
|
||||
if (right == 0) {
|
||||
exceptions.throwException(
|
||||
EvaluationException.divisionByZero(expressionInterval(), expressionText(), left)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
return left / right;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
import io.cloudevents.sql.impl.EvaluationContextImpl;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class EqualExpression extends BaseBinaryExpression {
|
||||
|
||||
public EqualExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
// x = y: Boolean x Boolean -> Boolean
|
||||
// x = y: Integer x Integer -> Boolean
|
||||
// x = y: String x String -> Boolean
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
left = runtime.cast(
|
||||
new EvaluationContextImpl(expressionInterval(), expressionText(), exceptions),
|
||||
left,
|
||||
Type.fromValue(right)
|
||||
);
|
||||
return Objects.equals(left, right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.CloudEventUtils;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class ExistsExpression extends BaseExpression {
|
||||
|
||||
private final String key;
|
||||
|
||||
public ExistsExpression(Interval expressionInterval, String expressionText, String key) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.key = key.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
return CloudEventUtils.hasContextAttribute(event, key);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Function;
|
||||
import io.cloudevents.sql.impl.EvaluationContextImpl;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class FunctionInvocationExpression extends BaseExpression {
|
||||
|
||||
private final String functionName;
|
||||
private final List<ExpressionInternal> arguments;
|
||||
|
||||
public FunctionInvocationExpression(Interval expressionInterval, String expressionText, String functionName, List<ExpressionInternal> arguments) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.functionName = functionName.toUpperCase();
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
EvaluationContext context = new EvaluationContextImpl(expressionInterval(), expressionText(), thrower);
|
||||
|
||||
Function function;
|
||||
try {
|
||||
function = runtime.resolveFunction(functionName, arguments.size());
|
||||
} catch (Exception e) {
|
||||
thrower.throwException(
|
||||
EvaluationException.cannotDispatchFunction(expressionInterval(), expressionText(), functionName, e)
|
||||
);
|
||||
return "";
|
||||
}
|
||||
|
||||
List<Object> computedArguments = new ArrayList<>(arguments.size());
|
||||
for (int i = 0; i < arguments.size(); i++) {
|
||||
ExpressionInternal expr = arguments.get(i);
|
||||
Object computed = expr.evaluate(runtime, event, thrower);
|
||||
Object casted = runtime
|
||||
.cast(context, computed, function.typeOfParameter(i));
|
||||
computedArguments.add(casted);
|
||||
}
|
||||
|
||||
return function.invoke(
|
||||
context,
|
||||
runtime,
|
||||
event,
|
||||
computedArguments
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
import io.cloudevents.sql.impl.EvaluationContextImpl;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public class InExpression extends BaseExpression {
|
||||
|
||||
// leftOperand IN (setExpressions...)
|
||||
private final ExpressionInternal leftExpression;
|
||||
private final List<ExpressionInternal> setExpressions;
|
||||
|
||||
// TODO this expression can be optimized if the ExpressionInternal are all ValueExpression (aka set is composed by literals)
|
||||
public InExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftExpression, List<ExpressionInternal> setExpressions) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.leftExpression = leftExpression;
|
||||
this.setExpressions = setExpressions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
Object leftValue = leftExpression.evaluate(runtime, event, thrower);
|
||||
return setExpressions.stream()
|
||||
.anyMatch(expr -> {
|
||||
Object rightValue = runtime.cast(
|
||||
new EvaluationContextImpl(expressionInterval(), expressionText(), thrower),
|
||||
expr.evaluate(runtime, event, thrower),
|
||||
Type.fromValue(leftValue)
|
||||
);
|
||||
|
||||
return Objects.equals(leftValue, rightValue);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
public class IntegerComparisonBinaryExpression extends BaseBinaryExpression {
|
||||
|
||||
public enum Operation {
|
||||
LESS((x, y) -> x < y),
|
||||
LESS_OR_EQUAL((x, y) -> x <= y),
|
||||
GREATER((x, y) -> x > y),
|
||||
GREATER_OR_EQUAL((x, y) -> x >= y);
|
||||
|
||||
private final BiFunction<Integer, Integer, Boolean> fn;
|
||||
|
||||
Operation(BiFunction<Integer, Integer, Boolean> fn) {
|
||||
this.fn = fn;
|
||||
}
|
||||
|
||||
boolean evaluate(int a, int b) {
|
||||
return this.fn.apply(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
private final Operation operation;
|
||||
|
||||
public IntegerComparisonBinaryExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand, Operation operation) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
this.operation = operation;
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
return this.operation.evaluate(
|
||||
castToInteger(runtime, exceptions, left),
|
||||
castToInteger(runtime, exceptions, right)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class LikeExpression extends BaseExpression {
|
||||
|
||||
private final ExpressionInternal internal;
|
||||
private final Pattern pattern;
|
||||
|
||||
public LikeExpression(Interval expressionInterval, String expressionText, ExpressionInternal internal, String pattern) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.internal = internal;
|
||||
// Converting to regex is not the most performant impl, but it works
|
||||
this.pattern = Pattern.compile("^" +
|
||||
pattern.replaceAll("(?<!\\\\)\\%", ".*")
|
||||
.replaceAll("(?<!\\\\)\\_", ".")
|
||||
.replaceAll("\\\\\\%", "%")
|
||||
.replaceAll("\\\\_", "_") + "$"
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
String value = castToString(
|
||||
runtime,
|
||||
thrower,
|
||||
internal.evaluate(runtime, event, thrower)
|
||||
);
|
||||
|
||||
return pattern.matcher(value).matches();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class ModuleExpression extends BaseIntegerBinaryExpression {
|
||||
|
||||
public ModuleExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions) {
|
||||
if (right == 0) {
|
||||
exceptions.throwException(
|
||||
EvaluationException.divisionByZero(expressionInterval(), expressionText(), left)
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
return left % right;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class MultiplicationExpression extends BaseIntegerBinaryExpression {
|
||||
|
||||
public MultiplicationExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions) {
|
||||
return left * right;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class NegateExpression extends BaseExpression {
|
||||
|
||||
private final ExpressionInternal internal;
|
||||
|
||||
public NegateExpression(Interval expressionInterval, String expressionText, ExpressionInternal internal) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.internal = internal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
return -castToInteger(runtime, thrower, internal.evaluate(runtime, event, thrower));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class NotExpression extends BaseExpression {
|
||||
|
||||
private final ExpressionInternal internal;
|
||||
|
||||
public NotExpression(Interval expressionInterval, String expressionText, ExpressionInternal internal) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.internal = internal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
return !castToBoolean(runtime, thrower, internal.evaluate(runtime, event, thrower));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class OrExpression extends BaseBinaryExpression {
|
||||
|
||||
public OrExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
boolean x = castToBoolean(runtime, exceptions, left);
|
||||
if (x) {
|
||||
// Short circuit
|
||||
return true;
|
||||
}
|
||||
return castToBoolean(runtime, exceptions, right);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class SumExpression extends BaseIntegerBinaryExpression {
|
||||
|
||||
public SumExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, int left, int right, ExceptionThrower exceptions) {
|
||||
return left + right;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.LiteralUtils;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
import org.antlr.v4.runtime.tree.TerminalNode;
|
||||
|
||||
public class ValueExpression extends BaseExpression {
|
||||
|
||||
private final Object value;
|
||||
|
||||
public ValueExpression(Interval expressionInterval, String expressionText, Object value) {
|
||||
super(expressionInterval, expressionText);
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object evaluate(EvaluationRuntime runtime, CloudEvent event, ExceptionThrower thrower) {
|
||||
return value;
|
||||
}
|
||||
|
||||
public static ValueExpression fromIntegerLiteral(TerminalNode node) {
|
||||
return new ValueExpression(node.getSourceInterval(), node.getText(), Integer.parseInt(node.getText()));
|
||||
}
|
||||
|
||||
public static ValueExpression fromSQuotedStringLiteral(TerminalNode node) {
|
||||
return new ValueExpression(node.getSourceInterval(), node.getText(), LiteralUtils.parseSQuotedStringLiteral(node));
|
||||
}
|
||||
|
||||
public static ValueExpression fromDQuotedStringLiteral(TerminalNode node) {
|
||||
return new ValueExpression(node.getSourceInterval(), node.getText(), LiteralUtils.parseDQuotedStringLiteral(node));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package io.cloudevents.sql.impl.expressions;
|
||||
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.impl.ExceptionThrower;
|
||||
import io.cloudevents.sql.impl.ExpressionInternal;
|
||||
import org.antlr.v4.runtime.misc.Interval;
|
||||
|
||||
public class XorExpression extends BaseBinaryExpression {
|
||||
|
||||
public XorExpression(Interval expressionInterval, String expressionText, ExpressionInternal leftOperand, ExpressionInternal rightOperand) {
|
||||
super(expressionInterval, expressionText, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object evaluate(EvaluationRuntime runtime, Object left, Object right, ExceptionThrower exceptions) {
|
||||
return Boolean.logicalXor(
|
||||
castToBoolean(runtime, exceptions, left),
|
||||
castToBoolean(runtime, exceptions, right)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.sql.Function;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public abstract class BaseFunction implements Function {
|
||||
|
||||
private final String name;
|
||||
|
||||
protected BaseFunction(String name) {
|
||||
this.name = name.toUpperCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
protected void requireValidParameterIndex(int i) {
|
||||
if (!isVariadic() && i >= arity()) {
|
||||
throw new IllegalArgumentException("The provided index must less than the arity of the function: " + i + " < " + arity());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append(name());
|
||||
builder.append('(');
|
||||
if (arity() > 0) {
|
||||
builder.append(
|
||||
IntStream.range(0, arity())
|
||||
.mapToObj(i -> typeOfParameter(i).name())
|
||||
.collect(Collectors.joining(","))
|
||||
);
|
||||
if (isVariadic()) {
|
||||
builder.append(", ")
|
||||
.append(typeOfParameter(arity()))
|
||||
.append("...");
|
||||
}
|
||||
}
|
||||
builder.append(')');
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseOneArgumentFunction<T> extends BaseFunction {
|
||||
|
||||
private final Type argumentClass;
|
||||
|
||||
public BaseOneArgumentFunction(String name, Class<T> argumentClass) {
|
||||
super(name);
|
||||
this.argumentClass = Type.fromClass(argumentClass);
|
||||
}
|
||||
|
||||
abstract Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, T argument);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return this.invoke(ctx, evaluationRuntime, event, (T) arguments.get(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) {
|
||||
requireValidParameterIndex(i);
|
||||
return argumentClass;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseThreeArgumentFunction<X, Y, Z> extends BaseFunction {
|
||||
|
||||
private final Type firstArg;
|
||||
private final Type secondArg;
|
||||
private final Type thirdArg;
|
||||
|
||||
public BaseThreeArgumentFunction(String name, Class<X> firstArg, Class<Y> secondArg, Class<Z> thirdArg) {
|
||||
super(name);
|
||||
this.firstArg = Type.fromClass(firstArg);
|
||||
this.secondArg = Type.fromClass(secondArg);
|
||||
this.thirdArg = Type.fromClass(thirdArg);
|
||||
}
|
||||
|
||||
abstract Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, X x, Y y, Z z);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return this.invoke(ctx, evaluationRuntime, event, (X) arguments.get(0), (Y) arguments.get(1), (Z) arguments.get(2));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) {
|
||||
requireValidParameterIndex(i);
|
||||
switch (i) {
|
||||
case 0:
|
||||
return firstArg;
|
||||
case 1:
|
||||
return secondArg;
|
||||
case 2:
|
||||
return thirdArg;
|
||||
}
|
||||
throw new IllegalArgumentException(); // This should be already checked by requireValidParameterIndex
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class BaseTwoArgumentFunction<X, Y> extends BaseFunction {
|
||||
|
||||
private final Type firstArg;
|
||||
private final Type secondArg;
|
||||
|
||||
public BaseTwoArgumentFunction(String name, Class<X> firstArg, Class<Y> secondArg) {
|
||||
super(name);
|
||||
this.firstArg = Type.fromClass(firstArg);
|
||||
this.secondArg = Type.fromClass(secondArg);
|
||||
|
||||
}
|
||||
|
||||
abstract Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, X x, Y y);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return this.invoke(ctx, evaluationRuntime, event, (X) arguments.get(0), (Y) arguments.get(1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) {
|
||||
requireValidParameterIndex(i);
|
||||
switch (i) {
|
||||
case 0:
|
||||
return firstArg;
|
||||
case 1:
|
||||
return secondArg;
|
||||
}
|
||||
throw new IllegalArgumentException(); // This should be already checked by requireValidParameterIndex
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class BoolFunction extends BaseOneArgumentFunction<String> {
|
||||
|
||||
public BoolFunction() {
|
||||
super("BOOL", String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String argument) {
|
||||
return evaluationRuntime.cast(ctx, argument, Type.BOOLEAN);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ConcatFunction extends BaseFunction {
|
||||
|
||||
public ConcatFunction() {
|
||||
super("CONCAT");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return arguments.stream()
|
||||
.map(o -> (String) o)
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) throws IllegalArgumentException {
|
||||
return Type.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ConcatWSFunction extends BaseFunction {
|
||||
|
||||
public ConcatWSFunction() {
|
||||
super("CONCAT_WS");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return arguments.stream()
|
||||
.skip(1)
|
||||
.map(o -> (String) o)
|
||||
.collect(Collectors.joining((String) arguments.get(0)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) throws IllegalArgumentException {
|
||||
return Type.STRING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
public class InfallibleOneArgumentFunction<T> extends BaseOneArgumentFunction<T> {
|
||||
|
||||
private final Function<T, Object> functionImplementation;
|
||||
|
||||
public InfallibleOneArgumentFunction(String name, Class<T> argumentClass, Function<T, Object> functionImplementation) {
|
||||
super(name, argumentClass);
|
||||
this.functionImplementation = functionImplementation;
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, T argument) {
|
||||
return this.functionImplementation.apply(argument);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class IntFunction extends BaseOneArgumentFunction<String> {
|
||||
|
||||
public IntFunction() {
|
||||
super("INT", String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String argument) {
|
||||
return evaluationRuntime.cast(ctx, argument, Type.INTEGER);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class IsBoolFunction extends BaseOneArgumentFunction<String> {
|
||||
|
||||
public IsBoolFunction() {
|
||||
super("IS_BOOL", String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String argument) {
|
||||
return evaluationRuntime.canCast(argument, Type.BOOLEAN);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class IsIntFunction extends BaseOneArgumentFunction<String> {
|
||||
|
||||
public IsIntFunction() {
|
||||
super("IS_INT", String.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String argument) {
|
||||
return evaluationRuntime.canCast(argument, Type.INTEGER);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
|
||||
public class LeftFunction extends BaseTwoArgumentFunction<String, Integer> {
|
||||
public LeftFunction() {
|
||||
super("LEFT", String.class, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String s, Integer length) {
|
||||
if (length > s.length()) {
|
||||
return s;
|
||||
}
|
||||
if (length < 0) {
|
||||
ctx.appendException(
|
||||
EvaluationException.functionExecutionError(name(), new IllegalArgumentException("The length of the LEFT substring is lower than 0: " + length))
|
||||
);
|
||||
return s;
|
||||
}
|
||||
return s.substring(0, length);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
|
||||
public class RightFunction extends BaseTwoArgumentFunction<String, Integer> {
|
||||
public RightFunction() {
|
||||
super("RIGHT", String.class, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String s, Integer length) {
|
||||
if (length > s.length()) {
|
||||
return s;
|
||||
}
|
||||
if (length < 0) {
|
||||
ctx.appendException(
|
||||
EvaluationException.functionExecutionError(name(), new IllegalArgumentException("The length of the RIGHT substring is lower than 0: " + length))
|
||||
);
|
||||
return s;
|
||||
}
|
||||
return s.substring(s.length() - length);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
import io.cloudevents.sql.Type;
|
||||
|
||||
public class StringFunction extends BaseOneArgumentFunction<Object> {
|
||||
|
||||
public StringFunction() {
|
||||
super("STRING", Object.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, Object argument) {
|
||||
return evaluationRuntime.cast(ctx, argument, Type.STRING);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
|
||||
public class SubstringFunction extends BaseTwoArgumentFunction<String, Integer> {
|
||||
public SubstringFunction() {
|
||||
super("SUBSTRING", String.class, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String x, Integer pos) {
|
||||
try {
|
||||
return SubstringWithLengthFunction.substring(x, pos, null);
|
||||
} catch (Exception e) {
|
||||
ctx.appendException(EvaluationException.functionExecutionError(
|
||||
name(),
|
||||
e
|
||||
));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package io.cloudevents.sql.impl.functions;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.sql.EvaluationContext;
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.EvaluationRuntime;
|
||||
|
||||
public class SubstringWithLengthFunction extends BaseThreeArgumentFunction<String, Integer, Integer> {
|
||||
public SubstringWithLengthFunction() {
|
||||
super("SUBSTRING", String.class, Integer.class, Integer.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, String x, Integer pos, Integer len) {
|
||||
try {
|
||||
return substring(x, pos, len);
|
||||
} catch (Exception e) {
|
||||
ctx.appendException(EvaluationException.functionExecutionError(
|
||||
name(),
|
||||
e
|
||||
));
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
static String substring(String x, Integer pos, Integer len) throws IllegalArgumentException {
|
||||
if (pos == 0) {
|
||||
return "";
|
||||
}
|
||||
if (pos < -x.length() || pos > x.length()) {
|
||||
throw new IllegalArgumentException("The pos argument is out of bounds: " + pos);
|
||||
}
|
||||
int beginning;
|
||||
if (pos < 0) {
|
||||
beginning = x.length() + pos;
|
||||
} else {
|
||||
// Indexes are 1-based
|
||||
beginning = pos - 1;
|
||||
}
|
||||
int end;
|
||||
if (len == null || beginning + len > x.length()) {
|
||||
end = x.length();
|
||||
} else {
|
||||
end = beginning + len;
|
||||
}
|
||||
return x.substring(beginning, end);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.core.test.Data;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
import static io.cloudevents.sql.asserts.MyAssertions.assertThat;
|
||||
|
||||
public class ContextAttributesAccessTest {
|
||||
|
||||
@Test
|
||||
void stringConversionForComplexTypes() {
|
||||
CloudEvent event = CloudEventBuilder.v1()
|
||||
.withId(Data.ID)
|
||||
.withType(Data.TYPE)
|
||||
.withSource(Data.SOURCE)
|
||||
.withData(Data.DATACONTENTTYPE_JSON, Data.DATASCHEMA, Data.DATA_JSON_SERIALIZED)
|
||||
.withSubject(Data.SUBJECT)
|
||||
.withTime(Data.TIME)
|
||||
.withExtension("numberext", 10)
|
||||
.withExtension("binaryext", new byte[]{0x01, 0x02, 0x03})
|
||||
.build();
|
||||
|
||||
assertThat(Parser.parseDefault("source").evaluate(event))
|
||||
.isNotFailed()
|
||||
.asString()
|
||||
.isEqualTo(event.getSource().toString());
|
||||
assertThat(Parser.parseDefault("time").evaluate(event))
|
||||
.isNotFailed()
|
||||
.asString()
|
||||
.isEqualTo(event.getTime().toString());
|
||||
assertThat(Parser.parseDefault("dataschema").evaluate(event))
|
||||
.isNotFailed()
|
||||
.asString()
|
||||
.isEqualTo(event.getDataSchema().toString());
|
||||
assertThat(Parser.parseDefault("binaryext").evaluate(event))
|
||||
.isNotFailed()
|
||||
.asString()
|
||||
.isEqualTo(Base64.getEncoder().encodeToString((byte[]) event.getExtension("binaryext")));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.core.test.Data;
|
||||
import io.cloudevents.sql.impl.EvaluationRuntimeBuilder;
|
||||
import io.cloudevents.sql.impl.functions.BaseFunction;
|
||||
import io.cloudevents.sql.impl.functions.InfallibleOneArgumentFunction;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static io.cloudevents.sql.asserts.MyAssertions.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
|
||||
public class CustomFunctionsTest {
|
||||
|
||||
@Test
|
||||
void addSimpleFunction() {
|
||||
EvaluationRuntime runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_PREDICATE",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
))
|
||||
.build();
|
||||
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_PREDICATE('abc')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asBoolean()
|
||||
.isFalse();
|
||||
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_PREDICATE('abc', 'xyz')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.hasFailure(EvaluationException.ErrorKind.FUNCTION_DISPATCH);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_PR('abc', 'xyz')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.hasFailure(EvaluationException.ErrorKind.FUNCTION_DISPATCH);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addVariadicFunction() {
|
||||
EvaluationRuntime runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new VariadicMockFunction("MY_STRING_FN", 2, Type.STRING))
|
||||
.build();
|
||||
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.hasFailure(EvaluationException.ErrorKind.FUNCTION_DISPATCH);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(2);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b', 'c')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(3);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b', 'c', 123, 456, 789)")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSimpleFunctionAndVariadicFunction() {
|
||||
EvaluationRuntime runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_FN",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
))
|
||||
.addFunction(new VariadicMockFunction("MY_STRING_FN", 2, Type.STRING))
|
||||
.build();
|
||||
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asBoolean()
|
||||
.isFalse();
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(2);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b', 'c')")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(3);
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_FN('abc', 'b', 'c', 123, 456, 789)")
|
||||
.evaluate(runtime, Data.V1_MIN)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asInteger()
|
||||
.isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotAddVariadicWithFixedArgsLowerThanMaxArgsOverload() {
|
||||
EvaluationRuntimeBuilder runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_FN",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
));
|
||||
|
||||
assertThatThrownBy(() -> runtime.addFunction(
|
||||
new VariadicMockFunction("MY_STRING_FN", 0, Type.STRING)
|
||||
)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> runtime.addFunction(
|
||||
new VariadicMockFunction("MY_STRING_FN", 1, Type.STRING)
|
||||
)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void cannotAddTwoVariadicOverloads() {
|
||||
EvaluationRuntimeBuilder runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new VariadicMockFunction("MY_STRING_FN", 0, Type.STRING));
|
||||
|
||||
assertThatThrownBy(() -> runtime.addFunction(
|
||||
new VariadicMockFunction("MY_STRING_FN", 1, Type.STRING)
|
||||
)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void addSimpleFunctionFails() {
|
||||
EvaluationRuntimeBuilder runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_FN",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
));
|
||||
|
||||
assertThatThrownBy(() -> runtime.addFunction(
|
||||
new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_FN",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
)
|
||||
)).isInstanceOf(IllegalArgumentException.class);
|
||||
assertThatThrownBy(() -> runtime.addFunction(
|
||||
new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_FN",
|
||||
Integer.class,
|
||||
s -> s % 2 == 0
|
||||
)
|
||||
)).isInstanceOf(IllegalArgumentException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void customFunctionSpecTest() {
|
||||
EvaluationRuntime runtime = EvaluationRuntime.builder()
|
||||
.addFunction(new InfallibleOneArgumentFunction<>(
|
||||
"MY_STRING_PREDICATE",
|
||||
String.class,
|
||||
s -> s.length() % 2 == 0
|
||||
))
|
||||
.build();
|
||||
|
||||
CloudEvent event = CloudEventBuilder.v1()
|
||||
.withId(Data.ID)
|
||||
.withSource(Data.SOURCE)
|
||||
.withType(Data.TYPE)
|
||||
.withExtension("sequence", "12")
|
||||
.build();
|
||||
|
||||
assertThat(
|
||||
Parser.parseDefault("MY_STRING_PREDICATE(sequence + 10)")
|
||||
.evaluate(runtime, event)
|
||||
)
|
||||
.isNotFailed()
|
||||
.asBoolean()
|
||||
.isTrue();
|
||||
|
||||
}
|
||||
|
||||
private static class VariadicMockFunction extends BaseFunction {
|
||||
|
||||
private final int fixedArgs;
|
||||
private final Type argsType;
|
||||
|
||||
private VariadicMockFunction(String name, int fixedArgs, Type argsType) {
|
||||
super(name);
|
||||
this.fixedArgs = fixedArgs;
|
||||
this.argsType = argsType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(EvaluationContext ctx, EvaluationRuntime evaluationRuntime, CloudEvent event, List<Object> arguments) {
|
||||
return arguments.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Type typeOfParameter(int i) throws IllegalArgumentException {
|
||||
return argsType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int arity() {
|
||||
return fixedArgs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isVariadic() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
package io.cloudevents.sql;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
|
||||
import io.cloudevents.CloudEvent;
|
||||
import io.cloudevents.core.builder.CloudEventBuilder;
|
||||
import io.cloudevents.core.test.Data;
|
||||
import io.cloudevents.jackson.JsonFormat;
|
||||
import io.cloudevents.sql.impl.ParserImpl;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static io.cloudevents.sql.asserts.MyAssertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
public class TCKTestSuite {
|
||||
|
||||
public static class TestSuiteModel {
|
||||
public String name;
|
||||
public List<TestCaseModel> tests;
|
||||
}
|
||||
|
||||
public enum Error {
|
||||
PARSE("parse"),
|
||||
MATH("math"),
|
||||
CAST("cast"),
|
||||
MISSING_ATTRIBUTE("missingAttribute"),
|
||||
MISSING_FUNCTION("missingFunction"),
|
||||
FUNCTION_EVALUATION("functionEvaluation");
|
||||
|
||||
private final String name;
|
||||
|
||||
Error(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TestCaseModel {
|
||||
public String name;
|
||||
public String expression;
|
||||
public Object result;
|
||||
public CloudEvent event;
|
||||
public Map<String, Object> eventOverrides;
|
||||
public Error error;
|
||||
|
||||
public CloudEvent getTestInputEvent() {
|
||||
CloudEvent inputEvent = (this.event == null) ? Data.V1_MIN : this.event;
|
||||
if (this.eventOverrides != null) {
|
||||
CloudEventBuilder builder = CloudEventBuilder.from(inputEvent);
|
||||
this.eventOverrides.forEach((k, v) -> {
|
||||
if (v instanceof String) {
|
||||
builder.withContextAttribute(k, (String) v);
|
||||
} else if (v instanceof Boolean) {
|
||||
builder.withContextAttribute(k, (Boolean) v);
|
||||
} else if (v instanceof Number) {
|
||||
builder.withContextAttribute(k, ((Number) v).intValue());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unexpected event override attribute '" + k + "' type: " + v.getClass());
|
||||
}
|
||||
});
|
||||
inputEvent = builder.build();
|
||||
}
|
||||
return inputEvent;
|
||||
}
|
||||
|
||||
public EvaluationException.ErrorKind getEvaluationExceptionErrorKind() {
|
||||
switch (this.error) {
|
||||
case CAST:
|
||||
return EvaluationException.ErrorKind.INVALID_CAST;
|
||||
case MATH:
|
||||
return EvaluationException.ErrorKind.MATH;
|
||||
case MISSING_FUNCTION:
|
||||
return EvaluationException.ErrorKind.FUNCTION_DISPATCH;
|
||||
case MISSING_ATTRIBUTE:
|
||||
return EvaluationException.ErrorKind.MISSING_ATTRIBUTE;
|
||||
case FUNCTION_EVALUATION:
|
||||
return EvaluationException.ErrorKind.FUNCTION_EXECUTION;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Stream<Map.Entry<String, TestCaseModel>> tckTestCases() {
|
||||
ObjectMapper mapper = new YAMLMapper();
|
||||
mapper.registerModule(JsonFormat.getCloudEventJacksonModule());
|
||||
|
||||
// Files to load
|
||||
Stream<String> tckFiles = Stream.of(
|
||||
"binary_math_operators",
|
||||
"binary_logical_operators",
|
||||
"binary_comparison_operators",
|
||||
"case_sensitivity",
|
||||
"casting_functions",
|
||||
"context_attributes_access",
|
||||
"exists_expression",
|
||||
"in_expression",
|
||||
"integer_builtin_functions",
|
||||
"like_expression",
|
||||
"literals",
|
||||
"negate_operator",
|
||||
"not_operator",
|
||||
"parse_errors",
|
||||
"spec_examples",
|
||||
"string_builtin_functions",
|
||||
"sub_expression"
|
||||
).map(fileName -> "/tck/" + fileName + ".yaml");
|
||||
|
||||
return tckFiles
|
||||
.map(fileName -> {
|
||||
try {
|
||||
return mapper.readValue(this.getClass().getResource(fileName), TestSuiteModel.class);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.flatMap(m -> m.tests.stream().map(tc -> new AbstractMap.SimpleImmutableEntry<>(m.name + ": " + tc.name, tc)));
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> evaluate() {
|
||||
Parser parser = new ParserImpl();
|
||||
|
||||
return DynamicTest.stream(
|
||||
tckTestCases(),
|
||||
Map.Entry::getKey,
|
||||
tcEntry -> evaluateTestCase(parser, tcEntry.getValue())
|
||||
);
|
||||
}
|
||||
|
||||
@TestFactory
|
||||
Stream<DynamicTest> tryEvaluate() {
|
||||
Parser parser = new ParserImpl();
|
||||
|
||||
return DynamicTest.stream(
|
||||
tckTestCases(),
|
||||
Map.Entry::getKey,
|
||||
tcEntry -> tryEvaluateTestCase(parser, tcEntry.getValue())
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public void evaluateTestCase(Parser parser, TestCaseModel testCase) throws Exception {
|
||||
// Assert parse errors
|
||||
if (testCase.error == Error.PARSE) {
|
||||
assertThatCode(() -> parser.parse(testCase.expression))
|
||||
.isInstanceOf(ParseException.class);
|
||||
return;
|
||||
}
|
||||
|
||||
Expression expression = parser.parse(testCase.expression);
|
||||
assertThat(expression).isNotNull();
|
||||
|
||||
Result result = expression.evaluate(testCase.getTestInputEvent());
|
||||
|
||||
if (testCase.result != null) {
|
||||
assertThat(result)
|
||||
.value()
|
||||
.isEqualTo(testCase.result);
|
||||
}
|
||||
|
||||
if (testCase.error == null) {
|
||||
assertThat(result)
|
||||
.isNotFailed();
|
||||
} else {
|
||||
assertThat(result)
|
||||
.hasFailure(testCase.getEvaluationExceptionErrorKind());
|
||||
}
|
||||
}
|
||||
|
||||
public void tryEvaluateTestCase(Parser parser, TestCaseModel testCase) throws Exception {
|
||||
// Assert parse errors
|
||||
if (testCase.error == Error.PARSE) {
|
||||
assertThatCode(() -> parser.parse(testCase.expression))
|
||||
.isInstanceOf(ParseException.class);
|
||||
return;
|
||||
}
|
||||
|
||||
Expression expression = parser.parse(testCase.expression);
|
||||
assertThat(expression).isNotNull();
|
||||
|
||||
if (testCase.error != null) {
|
||||
assertThatCode(() -> expression.tryEvaluate(testCase.getTestInputEvent()))
|
||||
.isInstanceOf(EvaluationException.class)
|
||||
.extracting(t -> ((EvaluationException) t).getKind())
|
||||
.isEqualTo(testCase.getEvaluationExceptionErrorKind());
|
||||
} else {
|
||||
assertThat(
|
||||
expression.tryEvaluate(testCase.getTestInputEvent())
|
||||
).isEqualTo(testCase.result);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package io.cloudevents.sql.asserts;
|
||||
|
||||
import io.cloudevents.sql.Result;
|
||||
|
||||
public class MyAssertions {
|
||||
|
||||
public static ResultAssert assertThat(Result result) {
|
||||
return new ResultAssert(result);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
package io.cloudevents.sql.asserts;
|
||||
|
||||
import io.cloudevents.sql.EvaluationException;
|
||||
import io.cloudevents.sql.Result;
|
||||
import org.assertj.core.api.*;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.InstanceOfAssertFactories.*;
|
||||
|
||||
public class ResultAssert extends AbstractAssert<ResultAssert, Result> {
|
||||
|
||||
public ResultAssert(Result actual) {
|
||||
super(actual, ResultAssert.class);
|
||||
}
|
||||
|
||||
public BooleanAssert asBoolean() {
|
||||
isNotNull();
|
||||
return (BooleanAssert) assertThat(this.actual.value())
|
||||
.asInstanceOf(BOOLEAN);
|
||||
}
|
||||
|
||||
public IntegerAssert asInteger() {
|
||||
isNotNull();
|
||||
return (IntegerAssert) assertThat(this.actual.value())
|
||||
.asInstanceOf(INTEGER);
|
||||
}
|
||||
|
||||
public StringAssert asString() {
|
||||
isNotNull();
|
||||
return (StringAssert) assertThat(this.actual.value())
|
||||
.asInstanceOf(STRING);
|
||||
}
|
||||
|
||||
public ResultAssert isNotFailed() {
|
||||
isNotNull();
|
||||
assertThat(this.actual.isFailed())
|
||||
.withFailMessage(
|
||||
"Failed with causes: %s",
|
||||
this.actual.causes()
|
||||
)
|
||||
.isFalse();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResultAssert isFailed() {
|
||||
isNotNull();
|
||||
assertThat(this.actual.isFailed()).isTrue();
|
||||
return this;
|
||||
}
|
||||
|
||||
public ObjectAssert<Object> value() {
|
||||
isNotNull();
|
||||
return assertThat(this.actual.value());
|
||||
}
|
||||
|
||||
public IterableAssert<EvaluationException> causes() {
|
||||
return assertThat(this.actual.causes());
|
||||
}
|
||||
|
||||
public ResultAssert hasFailure(EvaluationException.ErrorKind exceptionErrorKind) {
|
||||
causes()
|
||||
.anySatisfy(ex ->
|
||||
assertThat(ex.getKind())
|
||||
.isEqualTo(exceptionErrorKind)
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public ResultAssert hasFailure(EvaluationException.ErrorKind exceptionErrorKind, String expression) {
|
||||
causes()
|
||||
.anySatisfy(ex -> {
|
||||
assertThat(ex.getKind())
|
||||
.isEqualTo(exceptionErrorKind);
|
||||
assertThat(ex.getExpressionText())
|
||||
.isEqualTo(expression);
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# CloudEvents Expression Language TCK
|
||||
|
||||
Each file of this TCK contains a set of test cases, testing one or more specific features of the language.
|
||||
|
||||
The root file structure is composed by:
|
||||
|
||||
* `name`: Name of the test suite contained in the file
|
||||
* `tests`: List of tests
|
||||
|
||||
Each test definition includes:
|
||||
|
||||
* `name`: Name of the test case
|
||||
* `expression`: Expression to test.
|
||||
* `result`: Expected result (optional). Can be a boolean, an integer or a string.
|
||||
* `error`: Expected error (optional). If absent, no error is expected.
|
||||
* `event`: Input event (optional). If present, this is a valid event serialized in JSON format. If absent, when testing
|
||||
the expression, any valid event can be passed.
|
||||
* `eventOverrides`: Overrides to the input event (optional). This might be used when `event` is missing, in order to
|
||||
define only some specific values, while the other (required) attributes can be any value.
|
||||
|
||||
The `error` values could be any of the following:
|
||||
|
||||
* `parse`: Error while parsing the expression
|
||||
* `math`: Math error while evaluating a math operator
|
||||
* `cast`: Casting error
|
||||
* `missingAttribute`: Addressed a missing attribute
|
||||
* `missingFunction`: Addressed a missing function
|
||||
* `functionEvaluation`: Error while evaluating a function
|
|
@ -0,0 +1,90 @@
|
|||
name: Binary comparison operations
|
||||
tests:
|
||||
- name: True is equal to false
|
||||
expression: TRUE = FALSE
|
||||
result: false
|
||||
- name: False is equal to false
|
||||
expression: FALSE = FALSE
|
||||
result: true
|
||||
- name: 1 is equal to 2
|
||||
expression: 1 = 2
|
||||
result: false
|
||||
- name: 2 is equal to 2
|
||||
expression: 2 = 2
|
||||
result: true
|
||||
- name: abc is equal to 123
|
||||
expression: "'abc' = '123'"
|
||||
result: false
|
||||
- name: abc is equal to abc
|
||||
expression: "'abc' = 'abc'"
|
||||
result: true
|
||||
|
||||
- name: True is not equal to false
|
||||
expression: TRUE != FALSE
|
||||
result: true
|
||||
- name: False is not equal to false
|
||||
expression: FALSE != FALSE
|
||||
result: false
|
||||
- name: 1 is not equal to 2
|
||||
expression: 1 != 2
|
||||
result: true
|
||||
- name: 2 is not equal to 2
|
||||
expression: 2 != 2
|
||||
result: false
|
||||
- name: abc is not equal to 123
|
||||
expression: "'abc' != '123'"
|
||||
result: true
|
||||
- name: abc is not equal to abc
|
||||
expression: "'abc' != 'abc'"
|
||||
result: false
|
||||
|
||||
- name: True is not equal to false (diamond operator)
|
||||
expression: TRUE <> FALSE
|
||||
result: true
|
||||
- name: False is not equal to false (diamond operator)
|
||||
expression: FALSE <> FALSE
|
||||
result: false
|
||||
- name: 1 is not equal to 2 (diamond operator)
|
||||
expression: 1 <> 2
|
||||
result: true
|
||||
- name: 2 is not equal to 2 (diamond operator)
|
||||
expression: 2 <> 2
|
||||
result: false
|
||||
- name: abc is not equal to 123 (diamond operator)
|
||||
expression: "'abc' <> '123'"
|
||||
result: true
|
||||
- name: abc is not equal to abc (diamond operator)
|
||||
expression: "'abc' <> 'abc'"
|
||||
result: false
|
||||
|
||||
- name: 1 is less or equal than 2
|
||||
expression: 2 <= 2
|
||||
result: true
|
||||
- name: 3 is less or equal than 2
|
||||
expression: 3 <= 2
|
||||
result: false
|
||||
- name: 1 is less than 2
|
||||
expression: 1 < 2
|
||||
result: true
|
||||
- name: 2 is less than 2
|
||||
expression: 2 < 2
|
||||
result: false
|
||||
- name: 2 is greater or equal than 2
|
||||
expression: 2 >= 2
|
||||
result: true
|
||||
- name: 2 is greater or equal than 3
|
||||
expression: 2 >= 3
|
||||
result: false
|
||||
- name: 2 is greater than 1
|
||||
expression: 2 > 1
|
||||
result: true
|
||||
- name: 2 is greater than 2
|
||||
expression: 2 > 2
|
||||
result: false
|
||||
|
||||
- name: implicit casting with string as right type
|
||||
expression: "true = 'TRUE'"
|
||||
result: false
|
||||
- name: implicit casting with boolean as right type
|
||||
expression: "'TRUE' = true"
|
||||
result: true
|
|
@ -0,0 +1,40 @@
|
|||
name: Binary logical operations
|
||||
tests:
|
||||
- name: False and false
|
||||
expression: FALSE AND FALSE
|
||||
result: false
|
||||
- name: False and true
|
||||
expression: FALSE AND TRUE
|
||||
result: false
|
||||
- name: True and false
|
||||
expression: TRUE AND FALSE
|
||||
result: false
|
||||
- name: True and true
|
||||
expression: TRUE AND TRUE
|
||||
result: true
|
||||
|
||||
- name: False or false
|
||||
expression: FALSE OR FALSE
|
||||
result: false
|
||||
- name: False or true
|
||||
expression: FALSE OR TRUE
|
||||
result: true
|
||||
- name: True or false
|
||||
expression: TRUE OR FALSE
|
||||
result: true
|
||||
- name: True or true
|
||||
expression: TRUE OR TRUE
|
||||
result: true
|
||||
|
||||
- name: False xor false
|
||||
expression: FALSE XOR FALSE
|
||||
result: false
|
||||
- name: False xor true
|
||||
expression: FALSE XOR TRUE
|
||||
result: true
|
||||
- name: True xor false
|
||||
expression: TRUE XOR FALSE
|
||||
result: true
|
||||
- name: True xor true
|
||||
expression: TRUE XOR TRUE
|
||||
result: false
|
|
@ -0,0 +1,60 @@
|
|||
name: Binary math operations
|
||||
tests:
|
||||
- name: Operator precedence without parenthesis
|
||||
expression: 4 * 2 + 4 / 2
|
||||
result: 10
|
||||
- name: Operator precedence with parenthesis
|
||||
expression: 4 * (2 + 4) / 2
|
||||
result: 12
|
||||
|
||||
- name: Truncated division
|
||||
expression: 5 / 3
|
||||
result: 1
|
||||
- name: Division by zero returns 0 and fail
|
||||
expression: 5 / 0
|
||||
result: 0
|
||||
error: math
|
||||
- name: Module
|
||||
expression: 5 % 2
|
||||
result: 1
|
||||
- name: Module by zero returns 0 and fail
|
||||
expression: 5 % 0
|
||||
result: 0
|
||||
error: math
|
||||
|
||||
- name: Positive plus positive number
|
||||
expression: 4 + 1
|
||||
result: 5
|
||||
- name: Negative plus positive number
|
||||
expression: -4 + 1
|
||||
result: -3
|
||||
- name: Negative plus Negative number
|
||||
expression: -4 + -1
|
||||
result: -5
|
||||
- name: Positive plus negative number
|
||||
expression: 4 + -1
|
||||
result: 3
|
||||
- name: Positive minus positive number
|
||||
expression: 4 - 1
|
||||
result: 3
|
||||
- name: Negative minus positive number
|
||||
expression: -4 - 1
|
||||
result: -5
|
||||
|
||||
- name: Implicit casting, with left value string
|
||||
expression: "'5' + 3"
|
||||
result: 8
|
||||
- name: Implicit casting, with right value string
|
||||
expression: "5 + '3'"
|
||||
result: 8
|
||||
- name: Implicit casting, with both values string
|
||||
expression: "'5' + '3'"
|
||||
result: 8
|
||||
- name: Implicit casting, with invalid boolean value
|
||||
expression: "5 + TRUE"
|
||||
result: 5
|
||||
error: cast
|
||||
- name: Implicit casting, with invalid string value
|
||||
expression: "'5avc4' + 10"
|
||||
result: 10
|
||||
error: cast
|
|
@ -0,0 +1,25 @@
|
|||
name: Case sensitivity
|
||||
tests:
|
||||
- name: TRUE
|
||||
expression: TRUE
|
||||
result: true
|
||||
- name: true
|
||||
expression: true
|
||||
result: true
|
||||
- name: tRuE
|
||||
expression: tRuE
|
||||
result: true
|
||||
|
||||
- name: FALSE
|
||||
expression: FALSE
|
||||
result: false
|
||||
- name: false
|
||||
expression: false
|
||||
result: false
|
||||
- name: FaLsE
|
||||
expression: FaLsE
|
||||
result: false
|
||||
|
||||
- name: String literals casing preserved
|
||||
expression: "'aBcD'"
|
||||
result: aBcD
|
|
@ -0,0 +1,89 @@
|
|||
name: Casting functions
|
||||
tests:
|
||||
- name: Cast '1' to integer
|
||||
expression: INT('1')
|
||||
result: 1
|
||||
- name: Cast '-1' to integer
|
||||
expression: INT('-1')
|
||||
result: -1
|
||||
- name: Cast identity 1
|
||||
expression: INT(1)
|
||||
result: 1
|
||||
- name: Cast identity -1
|
||||
expression: INT(-1)
|
||||
result: -1
|
||||
- name: Invalid cast from boolean to int
|
||||
expression: INT(TRUE)
|
||||
result: 0
|
||||
error: cast
|
||||
- name: Invalid cast from string to int
|
||||
expression: INT('ABC')
|
||||
result: 0
|
||||
error: cast
|
||||
|
||||
- name: Cast 'TRUE' to boolean
|
||||
expression: BOOL('TRUE')
|
||||
result: true
|
||||
- name: Cast "false" to boolean
|
||||
expression: BOOL("false")
|
||||
result: false
|
||||
- name: Cast identity TRUE
|
||||
expression: BOOL(TRUE)
|
||||
result: true
|
||||
- name: Cast identity FALSE
|
||||
expression: BOOL(FALSE)
|
||||
result: FALSE
|
||||
- name: Invalid cast from string to boolean
|
||||
expression: BOOL('ABC')
|
||||
result: false
|
||||
error: cast
|
||||
- name: Invalid cast from int to boolean
|
||||
expression: BOOL(1)
|
||||
result: false
|
||||
error: cast
|
||||
|
||||
- name: Cast TRUE to string
|
||||
expression: STRING(TRUE)
|
||||
result: 'true'
|
||||
- name: Cast FALSE to string
|
||||
expression: STRING(FALSE)
|
||||
result: 'false'
|
||||
- name: Cast 1 to string
|
||||
expression: STRING(1)
|
||||
result: '1'
|
||||
- name: Cast -1 to string
|
||||
expression: STRING(-1)
|
||||
result: '-1'
|
||||
- name: Cast identity "abc"
|
||||
expression: STRING("abc")
|
||||
result: "abc"
|
||||
|
||||
- name: "'true' is a boolean"
|
||||
expression: IS_BOOL('true')
|
||||
result: true
|
||||
- name: "'FALSE' is a boolean"
|
||||
expression: IS_BOOL('FALSE')
|
||||
result: true
|
||||
- name: 1 is not a boolean
|
||||
expression: IS_BOOL(1)
|
||||
result: false
|
||||
- name: "'abc' is not a boolean"
|
||||
expression: IS_BOOL('abc')
|
||||
result: false
|
||||
|
||||
- name: "'-1' is an int"
|
||||
expression: IS_INT('-1')
|
||||
result: true
|
||||
- name: "'1' is an int"
|
||||
expression: IS_INT('1')
|
||||
result: true
|
||||
- name: true is not an int
|
||||
expression: IS_INT(TRUE)
|
||||
result: false
|
||||
- name: "'abc' is not an int"
|
||||
expression: IS_INT('abc')
|
||||
result: false
|
||||
|
||||
- name: IS_STRING does not exists
|
||||
expression: IS_STRING('ABC')
|
||||
error: missingFunction
|
|
@ -0,0 +1,53 @@
|
|||
name: Context attributest test
|
||||
tests:
|
||||
- name: Access to required attribute
|
||||
expression: id
|
||||
eventOverrides:
|
||||
id: myId
|
||||
result: myId
|
||||
- name: Access to optional attribute
|
||||
expression: subject
|
||||
eventOverrides:
|
||||
subject: mySubject
|
||||
result: mySubject
|
||||
- name: Absent optional attribute
|
||||
expression: subject
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: localhost.localdomain
|
||||
type: myType
|
||||
result: ""
|
||||
error: missingAttribute
|
||||
- name: Access to optional boolean extension
|
||||
expression: mybool
|
||||
eventOverrides:
|
||||
mybool: true
|
||||
result: true
|
||||
- name: Access to optional integer extension
|
||||
expression: myint
|
||||
eventOverrides:
|
||||
myint: 10
|
||||
result: 10
|
||||
- name: Access to optional string extension
|
||||
expression: myext
|
||||
eventOverrides:
|
||||
myext: "my extension"
|
||||
result: "my extension"
|
||||
- name: URL type cohercion to string
|
||||
expression: source
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: "http://localhost/source"
|
||||
- name: Timestamp type cohercion to string
|
||||
expression: time
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
time: 2018-04-26T14:48:09+02:00
|
||||
result: 2018-04-26T14:48:09+02:00
|
|
@ -0,0 +1,57 @@
|
|||
name: Exists expression
|
||||
tests:
|
||||
- name: required attributes always exist
|
||||
expression: EXISTS specversion AND EXISTS id AND EXISTS type AND EXISTS SOURCE
|
||||
result: true
|
||||
|
||||
- name: optional attribute available
|
||||
expression: EXISTS time
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
time: 2018-04-26T14:48:09+02:00
|
||||
result: true
|
||||
- name: optional attribute absent
|
||||
expression: EXISTS time
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: false
|
||||
- name: optional attribute absent (negated)
|
||||
expression: NOT EXISTS time
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: true
|
||||
|
||||
- name: optional extension available
|
||||
expression: EXISTS myext
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
myext: my value
|
||||
result: true
|
||||
- name: optional extension absent
|
||||
expression: EXISTS myext
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: false
|
||||
- name: optional extension absent (negated)
|
||||
expression: NOT EXISTS myext
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: true
|
|
@ -0,0 +1,78 @@
|
|||
name: In expression
|
||||
tests:
|
||||
- name: int in int set
|
||||
expression: 123 IN (1, 2, 3, 12, 13, 23, 123)
|
||||
result: true
|
||||
- name: int not in int set
|
||||
expression: 123 NOT IN (1, 2, 3, 12, 13, 23, 123)
|
||||
result: false
|
||||
|
||||
- name: string in string set
|
||||
expression: "'abc' IN ('abc', \"bcd\")"
|
||||
result: true
|
||||
- name: string not in string set
|
||||
expression: "'aaa' IN ('abc', \"bcd\")"
|
||||
result: false
|
||||
|
||||
- name: bool in bool set
|
||||
expression: TRUE IN (TRUE, FALSE)
|
||||
result: true
|
||||
- name: bool not in bool set
|
||||
expression: TRUE IN (FALSE)
|
||||
result: false
|
||||
|
||||
- name: mix literals and identifiers (1)
|
||||
expression: source IN (myext, 'abc')
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
myext: "http://localhost/source"
|
||||
result: true
|
||||
- name: mix literals and identifiers (2)
|
||||
expression: source IN (source)
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
myext: "http://localhost/source"
|
||||
result: true
|
||||
- name: mix literals and identifiers (3)
|
||||
expression: "source IN (id, \"http://localhost/source\")"
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
myext: "http://localhost/source"
|
||||
result: true
|
||||
- name: mix literals and identifiers (4)
|
||||
expression: source IN (id, 'xyz')
|
||||
event:
|
||||
specversion: "1.0"
|
||||
id: myId
|
||||
source: "http://localhost/source"
|
||||
type: myType
|
||||
result: false
|
||||
|
||||
- name: type coercion with booleans (1)
|
||||
expression: "'true' IN (TRUE, 'false')"
|
||||
result: true
|
||||
- name: type coercion with booleans (2)
|
||||
expression: "'true' IN ('TRUE', 'false')"
|
||||
result: false
|
||||
- name: type coercion with booleans (3)
|
||||
expression: TRUE IN ('true', 'false')
|
||||
result: true
|
||||
- name: type coercion with booleans (4)
|
||||
expression: "'TRUE' IN (TRUE, 'false')"
|
||||
result: false
|
||||
|
||||
- name: type coercion with int (1)
|
||||
expression: "1 IN ('1', '2')"
|
||||
result: true
|
||||
- name: type coercion with int (2)
|
||||
expression: "'1' IN (1, 2)"
|
||||
result: true
|
|
@ -0,0 +1,11 @@
|
|||
name: Integer builtin functions
|
||||
tests:
|
||||
- name: ABS (1)
|
||||
expression: ABS(10)
|
||||
result: 10
|
||||
- name: ABS (2)
|
||||
expression: ABS(-10)
|
||||
result: 10
|
||||
- name: ABS (3)
|
||||
expression: ABS(0)
|
||||
result: 0
|
|
@ -0,0 +1,80 @@
|
|||
name: Like expression
|
||||
tests:
|
||||
- name: Exact match
|
||||
expression: "'abc' LIKE 'abc'"
|
||||
result: true
|
||||
- name: Exact match (negate)
|
||||
expression: "'abc' NOT LIKE 'abc'"
|
||||
result: false
|
||||
|
||||
- name: Percentage operator (1)
|
||||
expression: "'abc' LIKE 'a%b%c'"
|
||||
result: true
|
||||
- name: Percentage operator (2)
|
||||
expression: "'azbc' LIKE 'a%b%c'"
|
||||
result: true
|
||||
- name: Percentage operator (3)
|
||||
expression: "'azzzbzzzc' LIKE 'a%b%c'"
|
||||
result: true
|
||||
- name: Percentage operator (4)
|
||||
expression: "'a%b%c' LIKE 'a%b%c'"
|
||||
result: true
|
||||
- name: Percentage operator (5)
|
||||
expression: "'ac' LIKE 'abc'"
|
||||
result: false
|
||||
- name: Percentage operator (6)
|
||||
expression: "'' LIKE 'abc'"
|
||||
result: false
|
||||
|
||||
- name: Underscore operator (1)
|
||||
expression: "'abc' LIKE 'a_b_c'"
|
||||
result: false
|
||||
- name: Underscore operator (2)
|
||||
expression: "'a_b_c' LIKE 'a_b_c'"
|
||||
result: true
|
||||
- name: Underscore operator (3)
|
||||
expression: "'abzc' LIKE 'a_b_c'"
|
||||
result: false
|
||||
- name: Underscore operator (4)
|
||||
expression: "'azbc' LIKE 'a_b_c'"
|
||||
result: false
|
||||
- name: Underscore operator (5)
|
||||
expression: "'azbzc' LIKE 'a_b_c'"
|
||||
result: true
|
||||
|
||||
- name: Escaped underscore wildcards (1)
|
||||
expression: "'a_b_c' LIKE 'a\\_b\\_c'"
|
||||
result: true
|
||||
- name: Escaped underscore wildcards (2)
|
||||
expression: "'a_b_c' NOT LIKE 'a\\_b\\_c'"
|
||||
result: false
|
||||
- name: Escaped underscore wildcards (3)
|
||||
expression: "'azbzc' LIKE 'a\\_b\\_c'"
|
||||
result: false
|
||||
- name: Escaped underscore wildcards (4)
|
||||
expression: "'abc' LIKE 'a\\_b\\_c'"
|
||||
result: false
|
||||
|
||||
- name: Escaped percentage wildcards (1)
|
||||
expression: "'abc' LIKE 'a\\%b\\%c'"
|
||||
result: false
|
||||
- name: Escaped percentage wildcards (2)
|
||||
expression: "'a%b%c' LIKE 'a\\%b\\%c'"
|
||||
result: true
|
||||
- name: Escaped percentage wildcards (3)
|
||||
expression: "'azbzc' LIKE 'a\\%b\\%c'"
|
||||
result: false
|
||||
- name: Escaped percentage wildcards (4)
|
||||
expression: "'abc' LIKE 'a\\%b\\%c'"
|
||||
result: false
|
||||
|
||||
- name: With access to event attributes
|
||||
expression: "myext LIKE 'abc%123\\%456\\_d_f'"
|
||||
eventOverrides:
|
||||
myext: "abc123123%456_dzf"
|
||||
result: true
|
||||
- name: With access to event attributes (negated)
|
||||
expression: "myext NOT LIKE 'abc%123\\%456\\_d_f'"
|
||||
eventOverrides:
|
||||
myext: "abc123123%456_dzf"
|
||||
result: false
|
|
@ -0,0 +1,36 @@
|
|||
name: Literals
|
||||
tests:
|
||||
- name: TRUE literal
|
||||
expression: TRUE
|
||||
result: true
|
||||
- name: FALSE literal
|
||||
expression: FALSE
|
||||
result: false
|
||||
|
||||
- name: 0 literal
|
||||
expression: 0
|
||||
result: 0
|
||||
- name: 1 literal
|
||||
expression: 1
|
||||
result: 1
|
||||
|
||||
- name: String literal single quoted
|
||||
expression: "'abc'"
|
||||
result: abc
|
||||
- name: String literal double quoted
|
||||
expression: "\"abc\""
|
||||
result: abc
|
||||
|
||||
- name: String literal single quoted with case
|
||||
expression: "'aBc'"
|
||||
result: aBc
|
||||
- name: String literal double quoted with case
|
||||
expression: "\"AbC\""
|
||||
result: AbC
|
||||
|
||||
- name: Escaped string literal (1)
|
||||
expression: "'a\"b\\'c'"
|
||||
result: a"b'c
|
||||
- name: Escaped string literal (2)
|
||||
expression: "\"a'b\\\"c\""
|
||||
result: a'b"c
|
|
@ -0,0 +1,20 @@
|
|||
name: Negate operator
|
||||
tests:
|
||||
- name: Minus 10
|
||||
expression: -10
|
||||
result: -10
|
||||
- name: Minus minus 10
|
||||
expression: --10
|
||||
result: 10
|
||||
|
||||
- name: Minus 10 with casting
|
||||
expression: -'10'
|
||||
result: -10
|
||||
- name: Minus minus 10 with casting
|
||||
expression: --'10'
|
||||
result: 10
|
||||
|
||||
- name: Invalid boolean cast
|
||||
expression: -TRUE
|
||||
result: 0
|
||||
error: cast
|
|
@ -0,0 +1,20 @@
|
|||
name: Not operator
|
||||
tests:
|
||||
- name: Not true
|
||||
expression: NOT TRUE
|
||||
result: false
|
||||
- name: Not false
|
||||
expression: NOT FALSE
|
||||
result: true
|
||||
|
||||
- name: Not true with casting
|
||||
expression: NOT 'TRUE'
|
||||
result: false
|
||||
- name: Not false 10 with casting
|
||||
expression: NOT 'FALSE'
|
||||
result: true
|
||||
|
||||
- name: Invalid int cast
|
||||
expression: NOT 10
|
||||
result: true
|
||||
error: cast
|
|
@ -0,0 +1,5 @@
|
|||
name: Parsing errors
|
||||
tests:
|
||||
- name: No closed parenthesis
|
||||
expression: ABC(
|
||||
error: parse
|
|
@ -0,0 +1,64 @@
|
|||
name: Specification examples
|
||||
tests:
|
||||
- name: Case insensitive hops (1)
|
||||
expression: int(hop) < int(ttl) and int(hop) < 1000
|
||||
eventOverrides:
|
||||
hop: '5'
|
||||
ttl: '10'
|
||||
result: true
|
||||
- name: Case insensitive hops (2)
|
||||
expression: INT(hop) < INT(ttl) AND INT(hop) < 1000
|
||||
eventOverrides:
|
||||
hop: '5'
|
||||
ttl: '10'
|
||||
result: true
|
||||
- name: Case insensitive hops (3)
|
||||
expression: hop < ttl
|
||||
eventOverrides:
|
||||
hop: '5'
|
||||
ttl: '10'
|
||||
result: true
|
||||
|
||||
- name: Equals with casting (1)
|
||||
expression: sequence = 5
|
||||
eventOverrides:
|
||||
sequence: '5'
|
||||
result: true
|
||||
- name: Equals with casting (2)
|
||||
expression: sequence = 5
|
||||
eventOverrides:
|
||||
sequence: '6'
|
||||
result: false
|
||||
|
||||
- name: Logic expression (1)
|
||||
expression: firstname = 'Francesco' OR subject = 'Francesco'
|
||||
eventOverrides:
|
||||
subject: Francesco
|
||||
firstname: Doug
|
||||
result: true
|
||||
- name: Logic expression (2)
|
||||
expression: firstname = 'Francesco' OR subject = 'Francesco'
|
||||
eventOverrides:
|
||||
firstname: Francesco
|
||||
subject: Doug
|
||||
result: true
|
||||
- name: Logic expression (3)
|
||||
expression: (firstname = 'Francesco' AND lastname = 'Guardiani') OR subject = 'Francesco Guardiani'
|
||||
eventOverrides:
|
||||
subject: Doug
|
||||
firstname: Francesco
|
||||
lastname: Guardiani
|
||||
result: true
|
||||
- name: Logic expression (4)
|
||||
expression: (firstname = 'Francesco' AND lastname = 'Guardiani') OR subject = 'Francesco Guardiani'
|
||||
eventOverrides:
|
||||
subject: Francesco Guardiani
|
||||
firstname: Doug
|
||||
lastname: Davis
|
||||
result: true
|
||||
|
||||
- name: Subject exists
|
||||
expression: EXISTS subject
|
||||
eventOverrides:
|
||||
subject: Francesco Guardiani
|
||||
result: true
|
|
@ -0,0 +1,142 @@
|
|||
name: String builtin functions
|
||||
tests:
|
||||
- name: LENGTH (1)
|
||||
expression: "LENGTH('abc')"
|
||||
result: 3
|
||||
- name: LENGTH (2)
|
||||
expression: "LENGTH('')"
|
||||
result: 0
|
||||
- name: LENGTH (3)
|
||||
expression: "LENGTH('2')"
|
||||
result: 1
|
||||
- name: LENGTH (4)
|
||||
expression: "LENGTH(TRUE)"
|
||||
result: 4
|
||||
|
||||
- name: CONCAT (1)
|
||||
expression: "CONCAT('a', 'b', 'c')"
|
||||
result: abc
|
||||
- name: CONCAT (2)
|
||||
expression: "CONCAT()"
|
||||
result: ""
|
||||
- name: CONCAT (3)
|
||||
expression: "CONCAT('a')"
|
||||
result: "a"
|
||||
|
||||
- name: CONCAT_WS (1)
|
||||
expression: "CONCAT_WS(',', 'a', 'b', 'c')"
|
||||
result: a,b,c
|
||||
- name: CONCAT_WS (2)
|
||||
expression: "CONCAT_WS(',')"
|
||||
result: ""
|
||||
- name: CONCAT_WS (3)
|
||||
expression: "CONCAT_WS(',', 'a')"
|
||||
result: "a"
|
||||
- name: CONCAT_WS without arguments doesn't exist
|
||||
expression: CONCAT_WS()
|
||||
error: missingFunction
|
||||
|
||||
- name: LOWER (1)
|
||||
expression: "LOWER('ABC')"
|
||||
result: abc
|
||||
- name: LOWER (2)
|
||||
expression: "LOWER('AbC')"
|
||||
result: abc
|
||||
- name: LOWER (3)
|
||||
expression: "LOWER('abc')"
|
||||
result: abc
|
||||
|
||||
- name: UPPER (1)
|
||||
expression: "UPPER('ABC')"
|
||||
result: ABC
|
||||
- name: UPPER (2)
|
||||
expression: "UPPER('AbC')"
|
||||
result: ABC
|
||||
- name: UPPER (3)
|
||||
expression: "UPPER('abc')"
|
||||
result: ABC
|
||||
|
||||
- name: TRIM (1)
|
||||
expression: "TRIM(' a b c ')"
|
||||
result: "a b c"
|
||||
- name: TRIM (2)
|
||||
expression: "TRIM(' a b c')"
|
||||
result: "a b c"
|
||||
- name: TRIM (3)
|
||||
expression: "TRIM('a b c ')"
|
||||
result: "a b c"
|
||||
- name: TRIM (4)
|
||||
expression: "TRIM('a b c')"
|
||||
result: "a b c"
|
||||
|
||||
- name: LEFT (1)
|
||||
expression: LEFT('abc', 2)
|
||||
result: ab
|
||||
- name: LEFT (2)
|
||||
expression: LEFT('abc', 10)
|
||||
result: abc
|
||||
- name: LEFT (3)
|
||||
expression: LEFT('', 0)
|
||||
result: ""
|
||||
- name: LEFT (4)
|
||||
expression: LEFT('abc', -2)
|
||||
result: "abc"
|
||||
error: functionEvaluation
|
||||
|
||||
- name: RIGHT (1)
|
||||
expression: RIGHT('abc', 2)
|
||||
result: bc
|
||||
- name: RIGHT (2)
|
||||
expression: RIGHT('abc', 10)
|
||||
result: abc
|
||||
- name: RIGHT (3)
|
||||
expression: RIGHT('', 0)
|
||||
result: ""
|
||||
- name: RIGHT (4)
|
||||
expression: RIGHT('abc', -2)
|
||||
result: "abc"
|
||||
error: functionEvaluation
|
||||
|
||||
- name: SUBSTRING (1)
|
||||
expression: "SUBSTRING('abcdef', 1)"
|
||||
result: "abcdef"
|
||||
- name: SUBSTRING (2)
|
||||
expression: "SUBSTRING('abcdef', 2)"
|
||||
result: "bcdef"
|
||||
- name: SUBSTRING (3)
|
||||
expression: "SUBSTRING('Quadratically', 5)"
|
||||
result: "ratically"
|
||||
- name: SUBSTRING (4)
|
||||
expression: "SUBSTRING('Sakila', -3)"
|
||||
result: "ila"
|
||||
- name: SUBSTRING (5)
|
||||
expression: "SUBSTRING('abcdef', 1, 6)"
|
||||
result: "abcdef"
|
||||
- name: SUBSTRING (6)
|
||||
expression: "SUBSTRING('abcdef', 2, 4)"
|
||||
result: "bcde"
|
||||
- name: SUBSTRING (7)
|
||||
expression: "SUBSTRING('Sakila', -5, 3)"
|
||||
result: "aki"
|
||||
- name: SUBSTRING (8)
|
||||
expression: "SUBSTRING('Quadratically', 0)"
|
||||
result: ""
|
||||
- name: SUBSTRING (9)
|
||||
expression: "SUBSTRING('Quadratically', 0, 1)"
|
||||
result: ""
|
||||
- name: SUBSTRING (10)
|
||||
expression: "SUBSTRING('abcdef', 10)"
|
||||
result: ""
|
||||
error: functionEvaluation
|
||||
- name: SUBSTRING (11)
|
||||
expression: "SUBSTRING('abcdef', -10)"
|
||||
result: ""
|
||||
error: functionEvaluation
|
||||
- name: SUBSTRING (12)
|
||||
expression: "SUBSTRING('abcdef', 10, 10)"
|
||||
result: ""
|
||||
error: functionEvaluation
|
||||
- name: SUBSTRING (13)
|
||||
expression: "SUBSTRING('abcdef', -10, 10)"
|
||||
result: ""
|
||||
error: functionEvaluation
|
|
@ -0,0 +1,12 @@
|
|||
name: Sub expressions
|
||||
tests:
|
||||
- name: Sub expression with literal
|
||||
expression: "(TRUE)"
|
||||
result: true
|
||||
|
||||
- name: Math (1)
|
||||
expression: "4 * (2 + 3)"
|
||||
result: 20
|
||||
- name: Math (2)
|
||||
expression: "(2 + 3) * 4"
|
||||
result: 20
|
Loading…
Reference in New Issue