Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 8 additions & 20 deletions src/main/java/com/yetanalytics/hlaxapi/InjectionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@
import com.yetanalytics.hlaxapi.cache.CachedObject;
import com.yetanalytics.hlaxapi.cache.ObjectCache;
import com.yetanalytics.hlaxapi.cache.ValueResolution;
import com.yetanalytics.hlaxapi.config.model.Criterion;
import com.yetanalytics.hlaxapi.config.model.Expression;
import com.yetanalytics.hlaxapi.config.model.LogicalExpression;
import com.yetanalytics.hlaxapi.config.model.ExpressionWalker;
import com.yetanalytics.hlaxapi.config.model.ObjectLookup;
import com.yetanalytics.hlaxapi.config.model.Target;
import com.yetanalytics.hlaxapi.config.model.TriggerExpression;
Expand Down Expand Up @@ -287,24 +286,13 @@ private Expression resolveTriggerExpressions(Expression expression, InjectionCon
if (expression == null || context == null) {
return expression;
}
if (expression instanceof TriggerExpression triggerExpression) {
ValueResolution vr = handleTrigger(triggerExpression.target, context);
return new ValueExpression(vr.value());
}
if (expression instanceof Criterion criterion) {
return new Criterion(
resolveTriggerExpressions(criterion.left, context),
criterion.operator,
resolveTriggerExpressions(criterion.right, context));
}
if (expression instanceof LogicalExpression logicalExpression) {
return new LogicalExpression(
logicalExpression.operator,
logicalExpression.operands.stream()
.map(operand -> resolveTriggerExpressions(operand, context))
.toList());
}
return expression;
return ExpressionWalker.rewrite(expression, candidate -> {
if (candidate instanceof TriggerExpression triggerExpression) {
ValueResolution resolution = handleTrigger(triggerExpression.target, context);
return new ValueExpression(resolution.value());
}
return candidate;
});
}

// for test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yetanalytics.hlaxapi.config.model.Criterion;
import com.yetanalytics.hlaxapi.config.model.Expression;
import com.yetanalytics.hlaxapi.config.model.ExpressionWalker;
import com.yetanalytics.hlaxapi.config.model.LogicalExpression;
import com.yetanalytics.hlaxapi.config.model.LookupExpression;
import com.yetanalytics.hlaxapi.config.model.QueryExpression;
import com.yetanalytics.hlaxapi.config.model.StatementTrigger;
import com.yetanalytics.hlaxapi.config.model.Target;
import com.yetanalytics.hlaxapi.config.model.TriggerExpression;
import com.yetanalytics.hlaxapi.config.model.ValueExpression;
import com.yetanalytics.hlaxapi.injection.StatementInjectionParser;
import com.yetanalytics.hlaxapi.injection.StatementInjectionParser.InlineInjection;
Expand All @@ -27,6 +29,47 @@ public final class QueryReferenceCollector {

private static final ObjectMapper MAPPER = new ObjectMapper();

private record ReferenceState(
Map<String, Set<String>> references,
Map<String, String> lookupClasses,
String activeCacheClass) {
}

private static final ExpressionWalker.Visitor<ReferenceState> REFERENCE_VISITOR =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This inline extension syntax is bananas. Let's just make a Visitor abstract superclass and then a subclass for Reference?

new ExpressionWalker.Visitor<>() {
@Override
public void visit(Expression expression, ReferenceState state) {
switch (expression) {
case Criterion ignored -> {
}
case LogicalExpression ignored -> {
}
case LookupExpression lookup -> addTarget(
state.references,
state.lookupClasses.get(lookup.alias),
lookup.target);
case QueryExpression query -> addTarget(state.references, query.clazz, query.target);
case Target target -> addTarget(state.references, state.activeCacheClass, target);
case TriggerExpression ignored -> {
}
case ValueExpression ignored -> {
}
}
}

@Override
public ReferenceState stateForChild(
Expression parent,
ExpressionWalker.Child child,
ReferenceState state) {
String activeCacheClass = switch (child.role()) {
case QUERY_FILTER -> ((QueryExpression) parent).clazz;
case LEFT, RIGHT, OPERAND -> state.activeCacheClass;
};
return new ReferenceState(state.references, state.lookupClasses, activeCacheClass);
}
};

private QueryReferenceCollector() {
}

Expand All @@ -40,7 +83,7 @@ public static Map<String, Set<String>> collect(List<StatementTrigger> triggers)
continue;
}
Map<String, String> lookupClasses = collectLookupDefinitions(trigger, references);
collectTriggerExpression(trigger.criteria, references, lookupClasses);
collectExpressionReferences(trigger.criteria, references, lookupClasses, null);
if (trigger.statement == null) {
continue;
}
Expand All @@ -65,25 +108,20 @@ private static Map<String, String> collectLookupDefinitions(
return;
}
lookupClasses.put(alias, lookup.clazz);
collectCriteriaTargets(references, lookup.clazz, lookup.criteria);
collectExpressionReferences(lookup.criteria, references, lookupClasses, lookup.clazz);
});
return lookupClasses;
}

