diff --git a/src/main/java/eu/europa/ted/efx/EfxTranslator.java b/src/main/java/eu/europa/ted/efx/EfxTranslator.java index 454327f7..66cd55c6 100644 --- a/src/main/java/eu/europa/ted/efx/EfxTranslator.java +++ b/src/main/java/eu/europa/ted/efx/EfxTranslator.java @@ -17,10 +17,12 @@ import java.io.InputStream; import java.nio.file.Path; import java.util.Map; +import java.util.Set; import eu.europa.ted.efx.component.EfxTranslatorFactory; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; import eu.europa.ted.efx.interfaces.TranslatorOptions; +import eu.europa.ted.efx.model.dependencies.DependencyGraph; /** * Provided for convenience, this class exposes static methods that allow you to quickly instantiate @@ -270,4 +272,63 @@ public static Map translateRules(final TranslatorDependencyFacto } //#endregion Translate EFX rules -------------------------------------------- + + //#region Extract EFX dependencies ------------------------------------------- + + /** + * Instantiates an EFX compute dependency extractor and extracts all field and node identifiers + * referenced in the given expression. + * + * @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the + * dependencies of the extractor. + * @param sdkVersion The version of the eForms SDK that defines the EFX grammar used by the + * expression to be analysed. + * @param expression The EFX expression to analyse. + * @return An unmodifiable set of field and node identifiers referenced in the expression. + * @throws InstantiationException If the dependency extractor cannot be instantiated. + */ + public static Set extractComputeDependencies(final TranslatorDependencyFactory dependencyFactory, + final String sdkVersion, final String expression) throws InstantiationException { + return EfxTranslatorFactory.getEfxComputeDependencyExtractor(sdkVersion, dependencyFactory) + .extractDependencies(expression); + } + + /** + * Instantiates an EFX validation dependency extractor and extracts the dependency graph + * from the given EFX rules string. + * + * @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the + * dependencies of the extractor. + * @param sdkVersion The version of the eForms SDK. + * @param rules The EFX rules to analyse. + * @return A {@link DependencyGraph} with all dependencies and reverse dependencies. + * @throws InstantiationException If the dependency extractor cannot be instantiated. + */ + public static DependencyGraph extractValidationDependencies( + final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final String rules) throws InstantiationException { + return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory) + .extractDependencyGraph(rules); + } + + /** + * Instantiates an EFX validation dependency extractor and extracts the dependency graph + * from the given EFX rules file. + * + * @param dependencyFactory A {@link TranslatorDependencyFactory} to be used for instantiating the + * dependencies of the extractor. + * @param sdkVersion The version of the eForms SDK. + * @param pathname The path to the EFX rules file. + * @return A {@link DependencyGraph} with all dependencies and reverse dependencies. + * @throws IOException If the file cannot be read. + * @throws InstantiationException If the dependency extractor cannot be instantiated. + */ + public static DependencyGraph extractValidationDependencies( + final TranslatorDependencyFactory dependencyFactory, final String sdkVersion, + final Path pathname) throws IOException, InstantiationException { + return EfxTranslatorFactory.getEfxValidationDependencyExtractor(sdkVersion, dependencyFactory) + .extractDependencyGraph(pathname); + } + + //#endregion Extract EFX dependencies ---------------------------------------- } diff --git a/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java b/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java index 00f21ce1..949c1144 100644 --- a/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java +++ b/src/main/java/eu/europa/ted/efx/component/EfxTranslatorFactory.java @@ -10,6 +10,8 @@ import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.interfaces.TranslatorDependencyFactory; import eu.europa.ted.efx.interfaces.TranslatorOptions; +import eu.europa.ted.efx.interfaces.EfxComputeDependencyExtractor; +import eu.europa.ted.efx.interfaces.EfxValidationDependencyExtractor; import eu.europa.ted.efx.interfaces.ValidatorGenerator; public class EfxTranslatorFactory extends SdkComponentFactory { @@ -71,4 +73,36 @@ public static EfxRulesTranslator getEfxRulesTranslator(final String sdkVersion, SdkComponentType.EFX_RULES_TRANSLATOR, qualifier, EfxRulesTranslator.class, validatorGenerator, symbolResolver, scriptGenerator, factory.createErrorListener()); } + + public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion, + final TranslatorDependencyFactory factory) throws InstantiationException { + return getEfxComputeDependencyExtractor(sdkVersion, "", factory); + } + + public static EfxComputeDependencyExtractor getEfxComputeDependencyExtractor(final String sdkVersion, + final String qualifier, final TranslatorDependencyFactory factory) + throws InstantiationException { + + SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier); + + return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion, + SdkComponentType.EFX_COMPUTE_DEPENDENCY_EXTRACTOR, qualifier, EfxComputeDependencyExtractor.class, + symbolResolver, factory.createErrorListener()); + } + + public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion, + final TranslatorDependencyFactory factory) throws InstantiationException { + return getEfxValidationDependencyExtractor(sdkVersion, "", factory); + } + + public static EfxValidationDependencyExtractor getEfxValidationDependencyExtractor(final String sdkVersion, + final String qualifier, final TranslatorDependencyFactory factory) + throws InstantiationException { + + SymbolResolver symbolResolver = factory.createSymbolResolver(sdkVersion, qualifier); + + return EfxTranslatorFactory.INSTANCE.getComponentImpl(sdkVersion, + SdkComponentType.EFX_VALIDATION_DEPENDENCY_EXTRACTOR, qualifier, EfxValidationDependencyExtractor.class, + symbolResolver, factory.createErrorListener()); + } } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/EfxComputeDependencyExtractor.java b/src/main/java/eu/europa/ted/efx/interfaces/EfxComputeDependencyExtractor.java new file mode 100644 index 00000000..bdf5d72d --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/interfaces/EfxComputeDependencyExtractor.java @@ -0,0 +1,19 @@ +package eu.europa.ted.efx.interfaces; + +import java.util.Set; + +/** + * Defines the API of an EFX compute dependency extractor. + * + * Given an EFX single expression, extracts all field and node identifiers referenced in it. + */ +public interface EfxComputeDependencyExtractor { + + /** + * Extracts all field and node identifiers referenced in the given EFX expression. + * + * @param expression A string containing the EFX single expression to analyse. + * @return An unmodifiable set of field and node identifiers referenced in the expression. + */ + Set extractDependencies(final String expression); +} diff --git a/src/main/java/eu/europa/ted/efx/interfaces/EfxValidationDependencyExtractor.java b/src/main/java/eu/europa/ted/efx/interfaces/EfxValidationDependencyExtractor.java new file mode 100644 index 00000000..1ad92234 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/interfaces/EfxValidationDependencyExtractor.java @@ -0,0 +1,32 @@ +package eu.europa.ted.efx.interfaces; + +import java.io.IOException; +import java.nio.file.Path; + +import eu.europa.ted.efx.model.dependencies.DependencyGraph; + +/** + * Defines the API of an EFX validation dependency extractor. + * + * Given an EFX rules file, extracts all field and node dependencies for each validation rule + * and builds a dependency graph. + */ +public interface EfxValidationDependencyExtractor { + + /** + * Extracts a dependency graph from the given EFX rules string. + * + * @param rules A string containing EFX validation rules. + * @return A {@link DependencyGraph} mapping each rule's target to its dependencies. + */ + DependencyGraph extractDependencyGraph(final String rules); + + /** + * Extracts a dependency graph from an EFX rules file. + * + * @param pathname The path to the EFX rules file. + * @return A {@link DependencyGraph} mapping each rule's target to its dependencies. + * @throws IOException If the file cannot be read. + */ + DependencyGraph extractDependencyGraph(final Path pathname) throws IOException; +} diff --git a/src/main/java/eu/europa/ted/efx/model/dependencies/DependencyGraph.java b/src/main/java/eu/europa/ted/efx/model/dependencies/DependencyGraph.java new file mode 100644 index 00000000..cb72d285 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/dependencies/DependencyGraph.java @@ -0,0 +1,151 @@ +package eu.europa.ted.efx.model.dependencies; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * A dependency graph mapping targets (fields and nodes) to their dependencies and dependants. + * + * The graph is built incrementally by the dependency extractor: assert dependencies are added + * per-rule during the tree walk, and reverse dependencies (requiredBy) are computed at the end. + */ +public class DependencyGraph { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + private final Map fieldEntries = new LinkedHashMap<>(); + private final Map nodeEntries = new LinkedHashMap<>(); + + public TargetDependencies getOrCreateFieldEntry(final String fieldId) { + return this.fieldEntries.computeIfAbsent(fieldId, id -> new TargetDependencies(id, true)); + } + + public TargetDependencies getOrCreateNodeEntry(final String nodeId) { + return this.nodeEntries.computeIfAbsent(nodeId, id -> new TargetDependencies(id, false)); + } + + public List getFieldEntries() { + return new ArrayList<>(this.fieldEntries.values()); + } + + public List getNodeEntries() { + return new ArrayList<>(this.nodeEntries.values()); + } + + /** + * Computes the reverse dependencies (requiredBy) from the forward dependencies (dependsOn). + * Must be called after all forward dependencies have been added. + */ + public void computeRequiredBy() { + for (TargetDependencies target : new ArrayList<>(this.fieldEntries.values())) { + this.addRequiredByFromAssertDeps(target); + } + for (TargetDependencies target : new ArrayList<>(this.nodeEntries.values())) { + this.addRequiredByFromAssertDeps(target); + } + } + + private void addRequiredByFromAssertDeps(final TargetDependencies target) { + for (RuleDependency rule : target.getAssertDependencies()) { + for (String depFieldId : rule.getFields()) { + this.addRequiredByAssert(this.getOrCreateFieldEntry(depFieldId), target); + } + for (String depNodeId : rule.getNodes()) { + this.addRequiredByAssert(this.getOrCreateNodeEntry(depNodeId), target); + } + } + } + + private void addRequiredByAssert(final TargetDependencies dependency, + final TargetDependencies requirer) { + if (requirer.isField()) { + dependency.addRequiredByAssertField(requirer.getId()); + } else { + dependency.addRequiredByAssertNode(requirer.getId()); + } + } + + public String toJson() { + final ObjectNode root = MAPPER.createObjectNode(); + root.set("fields", this.serializeEntries(this.fieldEntries)); + root.set("nodes", this.serializeEntries(this.nodeEntries)); + return root.toPrettyString(); + } + + private ArrayNode serializeEntries(final Map entries) { + final ArrayNode array = MAPPER.createArrayNode(); + for (TargetDependencies entry : entries.values()) { + array.add(this.serializeEntry(entry)); + } + return array; + } + + private ObjectNode serializeEntry(final TargetDependencies entry) { + final ObjectNode node = MAPPER.createObjectNode(); + node.put("id", entry.getId()); + this.putIfNotEmpty("dependsOn", this.serializeDependsOn(entry), node); + this.putIfNotEmpty("requiredBy", this.serializeRequiredBy(entry), node); + return node; + } + + private ObjectNode serializeDependsOn(final TargetDependencies entry) { + final ObjectNode dependsOn = MAPPER.createObjectNode(); + this.putIfNotEmpty("compute", this.serializeIdentifierSets( + entry.getComputeFieldDeps(), entry.getComputeNodeDeps()), dependsOn); + + final ArrayNode assertArray = MAPPER.createArrayNode(); + for (RuleDependency rule : entry.getAssertDependencies()) { + final ObjectNode ruleNode = MAPPER.createObjectNode(); + ruleNode.put("ruleId", rule.getRuleId()); + this.putIfNotEmpty("fields", this.toStringArray(rule.getFields()), ruleNode); + this.putIfNotEmpty("nodes", this.toStringArray(rule.getNodes()), ruleNode); + this.putIfNotEmpty("codeLists", this.toStringArray(rule.getCodelists()), ruleNode); + assertArray.add(ruleNode); + } + this.putIfNotEmpty("assert", assertArray, dependsOn); + return dependsOn; + } + + private ObjectNode serializeRequiredBy(final TargetDependencies entry) { + final ObjectNode requiredBy = MAPPER.createObjectNode(); + this.putIfNotEmpty("compute", this.serializeIdentifierSets( + entry.getRequiredByComputeFields(), entry.getRequiredByComputeNodes()), requiredBy); + this.putIfNotEmpty("assert", this.serializeIdentifierSets( + entry.getRequiredByAssertFields(), entry.getRequiredByAssertNodes()), requiredBy); + return requiredBy; + } + + private ObjectNode serializeIdentifierSets(final Iterable fields, + final Iterable nodes) { + final ObjectNode obj = MAPPER.createObjectNode(); + this.putIfNotEmpty("fields", this.toStringArray(fields), obj); + this.putIfNotEmpty("nodes", this.toStringArray(nodes), obj); + return obj; + } + + private void putIfNotEmpty(final String name, final ObjectNode value, final ObjectNode parent) { + if (value.size() > 0) { + parent.set(name, value); + } + } + + private void putIfNotEmpty(final String name, final ArrayNode value, final ObjectNode parent) { + if (value.size() > 0) { + parent.set(name, value); + } + } + + private ArrayNode toStringArray(final Iterable values) { + final ArrayNode array = MAPPER.createArrayNode(); + for (String value : values) { + array.add(value); + } + return array; + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/dependencies/DependencySet.java b/src/main/java/eu/europa/ted/efx/model/dependencies/DependencySet.java new file mode 100644 index 00000000..7533af01 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/dependencies/DependencySet.java @@ -0,0 +1,66 @@ +package eu.europa.ted.efx.model.dependencies; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * A set of field and node identifiers collected during a parse tree walk. + * Used as a stack frame in the dependency extraction process. + */ +public class DependencySet { + + private final Set fieldIds = new LinkedHashSet<>(); + private final Set nodeIds = new LinkedHashSet<>(); + private final Set codelistNames = new LinkedHashSet<>(); + + public void addField(final String fieldId) { + this.fieldIds.add(fieldId); + } + + public void addNode(final String nodeId) { + this.nodeIds.add(nodeId); + } + + public void addCodelist(final String codelistName) { + this.codelistNames.add(codelistName); + } + + public void removeField(final String fieldId) { + this.fieldIds.remove(fieldId); + } + + public void removeNode(final String nodeId) { + this.nodeIds.remove(nodeId); + } + + public void addAll(final DependencySet other) { + this.fieldIds.addAll(other.fieldIds); + this.nodeIds.addAll(other.nodeIds); + this.codelistNames.addAll(other.codelistNames); + } + + public Set getFieldIds() { + return Collections.unmodifiableSet(this.fieldIds); + } + + public Set getNodeIds() { + return Collections.unmodifiableSet(this.nodeIds); + } + + public Set getCodelistNames() { + return Collections.unmodifiableSet(this.codelistNames); + } + + public boolean isEmpty() { + return this.fieldIds.isEmpty() && this.nodeIds.isEmpty() && this.codelistNames.isEmpty(); + } + + public Set allIds() { + final Set result = new LinkedHashSet<>(); + result.addAll(this.fieldIds); + result.addAll(this.nodeIds); + result.addAll(this.codelistNames); + return Collections.unmodifiableSet(result); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/dependencies/RuleDependency.java b/src/main/java/eu/europa/ted/efx/model/dependencies/RuleDependency.java new file mode 100644 index 00000000..8548ea27 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/dependencies/RuleDependency.java @@ -0,0 +1,40 @@ +package eu.europa.ted.efx.model.dependencies; + +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +/** + * The dependencies of a single validation rule. + */ +public class RuleDependency { + + private final String ruleId; + private final Set fields; + private final Set nodes; + private final Set codelists; + + public RuleDependency(final String ruleId, final Set fields, final Set nodes, + final Set codelists) { + this.ruleId = ruleId; + this.fields = Collections.unmodifiableSet(new LinkedHashSet<>(fields)); + this.nodes = Collections.unmodifiableSet(new LinkedHashSet<>(nodes)); + this.codelists = Collections.unmodifiableSet(new LinkedHashSet<>(codelists)); + } + + public String getRuleId() { + return this.ruleId; + } + + public Set getFields() { + return this.fields; + } + + public Set getNodes() { + return this.nodes; + } + + public Set getCodelists() { + return this.codelists; + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/dependencies/TargetDependencies.java b/src/main/java/eu/europa/ted/efx/model/dependencies/TargetDependencies.java new file mode 100644 index 00000000..f6052394 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/dependencies/TargetDependencies.java @@ -0,0 +1,88 @@ +package eu.europa.ted.efx.model.dependencies; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Dependency information for a single target field or node. + * + * Tracks both what this target depends on (for validation and computation) and what other targets + * require this one. + */ +public class TargetDependencies { + + private final String id; + private final boolean isField; + + private final Set computeFieldDeps = new LinkedHashSet<>(); + private final Set computeNodeDeps = new LinkedHashSet<>(); + private final List assertDeps = new ArrayList<>(); + + private final Set requiredByComputeFields = new LinkedHashSet<>(); + private final Set requiredByComputeNodes = new LinkedHashSet<>(); + private final Set requiredByAssertFields = new LinkedHashSet<>(); + private final Set requiredByAssertNodes = new LinkedHashSet<>(); + + public TargetDependencies(final String id, final boolean isField) { + this.id = id; + this.isField = isField; + } + + public String getId() { + return this.id; + } + + public boolean isField() { + return this.isField; + } + + public void addAssertDependency(final RuleDependency ruleDependency) { + this.assertDeps.add(ruleDependency); + } + + public List getAssertDependencies() { + return this.assertDeps; + } + + public Set getComputeFieldDeps() { + return this.computeFieldDeps; + } + + public Set getComputeNodeDeps() { + return this.computeNodeDeps; + } + + public void addRequiredByAssertField(final String fieldId) { + this.requiredByAssertFields.add(fieldId); + } + + public void addRequiredByAssertNode(final String nodeId) { + this.requiredByAssertNodes.add(nodeId); + } + + public void addRequiredByComputeField(final String fieldId) { + this.requiredByComputeFields.add(fieldId); + } + + public void addRequiredByComputeNode(final String nodeId) { + this.requiredByComputeNodes.add(nodeId); + } + + public Set getRequiredByComputeFields() { + return this.requiredByComputeFields; + } + + public Set getRequiredByComputeNodes() { + return this.requiredByComputeNodes; + } + + public Set getRequiredByAssertFields() { + return this.requiredByAssertFields; + } + + public Set getRequiredByAssertNodes() { + return this.requiredByAssertNodes; + } +} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractor.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractor.java new file mode 100644 index 00000000..b063fa33 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractor.java @@ -0,0 +1,143 @@ +package eu.europa.ted.efx.sdk2; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Set; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import org.antlr.v4.runtime.tree.TerminalNode; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.efx.exceptions.SymbolResolutionException; +import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.model.dependencies.DependencySet; +import eu.europa.ted.efx.sdk2.EfxParser.*; + +/** + * Extracts field and node identifiers referenced in an EFX single expression. + * + * This listener walks the parse tree and collects every field and node identifier it encounters + * into a stack of {@link DependencySet} frames. Identifiers used as aliases are resolved through + * the {@link SymbolResolver}. + * + * Subclasses push and pop frames at the appropriate boundaries to group dependencies by scope. + */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.EFX_COMPUTE_DEPENDENCY_EXTRACTOR) +public class EfxComputeDependencyExtractor extends EfxBaseListener + implements eu.europa.ted.efx.interfaces.EfxComputeDependencyExtractor { + + protected final SymbolResolver symbols; + protected final BaseErrorListener errorListener; + protected final Deque stack = new ArrayDeque<>(); + + public EfxComputeDependencyExtractor(final SymbolResolver symbolResolver, + final BaseErrorListener errorListener) { + this.symbols = symbolResolver; + this.errorListener = errorListener; + } + + @Override + public Set extractDependencies(final String expression) { + this.stack.clear(); + this.stack.push(new DependencySet()); + + final EfxLexer lexer = new EfxLexer(CharStreams.fromString(expression)); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + final EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); + + if (this.errorListener != null) { + lexer.removeErrorListeners(); + lexer.addErrorListener(this.errorListener); + parser.removeErrorListeners(); + parser.addErrorListener(this.errorListener); + } + + final ParseTree tree = parser.singleExpression(); + new ParseTreeWalker().walk(this, tree); + + return this.stack.pop().allIds(); + } + + // #region Listener methods -------------------------------------------------- + + @Override + public void enterSingleExpression(final SingleExpressionContext ctx) { + final TerminalNode fieldId = ctx.FieldId(); + if (fieldId != null) { + this.stack.peek().addField(fieldId.getText()); + return; + } + + final TerminalNode nodeId = ctx.NodeId(); + if (nodeId != null) { + this.stack.peek().addNode(nodeId.getText()); + return; + } + + final TerminalNode alias = ctx.Identifier(); + if (alias != null) { + this.resolveAlias(alias.getText()); + } + } + + @Override + public void enterSimpleFieldReference(final SimpleFieldReferenceContext ctx) { + final TerminalNode fieldId = ctx.FieldId(); + if (fieldId != null) { + this.stack.peek().addField(fieldId.getText()); + return; + } + + final TerminalNode alias = ctx.Identifier(); + if (alias != null) { + this.resolveAlias(alias.getText()); + } + } + + @Override + public void enterFieldMention(final FieldMentionContext ctx) { + final TerminalNode fieldId = ctx.FieldId(); + if (fieldId != null) { + this.stack.peek().addField(fieldId.getText()); + return; + } + + final TerminalNode alias = ctx.Identifier(); + if (alias != null) { + this.resolveAlias(alias.getText()); + } + } + + @Override + public void enterSimpleNodeReference(final SimpleNodeReferenceContext ctx) { + this.stack.peek().addNode(ctx.NodeId().getText()); + } + + @Override + public void enterCodelistReference(final CodelistReferenceContext ctx) { + this.stack.peek().addCodelist(ctx.codelistName.getText()); + } + + // #endregion Listener methods + + protected void resolveAlias(final String alias) { + final String fieldId = this.symbols.getFieldIdFromAlias(alias); + if (fieldId != null) { + this.stack.peek().addField(fieldId); + return; + } + + final String nodeId = this.symbols.getNodeIdFromAlias(alias); + if (nodeId != null) { + this.stack.peek().addNode(nodeId); + return; + } + + throw SymbolResolutionException.unknownSymbol(alias); + } +} diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractor.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractor.java new file mode 100644 index 00000000..9f009c56 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractor.java @@ -0,0 +1,190 @@ +package eu.europa.ted.efx.sdk2; + +import java.io.IOException; +import java.nio.file.Path; + +import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.CharStream; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.ParseTreeWalker; +import eu.europa.ted.efx.interfaces.IncludedFileResolver; +import eu.europa.ted.eforms.sdk.component.SdkComponent; +import eu.europa.ted.eforms.sdk.component.SdkComponentType; +import eu.europa.ted.efx.interfaces.SymbolResolver; +import eu.europa.ted.efx.model.dependencies.DependencyGraph; +import eu.europa.ted.efx.model.dependencies.DependencySet; +import eu.europa.ted.efx.model.dependencies.RuleDependency; +import eu.europa.ted.efx.model.dependencies.TargetDependencies; +import eu.europa.ted.efx.sdk2.EfxParser.*; + +/** + * Extracts field and node dependencies from an EFX rules file and builds a + * {@link DependencyGraph}. + * + * Uses the inherited dependency stack from {@link EfxComputeDependencyExtractor}: + *
    + *
  • {@code enterRuleSet} pushes a frame for ruleSet-level (WITH clause) dependencies.
  • + *
  • {@code enterSimpleRule} / {@code enterConditionalRule} / {@code enterFallbackRule} + * push a rule frame seeded with the ruleSet dependencies.
  • + *
  • Field/node references during the rule's expressions accumulate on the rule frame.
  • + *
  • {@code exitSimpleRule} / etc. pop the rule frame, extract target and rule ID from the + * context, and add the result to the graph.
  • + *
  • {@code exitRuleSet} pops the ruleSet frame.
  • + *
