diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a221ac1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target/ +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..33af0c7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + com.yetanalytics + cel-demo + jar + 1.0-SNAPSHOT + cel-demo + http://maven.apache.org + + 17 + 17 + + + + org.junit.jupiter + junit-jupiter-engine + 5.11.0-M2 + test + + + dev.cel + cel + 0.11.0 + + + org.slf4j + slf4j-api + 2.0.16 + + + com.yetanalytics + xapi-tools + 0.0.3-beta + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.2 + + + + diff --git a/src/main/java/com/yetanalytics/CelExecutor.java b/src/main/java/com/yetanalytics/CelExecutor.java new file mode 100644 index 0000000..4a74b41 --- /dev/null +++ b/src/main/java/com/yetanalytics/CelExecutor.java @@ -0,0 +1,63 @@ +package com.yetanalytics; + +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.CelValidationException; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerBuilder; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelLiteRuntime.Program; + +public class CelExecutor { + private static final Logger log = LoggerFactory.getLogger(CelExecutor.class); + private CelCompiler celCompiler = CelCompilerFactory.standardCelCompilerBuilder().build(); + + public void runExpression(String expression) { + System.out.println(expression); + } + + CelAbstractSyntaxTree compile(String expression, Map varTypeMap) { + // Compile (parse + type-check) the expression + // CelCompiler is immutable and when statically configured can be moved to a + // static final + CelCompilerBuilder builder = CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.STANDARD_MACROS) + .setResultType(SimpleType.BOOL); + + varTypeMap.forEach((k, v) -> builder.addVar(k, v)); + + CelCompiler celCompiler = builder.build(); + + try { + return celCompiler.compile(expression).getAst(); + } catch (CelValidationException e) { + throw new IllegalArgumentException( + "Failed to compile expression. Reason: " + e.getMessage(), e); + } + } + + Boolean run(CelAbstractSyntaxTree ast, Map values) { + CelRuntime runtime = CelRuntimeFactory.standardCelRuntimeBuilder().build(); + + Boolean result = null; + try { + Program program = runtime.createProgram(ast); + result = (Boolean) program.eval(values); + } catch (CelEvaluationException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + return result; + } + +} diff --git a/src/test/java/com/yetanalytics/CelExecutorTest.java b/src/test/java/com/yetanalytics/CelExecutorTest.java new file mode 100644 index 0000000..93dae47 --- /dev/null +++ b/src/test/java/com/yetanalytics/CelExecutorTest.java @@ -0,0 +1,127 @@ +package com.yetanalytics; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yetanalytics.model.Container; +import com.yetanalytics.model.ListElement; +import com.yetanalytics.util.ReadStatement; +import com.yetanalytics.xapi.model.Statement; + +import dev.cel.common.CelAbstractSyntaxTree; +import dev.cel.common.types.CelType; +import dev.cel.common.types.SimpleType; + +public class CelExecutorTest { + + private static final Logger log = LoggerFactory.getLogger(CelExecutor.class); + + @Test + public void testEasyCommand() { + CelExecutor ce = new CelExecutor(); + ce.runExpression("Blargh"); + } + + @Test + public void testMediumCommand() { + CelExecutor ce = new CelExecutor(); + ce.runExpression("Bleeegh"); + } + + + private final String EXP_VERB_AND_ACT_ID = "statement.verb.id == '%s' && statement.object.id == '%s'"; + private final String VAR_VERB_ID_CORRECT = "https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/set"; + private final String VAR_OBJ_ID_CORRECT = "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1"; + + + @Test + public void evaluateStatementConditionCorrect() throws URISyntaxException { + Statement statement = ReadStatement.getJsonTestFile("basic"); + + Map typeMap = new HashMap(); + typeMap.put("statement", SimpleType.DYN); + + Map dyn = new ObjectMapper().convertValue(statement, new TypeReference<>() {}); + Map varMap = new HashMap(); + varMap.put("statement", dyn); + + CelExecutor ce = new CelExecutor(); + String expression = String.format(EXP_VERB_AND_ACT_ID, VAR_VERB_ID_CORRECT, VAR_OBJ_ID_CORRECT); + + CelAbstractSyntaxTree ast = ce.compile(expression, typeMap); + + Boolean result = ce.run(ast, varMap); + + assertTrue(result); + } + + @Test + public void evaluateStatementConditionIncorrect() throws URISyntaxException { + Statement statement = ReadStatement.getJsonTestFile("basic"); + + Map typeMap = new HashMap(); + typeMap.put("statement", SimpleType.DYN); + + Map dyn = new ObjectMapper().convertValue(statement, new TypeReference<>() {}); + Map varMap = new HashMap(); + varMap.put("statement", dyn); + + CelExecutor ce = new CelExecutor(); + String expression = String.format(EXP_VERB_AND_ACT_ID, "verbo", VAR_OBJ_ID_CORRECT); + + CelAbstractSyntaxTree ast = ce.compile(expression, typeMap); + + Boolean result = ce.run(ast, varMap); + + assertFalse(result); + } + + private final String EXP_ITERATION = "data.elements.filter(x, x.value == '%s').size() > 0"; + + @Test + public void testIteration() throws URISyntaxException { + // make a stupid object with a list inside + Container container = new Container(); + container.setElements(new ArrayList()); + container.getElements().add(new ListElement("Thing1")); + container.getElements().add(new ListElement("Thing2")); + container.getElements().add(new ListElement("Thing3")); + + //generic object datatype + Map typeMap = new HashMap(); + typeMap.put("data", SimpleType.DYN); + + //convert to generic maps and lists and put as data map + Map containerData = new ObjectMapper().convertValue(container, new TypeReference<>() {}); + Map varMap = new HashMap(); + varMap.put("data", containerData); + + CelExecutor ce = new CelExecutor(); + String expression = String.format(EXP_ITERATION, "Thing2"); + + CelAbstractSyntaxTree ast = ce.compile(expression, typeMap); + + Boolean result = ce.run(ast, varMap); + + assertTrue(result); + + String badExpression = String.format(EXP_ITERATION, "Woooo"); + CelAbstractSyntaxTree badAst = ce.compile(badExpression, typeMap); + + Boolean badResult = ce.run(badAst, varMap); + + assertFalse(badResult); + } + +} diff --git a/src/test/java/com/yetanalytics/model/Container.java b/src/test/java/com/yetanalytics/model/Container.java new file mode 100644 index 0000000..52be626 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/Container.java @@ -0,0 +1,17 @@ +package com.yetanalytics.model; + +import java.util.List; + +public class Container { + + private List elements; + + public List getElements() { + return elements; + } + + public void setElements(List elements) { + this.elements = elements; + } + +} diff --git a/src/test/java/com/yetanalytics/model/ListElement.java b/src/test/java/com/yetanalytics/model/ListElement.java new file mode 100644 index 0000000..00078d1 --- /dev/null +++ b/src/test/java/com/yetanalytics/model/ListElement.java @@ -0,0 +1,19 @@ +package com.yetanalytics.model; + +public class ListElement { + + public ListElement(String val){ + this.value = val; + } + + private String value; + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/test/java/com/yetanalytics/util/ReadStatement.java b/src/test/java/com/yetanalytics/util/ReadStatement.java new file mode 100644 index 0000000..5cee8e9 --- /dev/null +++ b/src/test/java/com/yetanalytics/util/ReadStatement.java @@ -0,0 +1,28 @@ +package com.yetanalytics.util; + +import java.io.File; +import java.io.IOException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.yetanalytics.CelExecutor; +import com.yetanalytics.xapi.model.Statement; +import com.yetanalytics.xapi.util.Mapper; + +public class ReadStatement { + + private static final Logger log = LoggerFactory.getLogger(CelExecutor.class); + + public static Statement getJsonTestFile(String filename){ + String path = String.format("src/test/resources/statements/%s.json", filename); + File file = new File(path); + Statement stmt = null; + try { + stmt = Mapper.getMapper().readValue(file, Statement.class); + } catch (IOException e) { + log.error("failed to read statement at location: {}", path, e); + } + return stmt; + } +} diff --git a/src/test/resources/statements/basic.json b/src/test/resources/statements/basic.json new file mode 100644 index 0000000..96da417 --- /dev/null +++ b/src/test/resources/statements/basic.json @@ -0,0 +1,32 @@ +{ + "id": "6fbd600f-d87c-4c74-801a-2ec2e53231c8", + "actor": { + "name": "Cliff Casey", + "account": { + "homePage": "https://users.training.com", + "name": "23897525" + } + }, + "verb": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/verbs/set", + "display": { "en-us": "Set" } + }, + "object": { + "id": "https://www.yetanalytics.com/profiles/thing/1.0/concepts/activities/act1", + "definition": { + "name": { + "en-us": "Activity 1" + }, + "description": { + "en-us": "The First Activity" + } + } + }, + "authority": { + "name": "Yet Analytics Inc", + "mbox": "mailto:authority@yetanalytics.com" + }, + "version": "1.0.3", + "stored": "2023-10-27T09:03:21.722Z", + "timestamp": "2023-10-27T09:03:21.723Z" +}