private static void collectTriggerExpression(
private static void collectExpressionReferences(
Expression expression,
Map<String, Set<String>> references,
Map<String, String> lookupClasses) {
if (expression instanceof QueryExpression query) {
collectQueryReference(query.clazz, query.target, query.criteria, references);
} else if (expression instanceof LookupExpression lookup) {
addTarget(references, lookupClasses.get(lookup.alias), lookup.target);
} else if (expression instanceof Criterion criterion) {
collectTriggerExpression(criterion.left, references, lookupClasses);
collectTriggerExpression(criterion.right, references, lookupClasses);
} else if (expression instanceof LogicalExpression logical) {
logical.operands.forEach(operand -> collectTriggerExpression(operand, references, lookupClasses));
}
Map<String, String> lookupClasses,
String activeCacheClass) {
ExpressionWalker.walk(
expression,
new ReferenceState(references, lookupClasses, activeCacheClass),
REFERENCE_VISITOR);
}

private static void collectFromNode(
Expand Down Expand Up @@ -144,7 +182,7 @@ private static void collectQueryReference(
}

addTarget(references, className, target);
collectCriteriaTargets(references, className, criteria);
collectExpressionReferences(criteria, references, Map.of(), className);
}

private static void collectLookup(
Expand All @@ -159,22 +197,6 @@ private static void collectLookup(
addTarget(references, className, lookup.target());
}

private static void collectCriteriaTargets(
Map<String, Set<String>> references,
String className,
Expression expression) {
if (expression instanceof Target target) {
addTarget(references, className, target);
} else if (expression instanceof Criterion criterion) {
collectCriteriaTargets(references, className, criterion.left);
collectCriteriaTargets(references, className, criterion.right);
} else if (expression instanceof LogicalExpression logicalExpression) {
logicalExpression.operands.forEach(operand -> collectCriteriaTargets(references, className, operand));
} else if (expression instanceof ValueExpression) {
return;
}
}

private static void addTarget(Map<String, Set<String>> references, String className, Target target) {
if (className == null || className.isBlank()) {
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.yetanalytics.hlaxapi.config.model.Criterion;
import com.yetanalytics.hlaxapi.config.model.Expression;
import com.yetanalytics.hlaxapi.config.model.ExpressionWalker;
import com.yetanalytics.hlaxapi.config.model.LogicalExpression;
import com.yetanalytics.hlaxapi.config.model.LookupExpression;
import com.yetanalytics.hlaxapi.config.model.ObjectLookup;
Expand All @@ -19,65 +20,99 @@ private enum Context {
CACHE_FILTER
}

private record ValidationState(
Context context,
Map<String, ObjectLookup> lookupDefinitions,
String location) {
}

private static final ExpressionWalker.Visitor<ValidationState> VALIDATION_VISITOR =
new ExpressionWalker.Visitor<>() {
@Override
public void visit(Expression expression, ValidationState state) {
switch (expression) {
case Criterion ignored -> {
}
case LogicalExpression ignored -> {
}
case LookupExpression lookup -> validateLookup(lookup, state);
case QueryExpression query -> validateQuery(query, state);
case Target target -> validateTarget(target, state);
case TriggerExpression ignored -> {
}
case ValueExpression ignored -> {
}
}
}

@Override
public ValidationState stateForChild(
Expression parent,
ExpressionWalker.Child child,
ValidationState state) {
Context childContext = child.role() == ExpressionWalker.ChildRole.QUERY_FILTER
? Context.CACHE_FILTER
: state.context;
String childLocation = switch (child.role()) {
case LEFT -> state.location + ".left";
case RIGHT -> state.location + ".right";
case OPERAND -> state.location + "[" + child.index() + "]";
case QUERY_FILTER -> state.location + ".queryFilter";
};
return new ValidationState(childContext, state.lookupDefinitions, childLocation);
}
};

private CriteriaExpressionValidator() {
}

public static void validateTrigger(Expression criteria, Map<String, ObjectLookup> lookupDefinitions) {
Map<String, ObjectLookup> definitions = lookupDefinitions == null ? Map.of() : lookupDefinitions;
visit(criteria, Context.TRIGGER, definitions, "criteria");
ExpressionWalker.walk(
criteria,
new ValidationState(Context.TRIGGER, definitions, "criteria"),
VALIDATION_VISITOR);
}

public static void validateCacheFilter(Expression criteria) {
visit(criteria, Context.CACHE_FILTER, Map.of(), "criteria");
ExpressionWalker.walk(
criteria,
new ValidationState(Context.CACHE_FILTER, Map.of(), "criteria"),
VALIDATION_VISITOR);
}

private static void visit(
Expression expression,
Context context,
Map<String, ObjectLookup> definitions,
String location) {
if (expression == null || expression instanceof ValueExpression) {
return;
}
if (expression instanceof Criterion criterion) {
visit(criterion.left, context, definitions, location + ".left");
visit(criterion.right, context, definitions, location + ".right");
return;
}
if (expression instanceof LogicalExpression logical) {
for (int index = 0; index < logical.operands.size(); index++) {
visit(logical.operands.get(index), context, definitions, location + "[" + index + "]");
}
return;
}
if (expression instanceof TriggerExpression) {
return;
private static void validateTarget(Target target, ValidationState state) {
if (state.context == Context.TRIGGER) {
throw new IllegalArgumentException(
state.location + " contains bare target " + target.parts
+ "; use [\"trigger\", [...]] for incoming event values");
}
if (context == Context.CACHE_FILTER && expression instanceof Target) {
return;
}

private static void validateQuery(QueryExpression query, ValidationState state) {
if (state.context != Context.TRIGGER) {
throw unsupported(query, state);
}
if (context == Context.TRIGGER && expression instanceof QueryExpression query) {
visit(query.criteria, Context.CACHE_FILTER, definitions, location + ".queryFilter");
return;
}

private static void validateLookup(LookupExpression lookup, ValidationState state) {
if (state.context != Context.TRIGGER) {
throw unsupported(lookup, state);
}
if (context == Context.TRIGGER && expression instanceof LookupExpression lookup) {
ObjectLookup definition = definitions.get(lookup.alias);
if (definition == null) {
throw new IllegalArgumentException(
location + " references unknown lookup alias '" + lookup.alias + "'");
}
if (definition.clazz == null || definition.clazz.isBlank()) {
throw new IllegalArgumentException(
location + " references lookup alias '" + lookup.alias + "' without a class");
}
return;

ObjectLookup definition = state.lookupDefinitions.get(lookup.alias);
if (definition == null) {
throw new IllegalArgumentException(
state.location + " references unknown lookup alias '" + lookup.alias + "'");
}
if (context == Context.TRIGGER && expression instanceof Target target) {
if (definition.clazz == null || definition.clazz.isBlank()) {
throw new IllegalArgumentException(
location + " contains bare target " + target.parts
+ "; use [\"trigger\", [...]] for incoming event values");
state.location + " references lookup alias '" + lookup.alias + "' without a class");
}
throw new IllegalArgumentException(
location + " contains unsupported " + expression.getClass().getSimpleName());
}

private static IllegalArgumentException unsupported(Expression expression, ValidationState state) {
return new IllegalArgumentException(
state.location + " contains unsupported " + expression.getClass().getSimpleName());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* Binary comparison node (left op right). Left and right are Expressions.
*/
public class Criterion implements Expression {
public final class Criterion implements Expression {
public final Expression left; // Target or nested Expression
public final ComparisonOperator operator;
public final Expression right; // ValueExpression, Target, or nested Expression
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
package com.yetanalytics.hlaxapi.config.model;

public interface Expression {
public sealed interface Expression
permits Criterion,
LogicalExpression,
LookupExpression,
QueryExpression,
Target,
TriggerExpression,
ValueExpression {
}
Loading
Loading