+ */ +@SdkComponent(versions = {"2"}, componentType = SdkComponentType.EFX_VALIDATION_DEPENDENCY_EXTRACTOR) +public class EfxValidationDependencyExtractor extends EfxComputeDependencyExtractor + implements eu.europa.ted.efx.interfaces.EfxValidationDependencyExtractor { + + private DependencyGraph graph; + + public EfxValidationDependencyExtractor(final SymbolResolver symbolResolver, + final BaseErrorListener errorListener) { + super(symbolResolver, errorListener); + } + + // #region Public API -------------------------------------------------------- + + @Override + public DependencyGraph extractDependencyGraph(final String rules) { + return this.extractFromCharStream(CharStreams.fromString(rules)); + } + + @Override + public DependencyGraph extractDependencyGraph(final Path pathname) throws IOException { + final Path baseDir = pathname.toAbsolutePath().getParent(); + final IncludedFileResolver resolver = new FileSystemIncludedFileResolver(baseDir); + final CharStream raw = CharStreams.fromPath(pathname); + final CharStream resolved = new IncludeProcessor(resolver).resolve(raw); + return this.extractFromCharStream(resolved); + } + + // #endregion Public API + + // #region Parsing ----------------------------------------------------------- + + private DependencyGraph extractFromCharStream(final CharStream input) { + this.graph = new DependencyGraph(); + this.stack.clear(); + this.stack.push(new DependencySet()); + + final EfxLexer lexer = new EfxLexer(input); + final CommonTokenStream tokens = new CommonTokenStream(lexer); + final EfxParser parser = new EfxParser(tokens); + parser.setErrorHandler(new EfxErrorStrategy()); + + if (this.errorListener != null) { + lexer.removeErrorListeners(); + lexer.addErrorListener(this.errorListener); + parser.removeErrorListeners(); + parser.addErrorListener(this.errorListener); + } + + final ParseTree tree = parser.rulesFile(); + new ParseTreeWalker().walk(this, tree); + + this.graph.computeRequiredBy(); + return this.graph; + } + + // #endregion Parsing + + // #region Scope lifecycle ---------------------------------------------------- + + @Override + public void enterValidationStage(final ValidationStageContext ctx) { + final DependencySet stageFrame = new DependencySet(); + stageFrame.addAll(this.stack.peek()); + this.stack.push(stageFrame); + } + + @Override + public void exitValidationStage(final ValidationStageContext ctx) { + this.stack.pop(); + } + + @Override + public void enterRuleSet(final RuleSetContext ctx) { + final DependencySet ruleSetFrame = new DependencySet(); + ruleSetFrame.addAll(this.stack.peek()); + this.stack.push(ruleSetFrame); + } + + @Override + public void exitRuleSet(final RuleSetContext ctx) { + this.stack.pop(); + } + + // #endregion Scope lifecycle + + // #region Rule lifecycle ---------------------------------------------------- + + @Override + public void enterSimpleRule(final SimpleRuleContext ctx) { + this.pushRuleFrame(); + } + + @Override + public void exitSimpleRule(final SimpleRuleContext ctx) { + this.commitRule(ctx.asClause(), ctx.forClause()); + } + + @Override + public void enterConditionalRule(final ConditionalRuleContext ctx) { + this.pushRuleFrame(); + } + + @Override + public void exitConditionalRule(final ConditionalRuleContext ctx) { + this.commitRule(ctx.asClause(), ctx.forClause()); + } + + @Override + public void enterFallbackRule(final FallbackRuleContext ctx) { + this.pushRuleFrame(); + } + + @Override + public void exitFallbackRule(final FallbackRuleContext ctx) { + this.commitRule(ctx.asClause(), ctx.forClause()); + } + + private void pushRuleFrame() { + final DependencySet ruleFrame = new DependencySet(); + ruleFrame.addAll(this.stack.peek()); + this.stack.push(ruleFrame); + } + + private void commitRule(final AsClauseContext asClause, final ForClauseContext forClause) { + final DependencySet ruleDeps = this.stack.pop(); + final String ruleId = asClause.ruleId().getText().replaceAll("^\"|\"$", ""); + + final String targetId; + final boolean targetIsField; + + if (forClause.simpleFieldReference() != null) { + targetId = forClause.simpleFieldReference().FieldId().getText(); + targetIsField = true; + } else { + targetId = forClause.simpleNodeReference().NodeId().getText(); + targetIsField = false; + } + + ruleDeps.removeField(targetId); + ruleDeps.removeNode(targetId); + + if (ruleDeps.isEmpty()) { + return; + } + + final TargetDependencies target = targetIsField + ? this.graph.getOrCreateFieldEntry(targetId) + : this.graph.getOrCreateNodeEntry(targetId); + + target.addAssertDependency(new RuleDependency(ruleId, ruleDeps.getFieldIds(), ruleDeps.getNodeIds(), ruleDeps.getCodelistNames())); + } + + // #endregion Rule lifecycle +} diff --git a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java index d9dcebd5..b477e9cf 100644 --- a/src/test/java/eu/europa/ted/efx/EfxTestsBase.java +++ b/src/test/java/eu/europa/ted/efx/EfxTestsBase.java @@ -87,6 +87,16 @@ protected String translateExpressionWithContext(final String context, final Stri return translateExpression(String.format("{%s} ${%s}", context, expression)); } + protected void testComputeExpressionWithContext(final String expectedTranslation, + final String context, final String expression) { + assertEquals(expectedTranslation, translateComputeExpressionWithContext(context, expression)); + } + + protected String translateComputeExpressionWithContext(final String context, + final String expression) { + return translateExpression(String.format("WITH %s COMPUTE %s", context, expression)); + } + protected String translateExpression(final String expression, final String... params) { try { String result = EfxTranslator.translateExpression(DependencyFactoryMock.INSTANCE, diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java new file mode 100644 index 00000000..89545dc6 --- /dev/null +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java @@ -0,0 +1,135 @@ +package eu.europa.ted.efx.sdk2; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import eu.europa.ted.efx.EfxTranslator; +import eu.europa.ted.efx.mock.DependencyFactoryMock; + +class EfxComputeDependencyExtractorTest { + + private static final String SDK_VERSION = "eforms-sdk-2.0"; + + private static Set extract(final String expression) { + try { + return EfxTranslator.extractComputeDependencies(DependencyFactoryMock.INSTANCE, SDK_VERSION, + expression); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } + } + + // #region: Context dependencies --------------------------------------------- + + @Test + void testFieldContext() { + Set deps = extract("{BT-00-Text} ${ALWAYS}"); + assertTrue(deps.contains("BT-00-Text")); + } + + @Test + void testNodeContext() { + Set deps = extract("{ND-Root} ${ALWAYS}"); + assertTrue(deps.contains("ND-Root")); + } + + @Test + void testFieldContext_ComputeSyntax() { + Set deps = extract("WITH BT-00-Text COMPUTE ALWAYS"); + assertTrue(deps.contains("BT-00-Text")); + } + + @Test + void testNodeContext_ComputeSyntax() { + Set deps = extract("WITH ND-Root COMPUTE ALWAYS"); + assertTrue(deps.contains("ND-Root")); + } + + // #endregion: Context dependencies + + // #region: Expression field references -------------------------------------- + + @Test + void testFieldReferenceInExpression() { + Set deps = extract("{ND-Root} ${BT-00-Text is present}"); + assertEquals(Set.of("ND-Root", "BT-00-Text"), deps); + } + + @Test + void testMultipleFieldReferences() { + Set deps = extract("{ND-Root} ${BT-00-Text == BT-00-Number}"); + assertEquals(Set.of("ND-Root", "BT-00-Text", "BT-00-Number"), deps); + } + + @Test + void testAbsoluteFieldReference() { + Set deps = extract("{ND-Root} ${/BT-00-Text is present}"); + assertEquals(Set.of("ND-Root", "BT-00-Text"), deps); + } + + @Test + void testFieldReferenceWithContextOverride() { + Set deps = extract("{ND-Root} ${ND-Root::BT-00-Text is present}"); + assertTrue(deps.contains("ND-Root")); + assertTrue(deps.contains("BT-00-Text")); + } + + // #endregion: Expression field references + + // #region: Node references -------------------------------------------------- + + @Test + void testNodeReferenceInExpression() { + Set deps = extract("{ND-Root} ${ND-Root::BT-00-Text is present}"); + assertTrue(deps.contains("ND-Root")); + assertTrue(deps.contains("BT-00-Text")); + } + + // #endregion: Node references + + // #region: No spurious dependencies ----------------------------------------- + + @Test + void testLiteralExpression_NoDependencies() { + Set deps = extract("{BT-00-Text} ${1 + 2}"); + assertEquals(Set.of("BT-00-Text"), deps); + } + + @Test + void testBooleanLiteral_NoDependencies() { + Set deps = extract("{BT-00-Text} ${ALWAYS and TRUE}"); + assertEquals(Set.of("BT-00-Text"), deps); + } + + // #endregion: No spurious dependencies + + // #region: COMPUTE syntax --------------------------------------------------- + + @Test + void testComputeSyntax_WithFieldReferences() { + Set deps = extract("WITH ND-Root COMPUTE BT-00-Text == BT-00-Number"); + assertEquals(Set.of("ND-Root", "BT-00-Text", "BT-00-Number"), deps); + } + + @Test + void testComputeSyntax_WithAbsoluteReference() { + Set deps = extract("WITH ND-Root COMPUTE /BT-00-Text is present"); + assertEquals(Set.of("ND-Root", "BT-00-Text"), deps); + } + + // #endregion: COMPUTE syntax + + // #region: Deduplication ---------------------------------------------------- + + @Test + void testDuplicateFieldReferences() { + Set deps = extract("{ND-Root} ${BT-00-Text == BT-00-Text}"); + assertEquals(Set.of("ND-Root", "BT-00-Text"), deps); + } + + // #endregion: Deduplication +} diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java index ea4b5b0f..ddb7633c 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -4581,4 +4581,74 @@ void testComputedProperty_privacyCode_Multilingual() { // #endregion: Privacy property cardinality // #endregion: Cardinality x Consumer Gaps + + // #region: EFX-2 COMPUTE syntax --------------------------------------------- + + @Test + void testCompute_BooleanExpression() { + testComputeExpressionWithContext("(true() or true()) and false()", "BT-00-Text", + "(ALWAYS or TRUE) and NEVER"); + } + + @Test + void testCompute_PresenceCondition() { + testComputeExpressionWithContext("PathNode/TextField", "ND-Root", "BT-00-Text is present"); + } + + @Test + void testCompute_NumericExpression() { + testComputeExpressionWithContext("1 + 2", "BT-00-Text", "1 + 2"); + } + + @Test + void testCompute_StringComparison() { + testComputeExpressionWithContext("'abc' = 'def'", "BT-00-Text", "'abc' == 'def'"); + } + + @Test + void testCompute_FieldReference() { + testComputeExpressionWithContext("PathNode/TextField/normalize-space(text())", "ND-Root", + "BT-00-Text"); + } + + @Test + void testCompute_WithNodeContext() { + testComputeExpressionWithContext("PathNode/TextField", "ND-Root", "BT-00-Text is present"); + } + + @Test + void testCompute_WithParameters() { + testExpressionTranslation("'hello' = 'world'", + "WITH ND-Root, text:$p1, text:$p2 COMPUTE $p1 == $p2", "'hello'", "'world'"); + } + + @Test + void testCompute_WithNumericParameters() { + testExpressionTranslation("1 = 2", + "WITH ND-Root, number:$p1, number:$p2 COMPUTE $p1 == $p2", "1", "2"); + } + + @Test + void testCompute_WithDateParameters() { + testExpressionTranslation("xs:date('2018-01-01Z') = xs:date('2020-01-01Z')", + "WITH ND-Root, date:$p1, date:$p2 COMPUTE $p1 == $p2", "2018-01-01Z", "2020-01-01Z"); + } + + @Test + void testCompute_WithSequenceParameter() { + testExpressionTranslation("count(('a','b','c'))", + "WITH ND-Root, text*:$items COMPUTE count($items)", "['a', 'b', 'c']"); + } + + @Test + void testCompute_CaseInsensitive() { + testComputeExpressionWithContext("(true() or true()) and false()", "BT-00-Text", + "(ALWAYS or TRUE) and NEVER"); + // Also test lowercase + assertEquals( + translateComputeExpressionWithContext("BT-00-Text", "(ALWAYS or TRUE) and NEVER"), + translateExpression("with BT-00-Text compute (ALWAYS or TRUE) and NEVER")); + } + + // #endregion: EFX-2 COMPUTE syntax } diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest.java new file mode 100644 index 00000000..31152a01 --- /dev/null +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest.java @@ -0,0 +1,133 @@ +package eu.europa.ted.efx.sdk2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import eu.europa.ted.efx.EfxTestsBase; +import eu.europa.ted.efx.exceptions.ThrowingErrorListener; +import eu.europa.ted.efx.mock.DependencyFactoryMock; +import eu.europa.ted.efx.model.dependencies.DependencyGraph; + +class EfxValidationDependencyExtractorTest extends EfxTestsBase { + + private static final String SDK_VERSION = "eforms-sdk-2.0"; + private EfxValidationDependencyExtractor extractor; + + @Override + protected String getSdkVersion() { + return SDK_VERSION; + } + + @BeforeEach + void setUp() { + this.extractor = new EfxValidationDependencyExtractor( + DependencyFactoryMock.INSTANCE.createSymbolResolver(SDK_VERSION, ""), + ThrowingErrorListener.INSTANCE); + } + + private String readResource(final String testMethodName, final String filename) + throws IOException { + String resourcePath = "/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/" + + testMethodName + "/" + filename; + try (InputStream stream = getClass().getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new IOException("Resource not found: " + resourcePath); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } + } + + private void assertDependencyGraph(final String testName) throws IOException { + String input = this.readResource(testName, "input.efx"); + DependencyGraph graph = this.extractor.extractDependencyGraph(input); + String actual = graph.toJson(); + String expected = this.readResource(testName, "expected.json"); + assertEquals(expected.stripTrailing(), actual.stripTrailing(), + "Dependency graph mismatch for " + testName); + } + + // #region: Basic rules ------------------------------------------------------ + + @Test + void testSimpleRule() throws IOException { + assertDependencyGraph("testSimpleRule"); + } + + @Test + void testMultipleRules_SameTarget() throws IOException { + assertDependencyGraph("testMultipleRules_SameTarget"); + } + + @Test + void testMultipleRules_DifferentTargets() throws IOException { + assertDependencyGraph("testMultipleRules_DifferentTargets"); + } + + @Test + void testConditionalRule() throws IOException { + assertDependencyGraph("testConditionalRule"); + } + + @Test + void testNodeTarget() throws IOException { + assertDependencyGraph("testNodeTarget"); + } + + // #endregion: Basic rules + + // #region: Context variations ----------------------------------------------- + + @Test + void testRootContextShortcut() throws IOException { + assertDependencyGraph("testRootContextShortcut"); + } + + @Test + void testContextOverride() throws IOException { + assertDependencyGraph("testContextOverride"); + } + + @Test + void testContextVariable() throws IOException { + assertDependencyGraph("testContextVariable"); + } + + // #endregion: Context variations + + // #region: Field reference variations --------------------------------------- + + @Test + void testAbsoluteFieldReference() throws IOException { + assertDependencyGraph("testAbsoluteFieldReference"); + } + + @Test + void testFieldWithPredicate() throws IOException { + assertDependencyGraph("testFieldWithPredicate"); + } + + // #endregion: Field reference variations + + // #region: Structure -------------------------------------------------------- + + @Test + void testMultipleStages() throws IOException { + assertDependencyGraph("testMultipleStages"); + } + + // #endregion: Structure + + // #region: Comprehensive ---------------------------------------------------- + + @Test + void testComprehensive() throws IOException { + assertDependencyGraph("testComprehensive"); + } + + // #endregion: Comprehensive +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/expected.json new file mode 100644 index 00000000..0bc46924 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/expected.json @@ -0,0 +1,34 @@ +{ + "fields" : [ { + "id" : "BT-01-SubNode-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-A01-001", + "fields" : [ "BT-00-Text", "BT-00-Number" ], + "nodes" : [ "ND-SubNode" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text" ] + } + } + }, { + "id" : "BT-00-Number", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-SubNode", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/input.efx new file mode 100644 index 00000000..308f3716 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testAbsoluteFieldReference/input.efx @@ -0,0 +1,8 @@ +// Test: Absolute field references with / + +---- STAGE test ---- + +WITH ND-SubNode + ASSERT /BT-00-Text is present and /BT-00-Number > 0 + AS ERROR R-A01-001 + FOR BT-01-SubNode-Text IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/expected.json new file mode 100644 index 00000000..d8745b12 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/expected.json @@ -0,0 +1,150 @@ +{ + "fields" : [ { + "id" : "BT-01-SubNode-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C01-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-SubNode" ] + }, { + "ruleId" : "R-C01-002", + "fields" : [ "BT-01-SubSubNode-Text" ], + "nodes" : [ "ND-SubNode" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + }, { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C01-003", + "fields" : [ "BT-00-Repeatable-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C01-004", + "fields" : [ "BT-00-Code", "BT-01-SubNode-Text" ], + "nodes" : [ "ND-Root", "ND-SubNode" ] + }, { + "ruleId" : "R-C01-005", + "fields" : [ "BT-00-Indicator" ], + "nodes" : [ "ND-Root" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text" ] + } + } + }, { + "id" : "BT-00-Text-In-Repeatable-Node", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C02-001", + "fields" : [ "BT-00-Date-In-Repeatable-Node" ], + "nodes" : [ "ND-RepeatableNode" ] + } ] + }, + "requiredBy" : { + "assert" : { + "nodes" : [ "ND-RepeatableNode" ] + } + } + }, { + "id" : "BT-00-EndDate", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C02-002", + "fields" : [ "BT-00-StartDate" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Code", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C02-003", + "nodes" : [ "ND-Root" ], + "codeLists" : [ "accessibility" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + }, { + "id" : "BT-01-SubSubNode-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text" ] + } + } + }, { + "id" : "BT-00-Repeatable-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + }, { + "id" : "BT-00-Indicator", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + }, { + "id" : "BT-00-Date-In-Repeatable-Node", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text-In-Repeatable-Node" ] + } + } + }, { + "id" : "BT-00-StartDate", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-EndDate" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-RepeatableNode", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-C01-006", + "fields" : [ "BT-00-Text-In-Repeatable-Node" ], + "nodes" : [ "ND-Root" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text-In-Repeatable-Node" ] + } + } + }, { + "id" : "ND-SubNode", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-01-SubNode-Text", "BT-00-Text" ] + } + } + }, { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number", "BT-00-Text", "BT-00-EndDate", "BT-00-Code" ], + "nodes" : [ "ND-RepeatableNode" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/input.efx new file mode 100644 index 00000000..6d72fd71 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testComprehensive/input.efx @@ -0,0 +1,58 @@ +// Comprehensive test: multiple stages, various contexts, predicates, cross-references + +---- STAGE validation ---- + +// Rule 1: SubNode context, references fields in different nodes +WITH ND-SubNode + ASSERT BT-01-SubNode-Text is present and BT-00-Text is present + AS ERROR R-C01-001 + FOR BT-01-SubNode-Text IN * + +// Rule 2: Same target as Rule 1, different dependencies +WITH ND-SubNode + ASSERT BT-01-SubSubNode-Text is present + AS ERROR R-C01-002 + FOR BT-01-SubNode-Text IN * + +// Rule 3: Absolute field reference with predicate +WITH ND-Root + ASSERT /BT-00-Repeatable-Text[BT-00-Repeatable-Text != ''] is present + AS ERROR R-C01-003 + FOR BT-00-Number IN * + +// Rule 4: Conditional rule with context override +WITH ND-Root + WHEN BT-00-Code is present + ASSERT ND-SubNode::BT-01-SubNode-Text is present + AS ERROR R-C01-004 + FOR BT-00-Text IN * + OTHERWISE + ASSERT BT-00-Indicator == TRUE + AS ERROR R-C01-005 + FOR BT-00-Text IN * + +// Rule 5: Node target +WITH ND-Root + ASSERT BT-00-Text-In-Repeatable-Node is present + AS ERROR R-C01-006 + FOR ND-RepeatableNode IN * + +---- STAGE cross-check ---- + +// Rule 6: Field from repeatable node context +WITH ND-RepeatableNode + ASSERT BT-00-Text-In-Repeatable-Node is present and BT-00-Date-In-Repeatable-Node is present + AS ERROR R-C02-001 + FOR BT-00-Text-In-Repeatable-Node IN * + +// Rule 7: References multiple types +WITH ND-Root + ASSERT BT-00-StartDate is present and BT-00-EndDate is present and BT-00-StartDate <= BT-00-EndDate + AS ERROR R-C02-002 + FOR BT-00-EndDate IN * + +// Rule 8: Codelist reference +WITH ND-Root + ASSERT BT-00-Code in [...accessibility] + AS ERROR R-C02-003 + FOR BT-00-Code IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/expected.json new file mode 100644 index 00000000..c5d13eee --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/expected.json @@ -0,0 +1,30 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T04-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + }, { + "ruleId" : "R-T04-002", + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/input.efx new file mode 100644 index 00000000..a102cf5d --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testConditionalRule/input.efx @@ -0,0 +1,13 @@ +// Test: Conditional rule with WHEN/OTHERWISE + +---- STAGE test ---- + +WITH ND-Root + WHEN BT-00-Text is present + ASSERT BT-00-Number > 0 + AS ERROR R-T04-001 + FOR BT-00-Number IN * + OTHERWISE + ASSERT ALWAYS + AS ERROR R-T04-002 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/expected.json new file mode 100644 index 00000000..c1b65a30 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/expected.json @@ -0,0 +1,34 @@ +{ + "fields" : [ { + "id" : "BT-00-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-O01-001", + "fields" : [ "BT-01-SubNode-Text" ], + "nodes" : [ "ND-Root", "ND-SubNode" ] + } ] + } + }, { + "id" : "BT-01-SubNode-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + }, { + "id" : "ND-SubNode", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/input.efx new file mode 100644 index 00000000..ff11f356 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextOverride/input.efx @@ -0,0 +1,8 @@ +// Test: Context override with ND-xxx::BT-xxx + +---- STAGE test ---- + +WITH ND-Root + ASSERT ND-SubNode::BT-01-SubNode-Text is present + AS ERROR R-O01-001 + FOR BT-00-Text IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/expected.json new file mode 100644 index 00000000..f33ef29a --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/expected.json @@ -0,0 +1,27 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-V01-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/input.efx new file mode 100644 index 00000000..5d173899 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testContextVariable/input.efx @@ -0,0 +1,8 @@ +// Test: WITH clause with a variable declaration referencing a field + +---- STAGE test ---- + +WITH text:$label = BT-00-Text, ND-Root + ASSERT $label != '' + AS ERROR R-V01-001 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/expected.json new file mode 100644 index 00000000..b231fa13 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/expected.json @@ -0,0 +1,27 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-P01-001", + "fields" : [ "BT-00-Repeatable-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Repeatable-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/input.efx new file mode 100644 index 00000000..296329b7 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testFieldWithPredicate/input.efx @@ -0,0 +1,8 @@ +// Test: Field references with predicates + +---- STAGE test ---- + +WITH ND-Root + ASSERT BT-00-Repeatable-Text[BT-00-Repeatable-Text != ''] is present + AS ERROR R-P01-001 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/expected.json new file mode 100644 index 00000000..27f58809 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/expected.json @@ -0,0 +1,39 @@ +{ + "fields" : [ { + "id" : "BT-00-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T03-001", + "fields" : [ "BT-00-Number" ], + "nodes" : [ "ND-Root" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + }, { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T03-002", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text", "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/input.efx new file mode 100644 index 00000000..b617ad97 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_DifferentTargets/input.efx @@ -0,0 +1,13 @@ +// Test: Two rules targeting different fields, cross-referencing each other + +---- STAGE test ---- + +WITH ND-Root + ASSERT BT-00-Number > 0 + AS ERROR R-T03-001 + FOR BT-00-Text IN * + +WITH ND-Root + ASSERT BT-00-Text is present + AS ERROR R-T03-002 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/expected.json new file mode 100644 index 00000000..b2663ef6 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/expected.json @@ -0,0 +1,38 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T02-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + }, { + "ruleId" : "R-T02-002", + "fields" : [ "BT-00-Repeatable-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + }, { + "id" : "BT-00-Repeatable-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/input.efx new file mode 100644 index 00000000..38b4ce68 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleRules_SameTarget/input.efx @@ -0,0 +1,13 @@ +// Test: Two rules targeting the same field + +---- STAGE test ---- + +WITH ND-Root + ASSERT BT-00-Text is present + AS ERROR R-T02-001 + FOR BT-00-Number IN * + +WITH ND-Root + ASSERT BT-00-Repeatable-Text is present + AS ERROR R-T02-002 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/expected.json new file mode 100644 index 00000000..b9d334eb --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/expected.json @@ -0,0 +1,57 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-S01-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + }, { + "ruleId" : "R-S02-001", + "fields" : [ "BT-01-SubNode-Text" ], + "nodes" : [ "ND-SubNode" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Text" ] + } + } + }, { + "id" : "BT-00-Text", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-S02-002", + "fields" : [ "BT-00-Number" ], + "nodes" : [ "ND-Root" ] + } ] + }, + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + }, { + "id" : "BT-01-SubNode-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number", "BT-00-Text" ] + } + } + }, { + "id" : "ND-SubNode", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/input.efx new file mode 100644 index 00000000..c61fd7d4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testMultipleStages/input.efx @@ -0,0 +1,20 @@ +// Test: Rules spread across multiple stages + +---- STAGE first ---- + +WITH ND-Root + ASSERT BT-00-Text is present + AS ERROR R-S01-001 + FOR BT-00-Number IN * + +---- STAGE second ---- + +WITH ND-SubNode + ASSERT BT-01-SubNode-Text is present + AS ERROR R-S02-001 + FOR BT-00-Number IN * + +WITH ND-Root + ASSERT BT-00-Number > 0 + AS ERROR R-S02-002 + FOR BT-00-Text IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/expected.json new file mode 100644 index 00000000..e30f1997 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/expected.json @@ -0,0 +1,27 @@ +{ + "fields" : [ { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "nodes" : [ "ND-SubNode" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-SubNode", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T05-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "nodes" : [ "ND-SubNode" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/input.efx new file mode 100644 index 00000000..1bc67be1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testNodeTarget/input.efx @@ -0,0 +1,8 @@ +// Test: Rule targeting a node instead of a field + +---- STAGE test ---- + +WITH ND-Root + ASSERT BT-00-Text is present + AS ERROR R-T05-001 + FOR ND-SubNode IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/expected.json new file mode 100644 index 00000000..a5d9ab1c --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/expected.json @@ -0,0 +1,19 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-R01-001", + "fields" : [ "BT-00-Text" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/input.efx new file mode 100644 index 00000000..cdcf1569 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testRootContextShortcut/input.efx @@ -0,0 +1,8 @@ +// Test: Root context shortcut / + +---- STAGE test ---- + +WITH / + ASSERT BT-00-Text is present + AS ERROR R-R01-001 + FOR BT-00-Number IN * diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/expected.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/expected.json new file mode 100644 index 00000000..0f0383b0 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/expected.json @@ -0,0 +1,27 @@ +{ + "fields" : [ { + "id" : "BT-00-Number", + "dependsOn" : { + "assert" : [ { + "ruleId" : "R-T01-001", + "fields" : [ "BT-00-Text" ], + "nodes" : [ "ND-Root" ] + } ] + } + }, { + "id" : "BT-00-Text", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ], + "nodes" : [ { + "id" : "ND-Root", + "requiredBy" : { + "assert" : { + "fields" : [ "BT-00-Number" ] + } + } + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/input.efx new file mode 100644 index 00000000..03132994 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxValidationDependencyExtractorTest/testSimpleRule/input.efx @@ -0,0 +1,8 @@ +// Test: Simple rule with field and node dependencies + +---- STAGE test ---- + +WITH ND-Root + ASSERT BT-00-Text is present + AS ERROR R-T01-001 + FOR BT-00-Number IN *