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:
Francesco Guardiani 2021-04-28 14:58:55 +02:00 committed by GitHub
parent 30fd6769eb
commit 2730ae4a13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
90 changed files with 4284 additions and 0 deletions

View File

@ -76,6 +76,7 @@
<module>http/restful-ws</module>
<module>kafka</module>
<module>spring</module>
<module>sql</module>
</modules>
<properties>

93
sql/pom.xml Normal file
View File

@ -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>

View File

@ -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_]*;

View File

@ -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
;

View File

@ -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);
}

View File

@ -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
);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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();
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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()));
}
}

View File

@ -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" +
'}';
}
}

View File

@ -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
);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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']");
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
);
}
}

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
);
}
}

View File

@ -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);
});
}
}

View File

@ -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)
);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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)
);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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 "";
}
}
}

View File

@ -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);
}
}

View File

@ -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")));
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
name: Parsing errors
tests:
- name: No closed parenthesis
expression: ABC(
error: parse

View File

@ -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

View File

@ -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

View File

@ -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