diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java index 1a89197f..98643614 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronAssert.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -13,21 +13,121 @@ */ package eu.europa.ted.eforms.sdk.schematron; +import java.util.List; + import eu.europa.ted.efx.model.Context; +import eu.europa.ted.efx.model.rules.RuleNature; +import eu.europa.ted.efx.model.rules.RuleSeverity; import eu.europa.ted.efx.model.rules.ValidationRule; +import eu.europa.ted.efx.model.variables.DynamicVariable; /** * Represents a Schematron <assert> element. * Fires when the test expression evaluates to false. + * For rules referencing dynamic variables, the test is guarded so that API errors + * do not cause the main rule to fire — a companion {@link NoApiError} assert handles that. */ public class SchematronAssert extends SchematronTest { - public SchematronAssert(ValidationRule rule, Context ruleContext) { + private final List dynamicVariables; + + public SchematronAssert(final ValidationRule rule, final Context ruleContext) { super(rule, ruleContext); + this.dynamicVariables = rule.findReferencedDynamicVariables(); + } + + @Override + public RuleNature getRuleNature() { + if (!this.dynamicVariables.isEmpty()) { + return RuleNature.DYNAMIC; + } + return super.getRuleNature(); } @Override public String getElementName() { return "assert"; } + + @Override + public String getTest() { + String baseTest = super.getTest(); + if (this.getRuleNature() != RuleNature.DYNAMIC) { + return baseTest; + } + // An assert fires when the test is false. Prepending "($varName = -1) or" makes the + // test true when any dynamic variable errored, preventing the main assert from firing. + // Companion NoApiError asserts handle API errors separately. + StringBuilder sb = new StringBuilder(); + for (var dynamicVar : this.dynamicVariables) { + sb.append("($").append(dynamicVar.name).append(" = -1) or "); + } + for (var variable : this.rule.getAutoGeneratedVariables()) { + sb.append("($").append(variable.name).append(" = -1) or "); + } + if (this.rule.getCondition() != null) { + // WHEN clause: combineWithOrParenthesized already wrapped each operand in parens. + sb.append(baseTest); + } else { + // No WHEN clause: the raw expression needs wrapping to isolate it from the guards. + sb.append("(").append(baseTest).append(")"); + } + return sb.toString(); + } + + /** + * A companion Schematron <assert> that checks a dynamic variable did not return an error (-1). + * Generated for each dynamic variable (declared or auto-generated) referenced by a rule. + */ + public static class NoApiError extends SchematronAssert { + + private final DynamicVariable dynamicVariable; + + public NoApiError(ValidationRule rule, DynamicVariable dynamicVariable, Context ruleContext) { + super(rule, ruleContext); + this.dynamicVariable = dynamicVariable; + } + + @Override + public RuleNature getRuleNature() { + return RuleNature.DYNAMIC; + } + + @Override + public SchematronLet getLetElement() { + if (this.dynamicVariable instanceof DynamicVariable.AutoGenerated) { + return new SchematronLet(this.dynamicVariable); + } + return null; + } + + @Override + public String getId() { + return this.rule.getId() + "-api-error-" + this.dynamicVariable.identity(); + } + + @Override + public String getRole() { + return this.dynamicVariable.errorSeverity().toString().toUpperCase(); + } + + @Override + public String getTest() { + return "not($" + this.dynamicVariable.name + " = -1)"; + } + + @Override + public String getMessage() { + if (this.dynamicVariable.errorLabel() != null) { + return this.dynamicVariable.errorLabel(); + } + return this.dynamicVariable.errorSeverity() == RuleSeverity.WARNING + ? "rule|text|api-warning" : "rule|text|api-error"; + } + + @Override + public SchematronDiagnostic getDiagnostic() { + return null; + } + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java index 749d0e6a..83a17f62 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronDiagnostic.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java index c1736813..42e4956d 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -32,7 +33,8 @@ import eu.europa.ted.eforms.sdk.component.SdkComponent; import eu.europa.ted.eforms.sdk.component.SdkComponentType; import eu.europa.ted.efx.interfaces.ValidatorGenerator; -import eu.europa.ted.efx.model.rules.CompleteValidation; +import eu.europa.ted.efx.model.rules.ValidationPlan; +import eu.europa.ted.efx.model.rules.RuleNature; import eu.europa.ted.efx.model.rules.ValidationStage; import eu.europa.ted.efx.model.variables.Variable; import freemarker.template.Configuration; @@ -69,31 +71,59 @@ public SchematronGenerator() { // #region ValidatorMarkupGenerator Implementation @Override - public Map generateOutput(CompleteValidation completeValidation) { - logger.debug("Generating Schematron output from {} stages", completeValidation.getStages().size()); + public Map generateOutput(ValidationPlan validationPlan) { + logger.debug("Generating Schematron output from {} stages", validationPlan.getStages().size()); // Create local state for this generation run SchematronSchema schema = new SchematronSchema("eForms schematron rules"); List patterns = new ArrayList<>(); Map diagnosticsMap = new LinkedHashMap<>(); + // Add endpoint params to schema + for (Map.Entry endpoint : validationPlan.getEndpoints().entrySet()) { + String url = endpoint.getValue() != null ? endpoint.getValue() : ""; + SchematronParam param = new SchematronParam("apiUrl-" + endpoint.getKey(), "'" + url + "'"); + schema.addParam(param); + logger.debug("Added endpoint param: {} = {}", param.getName(), param.getValue()); + } + // Add global variables to schema - for (Variable variable : completeValidation.getGlobalVariables()) { - String xpathValue = variable.initializationExpression.getScript(); - SchematronLet globalVar = new SchematronLet(variable.name, xpathValue); - schema.addGlobalVariable(globalVar); - logger.debug("Added global variable: {} = {}", variable.name, xpathValue); + for (Variable variable : validationPlan.getVariables()) { + SchematronLet letElement = new SchematronLet(variable); + schema.addLetElement(letElement); + logger.debug("Added global variable: {} = {}", variable.name, variable.initializationExpression.getScript()); } // Transform intermediate model (ValidationStage) to Schematron model (SchematronPattern) - transformStagesToPatterns(completeValidation.getStages(), patterns, diagnosticsMap); + for (ValidationStage stage : validationPlan.getStages()) { + if (stage.containsUniversalRules()) { + SchematronPattern sharedPattern = new SchematronPattern(stage); + if (sharedPattern.hasRules()) { + patterns.add(sharedPattern); + diagnosticsMap.putAll(sharedPattern.getDiagnostics()); + logger.debug("Created shared pattern {} for stage {}", + sharedPattern.getId(), stage.getName()); + } + } + for (String noticeSubtype : stage.getNoticeSubtypes()) { + SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype); + if (pattern.hasRules()) { + patterns.add(pattern); + diagnosticsMap.putAll(pattern.getDiagnostics()); + logger.debug("Created pattern {} for stage {} / notice subtype {}", + pattern.getId(), stage.getName(), noticeSubtype); + } + } + } // Add collected diagnostics to schema - addDiagnosticsToSchema(diagnosticsMap, schema); + for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) { + schema.addDiagnostic(diagnostic); + } // Generate all output files try { - return generateOutputFiles(completeValidation.getNoticeSubtypes(), patterns, schema); + return this.generateOutputFiles(validationPlan.getNoticeSubtypes(), patterns, schema); } catch (IOException e) { throw new RuntimeException("Failed to generate Schematron output", e); } @@ -135,11 +165,13 @@ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig Map model = new HashMap<>(); model.put("id", pattern.getId()); - model.put("variables", pattern.getVariables()); + List tags = config.ruleNatures().stream() + .map(Enum::name).collect(Collectors.toList()); + List letElements = pattern.getLetElements().stream() + .filter(v -> tags.contains(v.getTag())).collect(Collectors.toList()); + model.put("letElements", letElements); model.put("rules", pattern.getRules()); - model.put("tags", config.ruleNatures().stream() - .map(Enum::name) - .collect(Collectors.toList())); + model.put("tags", tags); template.process(model, writer); return writer.toString(); @@ -147,49 +179,6 @@ public String generatePattern(SchematronPattern pattern, SchematronOutputConfig // #endregion Freemarker Template Methods - // #region Transformation Methods - - /** - * Transforms validation stages into Schematron patterns. - * For each stage, first creates a shared pattern for rules that apply to all subtypes, - * then creates subtype-specific patterns for the remaining rules. - */ - private void transformStagesToPatterns(List stages, - List patterns, Map diagnosticsMap) { - for (ValidationStage stage : stages) { - // Create shared pattern for rules that apply to all subtypes - if (stage.containsUniversalRules()) { - SchematronPattern sharedPattern = new SchematronPattern(stage); - if (sharedPattern.hasRules()) { - patterns.add(sharedPattern); - diagnosticsMap.putAll(sharedPattern.getDiagnostics()); - logger.debug("Created shared pattern {} for stage {}", - sharedPattern.getId(), stage.getName()); - } - } - - // Create subtype-specific patterns for remaining rules - for (String noticeSubtype : stage.getNoticeSubtypes()) { - SchematronPattern pattern = new SchematronPattern(stage, noticeSubtype); - if (pattern.hasRules()) { - patterns.add(pattern); - diagnosticsMap.putAll(pattern.getDiagnostics()); - logger.debug("Created pattern {} for stage {} / notice subtype {}", - pattern.getId(), stage.getName(), noticeSubtype); - } - } - } - } - - private void addDiagnosticsToSchema(Map diagnosticsMap, - SchematronSchema schema) { - for (SchematronDiagnostic diagnostic : diagnosticsMap.values()) { - schema.addDiagnostic(diagnostic); - } - } - - // #endregion Transformation Methods - // #region Output Generation Methods /** @@ -219,11 +208,11 @@ private Map generateOutputFiles(List noticeSubtypeIds, try { for (SchematronOutputConfig config : configs) { - generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata); + this.generateOutputForConfig(config, noticeSubtypeIds, patterns, baseSchema, outputFiles, schematronsMetadata); } // Generate schematrons.json with entries from all configurations - String schematronsJson = generateSchematronsJson(schematronsMetadata); + String schematronsJson = this.generateSchematronsJson(schematronsMetadata); outputFiles.put("schematrons.json", schematronsJson); logger.debug("Generated {} Schematron files", outputFiles.size()); @@ -251,8 +240,18 @@ private void generateOutputForConfig( // Create a fresh schema for this configuration SchematronSchema schema = new SchematronSchema(baseSchema.getTitle()); - for (SchematronLet globalVar : baseSchema.getGlobalVariables()) { - schema.addGlobalVariable(globalVar); + // API endpoint params are only relevant for configurations that include dynamic rules + if (config.ruleNatures().contains(RuleNature.DYNAMIC)) { + for (SchematronParam param : baseSchema.getParams()) { + schema.addParam(param); + } + } + Set configTags = config.ruleNatures().stream() + .map(Enum::name).collect(Collectors.toSet()); + for (SchematronLet letElement : baseSchema.getLetElements()) { + if (configTags.contains(letElement.getTag())) { + schema.addLetElement(letElement); + } } for (SchematronDiagnostic diagnostic : baseSchema.getDiagnostics()) { schema.addDiagnostic(diagnostic); @@ -307,9 +306,9 @@ private void generateOutputForConfig( } // Generate complete-validation.sch for this configuration - String completeValidation = generateCompleteValidation(schema); + String completeValidationContent = generateCompleteValidation(schema); String completeFilename = folderPrefix + "complete-validation.sch"; - outputFiles.put(completeFilename, completeValidation); + outputFiles.put(completeFilename, completeValidationContent); // Add complete-validation to metadata at the beginning of this config's entries Map completeMetadata = new LinkedHashMap<>(); diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronLet.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronLet.java index ac426b27..40b4c293 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronLet.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronLet.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -13,16 +13,32 @@ */ package eu.europa.ted.eforms.sdk.schematron; +import eu.europa.ted.efx.model.rules.RuleNature; +import eu.europa.ted.efx.model.variables.DynamicVariable; +import eu.europa.ted.efx.model.variables.Variable; + /** * Represents a Schematron <let> element for variable declarations. */ public class SchematronLet { private final String name; private final String value; + private final RuleNature nature; + + public SchematronLet(final Variable variable) { + this(variable.name, variable.initializationExpression.getScript(), + variable instanceof DynamicVariable || variable.hasDynamicDependencies() + ? RuleNature.DYNAMIC : RuleNature.STATIC); + } public SchematronLet(String name, String value) { + this(name, value, RuleNature.STATIC); + } + + public SchematronLet(String name, String value, RuleNature nature) { this.name = name; this.value = value; + this.nature = nature; } /** Used by pattern.ftl and complete-validation.ftl */ @@ -34,4 +50,9 @@ public String getName() { public String getValue() { return this.value; } + + /** Used by pattern.ftl — returns the tag for filtering (derived from nature) */ + public String getTag() { + return this.nature.name(); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronOutputConfig.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronOutputConfig.java index d868ddab..13fd25ae 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronOutputConfig.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronOutputConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronParam.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronParam.java new file mode 100644 index 00000000..891cb618 --- /dev/null +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronParam.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.eforms.sdk.schematron; + +/** + * Represents a Schematron <param> element for API endpoint parameters. + */ +public class SchematronParam { + private final String name; + private final String value; + + public SchematronParam(String name, String value) { + this.name = name; + this.value = value; + } + + /** Used by complete-validation.ftl */ + public String getName() { + return this.name; + } + + /** Used by complete-validation.ftl */ + public String getValue() { + return this.value; + } +} diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPattern.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPattern.java index 9941b080..07333e82 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPattern.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPattern.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -14,15 +14,16 @@ package eu.europa.ted.eforms.sdk.schematron; import java.util.ArrayList; +import java.util.stream.Collectors; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import eu.europa.ted.efx.model.rules.RuleNature; -import eu.europa.ted.efx.model.rules.RuleSet; +import eu.europa.ted.efx.model.rules.ValidationRule; import eu.europa.ted.efx.model.rules.ValidationStage; -import eu.europa.ted.efx.model.variables.Variable; /** * Represents a Schematron <pattern> element. @@ -32,7 +33,7 @@ public class SchematronPattern { private final String stage; private final String noticeSubtype; - private final List variables; + private final List letElements; private final List rules; /** @@ -40,60 +41,37 @@ public class SchematronPattern { * * @param validationStage The validation stage */ - public SchematronPattern(ValidationStage validationStage) { - this.stage = validationStage.getName(); - this.noticeSubtype = null; - this.variables = collectVariables(validationStage); - this.rules = createUniversalRules(validationStage); + public SchematronPattern(final ValidationStage validationStage) { + this(validationStage, null); } /** * Creates a pattern for a specific stage and notice subtype combination. - * Only includes subtype-specific rules; rules that apply to all subtypes are excluded. + * When noticeSubtype is null, creates a shared pattern for rules that apply to all subtypes. * * @param validationStage The validation stage - * @param noticeSubtype The notice subtype to filter by + * @param noticeSubtype The notice subtype to filter by, or null for universal rules */ - public SchematronPattern(ValidationStage validationStage, String noticeSubtype) { + public SchematronPattern(final ValidationStage validationStage, final String noticeSubtype) { this.stage = validationStage.getName(); this.noticeSubtype = noticeSubtype; - this.variables = collectVariables(validationStage); - this.rules = createSubtypeSpecificRules(validationStage, noticeSubtype); - } - - private static List collectVariables(ValidationStage stage) { - List vars = new ArrayList<>(); - for (Variable var : stage.getVariables()) { - vars.add(new SchematronLet(var.name, var.initializationExpression.getScript())); - } - for (RuleSet ruleSet : stage.getRuleSets()) { - for (Variable var : ruleSet.getStageVariables()) { - vars.add(new SchematronLet(var.name, var.initializationExpression.getScript())); - } - } - return vars; - } - private static List createUniversalRules(ValidationStage stage) { - List rules = new ArrayList<>(); - for (RuleSet ruleSet : stage.getRuleSets()) { - SchematronRule rule = SchematronRule.createUniversalRule(ruleSet); - if (rule.hasTests()) { - rules.add(rule); - } - } - return rules; - } - - private static List createSubtypeSpecificRules(ValidationStage stage, String noticeSubtype) { - List rules = new ArrayList<>(); - for (RuleSet ruleSet : stage.getRuleSets()) { - SchematronRule rule = SchematronRule.createSubtypeSpecificRule(ruleSet, noticeSubtype); - if (rule.hasTests()) { - rules.add(rule); - } - } - return rules; + Predicate applicableRules = noticeSubtype == null + ? rule -> rule.isUniversal() && rule.isForPostValidation() + : rule -> !rule.isUniversal() && rule.isForPostValidation() + && rule.isForNoticeSubtype(noticeSubtype); + + this.rules = validationStage.getRuleSets().stream() + .map(ruleSet -> new SchematronRule(ruleSet, applicableRules)) + .filter(SchematronRule::hasTests) + .collect(Collectors.toCollection(ArrayList::new)); + + this.letElements = validationStage.getVariables().stream() + .map(SchematronLet::new) + .collect(Collectors.toCollection(ArrayList::new)); + this.letElements.addAll(this.rules.stream() + .flatMap(rule -> rule.getPromotedLetElements().stream()) + .collect(Collectors.toList())); } /** Used by pattern.ftl */ @@ -118,8 +96,8 @@ public boolean isShared() { } /** Used by pattern.ftl */ - public List getVariables() { - return this.variables; + public List getLetElements() { + return this.letElements; } /** Used by pattern.ftl */ @@ -138,7 +116,7 @@ public boolean hasRules() { * @param ruleNatures The set of rule natures to include * @return true if any rule has tests of the included natures */ - public boolean hasRulesFor(Set ruleNatures) { + public boolean hasRulesFor(final Set ruleNatures) { return this.rules.stream().anyMatch(r -> r.hasTestsFor(ruleNatures)); } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPhase.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPhase.java index bdead8d6..71f69ff3 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPhase.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronPhase.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronReport.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronReport.java index e3454ecf..b279b4c6 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronReport.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronReport.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -13,21 +13,59 @@ */ package eu.europa.ted.eforms.sdk.schematron; +import java.util.List; + import eu.europa.ted.efx.model.Context; +import eu.europa.ted.efx.model.rules.RuleNature; import eu.europa.ted.efx.model.rules.ValidationRule; +import eu.europa.ted.efx.model.variables.DynamicVariable; /** * Represents a Schematron <report> element. * Fires when the test expression evaluates to true. + * For rules referencing dynamic variables, the test is guarded so that errors + * do not cause the main rule to fire — a companion NoApiError assert handles that. */ public class SchematronReport extends SchematronTest { - public SchematronReport(ValidationRule rule, Context ruleContext) { + private final List dynamicVariables; + + public SchematronReport(final ValidationRule rule, final Context ruleContext) { super(rule, ruleContext); + this.dynamicVariables = rule.findReferencedDynamicVariables(); + } + + @Override + public RuleNature getRuleNature() { + if (!this.dynamicVariables.isEmpty()) { + return RuleNature.DYNAMIC; + } + return super.getRuleNature(); } @Override public String getElementName() { return "report"; } + + @Override + public String getTest() { + String baseTest = super.getTest(); + if (this.getRuleNature() != RuleNature.DYNAMIC) { + return baseTest; + } + // A report fires when the test is true. Prepending "not($var = -1) and" makes the + // test false when any dynamic variable errored, preventing the main report from firing. + // Companion NoApiError asserts handle errors separately. + StringBuilder sb = new StringBuilder(); + for (var variable : this.dynamicVariables) { + sb.append("not($").append(variable.name).append(" = -1) and "); + } + for (var variable : this.rule.getAutoGeneratedVariables()) { + sb.append("not($").append(variable.name).append(" = -1) and "); + } + // Always wrap baseTest: the "and" guards bind tighter than any "or" in the expression. + sb.append("(").append(baseTest).append(")"); + return sb.toString(); + } } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronRule.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronRule.java index e3b9aa52..36d44d43 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronRule.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -19,88 +19,73 @@ import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.rules.RuleNature; -import eu.europa.ted.efx.model.rules.RuleScope; import eu.europa.ted.efx.model.rules.ReportRule; import eu.europa.ted.efx.model.rules.RuleSet; import eu.europa.ted.efx.model.rules.ValidationRule; -import eu.europa.ted.efx.model.variables.Variable; +import eu.europa.ted.efx.model.variables.DynamicVariable; /** * Represents a Schematron <rule> element. */ public class SchematronRule { - private final List variables; + private final List letElements; + private final List promotedLetElements; private final List tests; private final Context context; - /** - * Creates a SchematronRule for rules that apply to all notice subtypes (shared pattern). - */ - public static SchematronRule createUniversalRule(RuleSet ruleSet) { - return new SchematronRule(ruleSet, rule -> isUniversal(rule) && isForPostValidation(rule)); - } + SchematronRule(final RuleSet ruleSet, final Predicate applicableRules) { + this.context = ruleSet.getContext(); - /** - * Creates a SchematronRule for rules specific to a single notice subtype. - * Excludes rules that apply to all subtypes (those go into the shared pattern). - */ - public static SchematronRule createSubtypeSpecificRule(RuleSet ruleSet, String noticeSubtype) { - return new SchematronRule(ruleSet, - rule -> !isUniversal(rule) && isForPostValidation(rule) && appliesToNoticeSubtype(rule, noticeSubtype)); - } + this.tests = this.createTests(ruleSet, applicableRules); - private SchematronRule(RuleSet ruleSet, Predicate filter) { - this.context = ruleSet.getContext(); + this.letElements = ruleSet.getLocalVariables().stream() + .map(SchematronLet::new) + .collect(Collectors.toCollection(ArrayList::new)); - List variables = new ArrayList<>(); - List tests = new ArrayList<>(); + // WITH-before-context variables promoted to pattern scope (see TEDEFO-4986) + this.promotedLetElements = ruleSet.getPromotedVariables().stream() + .map(SchematronLet::new) + .collect(Collectors.toList()); + } - for (Variable var : ruleSet.getLocalVariables()) { - variables.add(new SchematronLet(var.name, var.initializationExpression.getScript())); - } + private List createTests(final RuleSet ruleSet, + final Predicate applicableRules) { - for (ValidationRule validationRule : ruleSet) { - if (filter.test(validationRule)) { - if (validationRule instanceof ReportRule) { - tests.add(new SchematronReport(validationRule, this.context)); - } else { - tests.add(new SchematronAssert(validationRule, this.context)); - } - } - } + List tests = ruleSet.stream() + .filter(applicableRules) + .flatMap(rule -> this.createTests(rule).stream()) + .collect(Collectors.toCollection(ArrayList::new)); ValidationRule fallback = ruleSet.getFallbackRule(); - if (fallback != null && filter.test(fallback)) { - if (fallback instanceof ReportRule) { - tests.add(new SchematronReport(fallback, this.context)); - } else { - tests.add(new SchematronAssert(fallback, this.context)); - } + if (fallback != null && applicableRules.test(fallback)) { + tests.addAll(this.createTests(fallback)); } - - this.variables = variables; - this.tests = tests; + return tests; } - private static boolean isUniversal(ValidationRule rule) { - return rule.getNoticeSubtypeRange() != null - && rule.getNoticeSubtypeRange().isUniversal(); - } + private List createTests(final ValidationRule rule) { - private static boolean appliesToNoticeSubtype(ValidationRule rule, String noticeSubtype) { - return rule.getNoticeSubtypeRange() != null - && rule.getNoticeSubtypeRange().asList().contains(noticeSubtype); - } + // Error asserts for all dynamic variables (declared and auto-generated). + List tests = new ArrayList<>(); + for (DynamicVariable var : rule.findReferencedDynamicVariables()) { + tests.add(new SchematronAssert.NoApiError(rule, var, this.context)); + } + for (var variable : rule.getAutoGeneratedVariables()) { + tests.add(new SchematronAssert.NoApiError(rule, variable, this.context)); + } - private static boolean isForPostValidation(ValidationRule rule) { - return rule.getScope() != RuleScope.PRE; - } + // Main assert/report — guarded by the error asserts above. + if (rule instanceof ReportRule) { + tests.add(new SchematronReport(rule, this.context)); + } else { + tests.add(new SchematronAssert(rule, this.context)); + } - private static boolean isForPreValidation(ValidationRule rule) { - return rule.getScope() != RuleScope.POST; + return tests; } /** Used by pattern.ftl */ @@ -109,8 +94,13 @@ public String getContext() { } /** Used by pattern.ftl */ - public List getVariables() { - return this.variables; + public List getLetElements() { + return this.letElements; + } + + /** Returns let elements promoted to pattern scope (WITH-before-context variables). */ + public List getPromotedLetElements() { + return this.promotedLetElements; } /** Used by pattern.ftl */ @@ -129,7 +119,7 @@ public boolean hasTests() { * @param ruleNatures The set of rule natures to include * @return true if any test's nature is in the included set */ - public boolean hasTestsFor(Set ruleNatures) { + public boolean hasTestsFor(final Set ruleNatures) { return this.tests.stream().anyMatch(t -> ruleNatures.contains(t.getRuleNature())); } @@ -140,7 +130,7 @@ public boolean hasTestsFor(Set ruleNatures) { * @param tags The list of tags to include * @return true if any test's tag is in the list */ - public boolean hasTestsForTags(List tags) { + public boolean hasTestsFor(final List tags) { return this.tests.stream().anyMatch(t -> tags.contains(t.getTag())); } diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronSchema.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronSchema.java index fa33d472..00b2ed1a 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronSchema.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronSchema.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -22,7 +22,8 @@ */ public class SchematronSchema { private final String title; - private final List globalVariables = new ArrayList<>(); + private final List params = new ArrayList<>(); + private final List letElements = new ArrayList<>(); private final List diagnostics = new ArrayList<>(); private final List phases = new ArrayList<>(); private final List includes = new ArrayList<>(); @@ -37,8 +38,13 @@ public String getTitle() { } /** Used by complete-validation.ftl */ - public List getGlobalVariables() { - return this.globalVariables; + public List getParams() { + return this.params; + } + + /** Used by complete-validation.ftl */ + public List getLetElements() { + return this.letElements; } /** Used by complete-validation.ftl */ @@ -56,8 +62,12 @@ public List getIncludes() { return this.includes; } - public void addGlobalVariable(SchematronLet variable) { - this.globalVariables.add(variable); + public void addParam(SchematronParam param) { + this.params.add(param); + } + + public void addLetElement(SchematronLet letElement) { + this.letElements.add(letElement); } public void addDiagnostic(SchematronDiagnostic diagnostic) { diff --git a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronTest.java b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronTest.java index ee6d97d8..2b6c18e9 100644 --- a/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronTest.java +++ b/src/main/java/eu/europa/ted/eforms/sdk/schematron/SchematronTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -32,6 +32,11 @@ protected SchematronTest(ValidationRule rule, Context ruleContext) { : new SchematronDiagnostic(rule.getSubject(), ruleContext); } + /** Used by pattern.ftl — let element to render right before this test, or null. */ + public SchematronLet getLetElement() { + return null; + } + /** Used by pattern.ftl */ public String getId() { return this.rule.getId(); @@ -66,7 +71,7 @@ public RuleNature getRuleNature() { /** Used by pattern.ftl - returns the tag for filtering (derived from rule nature) */ public String getTag() { - return this.rule.getNature().name(); + return this.getRuleNature().name(); } /** Used by pattern.ftl - returns the flag value from SCOPE clause, or null if not set */ diff --git a/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java b/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java index 5f791ce4..e5b39084 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/EfxCompilationException.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 European Union + * Copyright 2026 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java index 3f1e7d8d..2d762cbc 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIdentifierException.java @@ -24,20 +24,17 @@ public class InvalidIdentifierException extends EfxCompilationException { public enum ErrorCode { UNDECLARED_IDENTIFIER, IDENTIFIER_ALREADY_DECLARED, - NOT_A_CONTEXT_VARIABLE + NOT_A_CONTEXT_VARIABLE, + UNDECLARED_ENDPOINT } private static final String UNDECLARED_IDENTIFIER = "Identifier '%s' is not declared."; private static final String IDENTIFIER_ALREADY_DECLARED = "Identifier '%s' is already declared in this scope."; private static final String NOT_A_CONTEXT_VARIABLE = "Variable '%s' is not a context variable."; + private static final String UNDECLARED_ENDPOINT = "Dynamic function '%s' references undeclared API endpoint '%s'."; private final ErrorCode errorCode; - private InvalidIdentifierException(ErrorCode errorCode, String template, Object... args) { - super(template, args); - this.errorCode = errorCode; - } - private InvalidIdentifierException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { super(ctx, template, args); this.errorCode = errorCode; @@ -47,15 +44,19 @@ public ErrorCode getErrorCode() { return this.errorCode; } - public static InvalidIdentifierException undeclaredIdentifier(String identifierName) { - return new InvalidIdentifierException(ErrorCode.UNDECLARED_IDENTIFIER, UNDECLARED_IDENTIFIER, identifierName); + public static InvalidIdentifierException undeclaredIdentifier(ParserRuleContext ctx, String identifierName) { + return new InvalidIdentifierException(ErrorCode.UNDECLARED_IDENTIFIER, ctx, UNDECLARED_IDENTIFIER, identifierName); } - public static InvalidIdentifierException alreadyDeclared(String identifierName) { - return new InvalidIdentifierException(ErrorCode.IDENTIFIER_ALREADY_DECLARED, IDENTIFIER_ALREADY_DECLARED, identifierName); + public static InvalidIdentifierException alreadyDeclared(ParserRuleContext ctx, String identifierName) { + return new InvalidIdentifierException(ErrorCode.IDENTIFIER_ALREADY_DECLARED, ctx, IDENTIFIER_ALREADY_DECLARED, identifierName); } public static InvalidIdentifierException notAContextVariable(ParserRuleContext ctx, String variableName) { return new InvalidIdentifierException(ErrorCode.NOT_A_CONTEXT_VARIABLE, ctx, NOT_A_CONTEXT_VARIABLE, variableName); } + + public static InvalidIdentifierException undeclaredEndpoint(ParserRuleContext ctx, String functionName, String endpointName) { + return new InvalidIdentifierException(ErrorCode.UNDECLARED_ENDPOINT, ctx, UNDECLARED_ENDPOINT, functionName, endpointName); + } } diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java index e2c26ce7..2e60f79a 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidIndentationException.java @@ -38,11 +38,6 @@ public enum ErrorCode { private final ErrorCode errorCode; - private InvalidIndentationException(ErrorCode errorCode, String template, Object... args) { - super(template, args); - this.errorCode = errorCode; - } - private InvalidIndentationException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { super(ctx, template, args); this.errorCode = errorCode; diff --git a/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java b/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java index 16e2303c..9da3fd13 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/InvalidUsageException.java @@ -30,7 +30,11 @@ public enum ErrorCode { FIELD_NOT_WITHHOLDABLE, TEMPLATE_ONLY_FUNCTION, UNSUPPORTED_REGEX_CONSTRUCT, - CIRCULAR_INCLUDE + CIRCULAR_INCLUDE, + EMPTY_RULES_FILE, + NOT_A_DYNAMIC_FUNCTION, + DYNAMIC_FUNCTION_OUTSIDE_RULE, + INVALID_ENDPOINT_NAME } private static final String SHORTHAND_REQUIRES_CODE_OR_INDICATOR = "Indirect label reference shorthand #{%1$s}, requires a field of type 'code' or 'indicator'. Field %1$s is of type %2$s."; @@ -41,6 +45,10 @@ public enum ErrorCode { private static final String TEMPLATE_ONLY_FUNCTION = "Function '%s' can only be used in templates, not in expressions or validation rules."; private static final String UNSUPPORTED_REGEX_CONSTRUCT = "Invalid regex pattern %s at position %d: %s"; private static final String CIRCULAR_INCLUDE = "Circular #include detected: '%s'."; + private static final String EMPTY_RULES_FILE = "Rules file must contain at least one validation stage."; + private static final String NOT_A_DYNAMIC_FUNCTION = "Function '%s' is not declared as a dynamic function. Only functions declared with CALL API can be used in a dynamic variable initializer."; + private static final String DYNAMIC_FUNCTION_OUTSIDE_RULE = "Dynamic function '%s' can only be called inline within a rule expression (ASSERT/REPORT). Use 'LET dynamic : $var = ?%s(...)' to declare a dynamic variable at this scope."; + private static final String INVALID_ENDPOINT_NAME = "Endpoint name '%s' is not valid. Endpoint names must start with a letter and contain only letters, digits, hyphens, and underscores."; private final ErrorCode errorCode; @@ -89,4 +97,20 @@ public static InvalidUsageException unsupportedRegexConstruct(String pattern, in public static InvalidUsageException circularInclude(String path) { return new InvalidUsageException(ErrorCode.CIRCULAR_INCLUDE, CIRCULAR_INCLUDE, path); } + + public static InvalidUsageException emptyRulesFile() { + return new InvalidUsageException(ErrorCode.EMPTY_RULES_FILE, EMPTY_RULES_FILE); + } + + public static InvalidUsageException notADynamicFunction(ParserRuleContext ctx, String functionName) { + return new InvalidUsageException(ErrorCode.NOT_A_DYNAMIC_FUNCTION, ctx, NOT_A_DYNAMIC_FUNCTION, functionName); + } + + public static InvalidUsageException dynamicFunctionOutsideRule(ParserRuleContext ctx, String functionName) { + return new InvalidUsageException(ErrorCode.DYNAMIC_FUNCTION_OUTSIDE_RULE, ctx, DYNAMIC_FUNCTION_OUTSIDE_RULE, functionName, functionName); + } + + public static InvalidUsageException invalidEndpointName(ParserRuleContext ctx, String endpointName) { + return new InvalidUsageException(ErrorCode.INVALID_ENDPOINT_NAME, ctx, INVALID_ENDPOINT_NAME, endpointName); + } } diff --git a/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java b/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java index c4220420..67c8f13f 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/TranslatorConfigurationException.java @@ -32,7 +32,8 @@ public enum ErrorCode { UNHANDLED_PREDICATE_CONTEXT, INCLUDE_RESOLVER_NOT_CONFIGURED, UNRESOLVED_INCLUDE_DIRECTIVE, - UNHANDLED_OPERATOR + UNHANDLED_OPERATOR, + RULES_STACK_ERROR } private static final String TYPE_NOT_REGISTERED = @@ -94,6 +95,10 @@ public enum ErrorCode { "If the grammar was updated to allow new operators, " + "add a handler for this operator."; + private static final String RULES_STACK_ERROR = + "Expected %s on rules stack, but found %s. " + + "This indicates a bug in the rules translator."; + private final ErrorCode errorCode; private TranslatorConfigurationException(ErrorCode errorCode, String template, Object... args) { @@ -156,4 +161,14 @@ public static TranslatorConfigurationException unresolvedIncludeDirective(String public static TranslatorConfigurationException unhandledOperator(String operator, String handlerName) { return new TranslatorConfigurationException(ErrorCode.UNHANDLED_OPERATOR, UNHANDLED_OPERATOR, operator, handlerName); } + + public static TranslatorConfigurationException rulesStackError(Class expectedType, Class actualType) { + return new TranslatorConfigurationException(ErrorCode.RULES_STACK_ERROR, RULES_STACK_ERROR, + expectedType.getSimpleName(), actualType.getSimpleName()); + } + + public static TranslatorConfigurationException rulesStackEmpty(Class expectedType) { + return new TranslatorConfigurationException(ErrorCode.RULES_STACK_ERROR, RULES_STACK_ERROR, + expectedType.getSimpleName(), "empty stack"); + } } diff --git a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java index b9b8069b..1054ce4b 100644 --- a/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java +++ b/src/main/java/eu/europa/ted/efx/exceptions/TypeMismatchException.java @@ -48,11 +48,6 @@ public enum ErrorCode { private final ErrorCode errorCode; - private TypeMismatchException(ErrorCode errorCode, String template, Object... args) { - super(template, args); - this.errorCode = errorCode; - } - private TypeMismatchException(ErrorCode errorCode, ParserRuleContext ctx, String template, Object... args) { super(ctx, template, args); this.errorCode = errorCode; @@ -62,18 +57,18 @@ public ErrorCode getErrorCode() { return this.errorCode; } - public static TypeMismatchException cannotConvert(Class expectedType, + public static TypeMismatchException cannotConvert(ParserRuleContext ctx, + Class expectedType, Class actualType) { if (TypedExpression.class.isAssignableFrom(actualType) && TypedExpression.class.isAssignableFrom(expectedType)) { var actual = actualType.asSubclass(TypedExpression.class); var expected = expectedType.asSubclass(TypedExpression.class); - - return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, CANNOT_CONVERT, + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, ctx, CANNOT_CONVERT, TypedExpression.getEfxDataType(expected).getSimpleName(), TypedExpression.getEfxDataType(actual).getSimpleName()); } - return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, CANNOT_CONVERT, + return new TypeMismatchException(ErrorCode.CANNOT_CONVERT, ctx, CANNOT_CONVERT, expectedType.getSimpleName(), actualType.getSimpleName()); } @@ -82,8 +77,8 @@ public static TypeMismatchException cannotCompare(ParserRuleContext ctx, Express left.getClass().getSimpleName(), right.getClass().getSimpleName()); } - public static TypeMismatchException incompatibleOperands(String operator, Expression left, Expression right) { - return new TypeMismatchException(ErrorCode.INCOMPATIBLE_OPERANDS, INCOMPATIBLE_OPERANDS, + public static TypeMismatchException incompatibleOperands(ParserRuleContext ctx, String operator, Expression left, Expression right) { + return new TypeMismatchException(ErrorCode.INCOMPATIBLE_OPERANDS, ctx, INCOMPATIBLE_OPERANDS, operator, left.getClass().getSimpleName(), right.getClass().getSimpleName()); } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/EfxRulesTranslator.java b/src/main/java/eu/europa/ted/efx/interfaces/EfxRulesTranslator.java index cd6e1004..93461727 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/EfxRulesTranslator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/EfxRulesTranslator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java index c9d7cfe7..0ea19391 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ScriptGenerator.java @@ -306,25 +306,23 @@ public T composeForExpression( public IteratorListExpression composeIteratorList(List iterators); /** - * When we need data from an external source, we need some script that gets that data. Getting the - * data is a two-step process: a) we need to access the data source, b) we need to get the actual - * data from the data source. This method should return the target language script that connects - * to the data source and permits us to subsequently get the data by using a PathExpression. - * - * @param externalReference The PathExpression that points to the external data source. - * @return a PathExpression with the target language script that retrieves the external data source. + * @deprecated Cross-notice references have been removed from EFX-2. + * This method is only used by EFX-1 and will be removed in a future version. */ - public PathExpression composeExternalReference(final StringExpression externalReference); + @Deprecated(forRemoval = true) + default PathExpression composeExternalReference(final StringExpression externalReference) { + return null; + } /** - * See {@link #composeExternalReference} for more details. - * - * @param externalReference The PathExpression that points to the external data source. - * @param fieldReference The PathExpression that points to the field in the external data source. - * @return a PathExpression with the target language script that retrieves the external data. + * @deprecated Cross-notice references have been removed from EFX-2. + * This method is only used by EFX-1 and will be removed in a future version. */ - public PathExpression composeFieldInExternalReference(final PathExpression externalReference, - final PathExpression fieldReference); + @Deprecated(forRemoval = true) + default PathExpression composeFieldInExternalReference(final PathExpression externalReference, + final PathExpression fieldReference) { + return null; + } /** @@ -1182,5 +1180,24 @@ public T composeIndexer(SequenceExpression list, public T composeFunctionInvocation(String functionName, List parameters, Class type); + /** + * Composes the raw dynamic function call expression. Dynamic functions are external + * functions that delegate to a REST API at validation runtime. + * The function returns a tri-state integer (1 = true, 0 = false, -1 = error). + * The returned expression is used as the value of a Schematron <let> variable. + * + * @param endpointName The name of the API endpoint to call. + * @param apiName The name of the dynamic function. + * @param arguments The evaluated arguments to pass to the function. + * @return A numeric expression representing the raw dynamic function call (without error handling). + */ + default NumericExpression composeDynamicFunction(String endpointName, String apiName, + List arguments) { + throw new UnsupportedOperationException( + "This translator does not support dynamic rules. " + + "Override composeDynamicFunction() in " + this.getClass().getSimpleName() + + " to enable dynamic function calling."); + } + // #endregion Function Invocation ----------------------------------------- } diff --git a/src/main/java/eu/europa/ted/efx/interfaces/ValidatorGenerator.java b/src/main/java/eu/europa/ted/efx/interfaces/ValidatorGenerator.java index c170faa0..21db5e0c 100644 --- a/src/main/java/eu/europa/ted/efx/interfaces/ValidatorGenerator.java +++ b/src/main/java/eu/europa/ted/efx/interfaces/ValidatorGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -15,7 +15,7 @@ import java.util.Map; -import eu.europa.ted.efx.model.rules.CompleteValidation; +import eu.europa.ted.efx.model.rules.ValidationPlan; /** * Interface for generating validation output from the intermediate validation model. @@ -33,9 +33,9 @@ public interface ValidatorGenerator { /** * Generates validation output files from the intermediate model. * - * @param completeValidation The complete validation model containing stages, + * @param validationPlan The complete validation model containing stages, * global variables, and notice subtypes. * @return A map of filename to file content for all generated validation files. */ - Map generateOutput(CompleteValidation completeValidation); + Map generateOutput(ValidationPlan validationPlan); } diff --git a/src/main/java/eu/europa/ted/efx/model/CallStack.java b/src/main/java/eu/europa/ted/efx/model/CallStack.java index d72d2f04..e84248c9 100644 --- a/src/main/java/eu/europa/ted/efx/model/CallStack.java +++ b/src/main/java/eu/europa/ted/efx/model/CallStack.java @@ -13,6 +13,8 @@ */ package eu.europa.ted.efx.model; +import java.util.ArrayDeque; +import java.util.Deque; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; @@ -22,6 +24,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.misc.ParseCancellationException; import eu.europa.ted.efx.exceptions.InvalidIdentifierException; @@ -52,6 +55,12 @@ public class CallStack { */ private final TypeChecker typeChecker; + /** + * Tracks the current parser rule context for error reporting. + * Mirrors the ANTLR parse tree walk via {@link #pushContext} / {@link #popContext}. + */ + private final Deque contextStack = new ArrayDeque<>(); + /** * Stack frames are means of controlling the scope of variables and parameters. * Certain @@ -98,10 +107,10 @@ synchronized T pop(Class expectedType) { if (typeChecker.canConvert(actual, expected)) { return expectedType.cast(TypedExpression.from((TypedExpression) this.pop(), expected)); } - throw TypeMismatchException.cannotConvert(expected, actual); + throw TypeMismatchException.cannotConvert(currentContext(), expected, actual); } - throw TypeMismatchException.cannotConvert(expectedType, actualType); + throw TypeMismatchException.cannotConvert(currentContext(), expectedType, actualType); } synchronized T peek(Class expectedType) { @@ -117,7 +126,7 @@ synchronized T peek(Class expectedType) { return expectedType.cast(TypedExpression.from((TypedExpression) this.peek(), expected)); } } - throw TypeMismatchException.cannotConvert(expectedType, actualType); + throw TypeMismatchException.cannotConvert(currentContext(), expectedType, actualType); } /** @@ -159,6 +168,28 @@ public CallStack() { this(TypeChecker.V2); } + /** + * Pushes a parser rule context onto the context stack. + * Called by the translator's {@code enterEveryRule}. + */ + public void pushContext(final ParserRuleContext context) { + this.contextStack.push(context); + } + + /** + * Pops the current parser rule context from the context stack. + * Called by the translator's {@code exitEveryRule}. + */ + public void popContext() { + if (!this.contextStack.isEmpty()) { + this.contextStack.pop(); + } + } + + private ParserRuleContext currentContext() { + return this.contextStack.isEmpty() ? null : this.contextStack.peek(); + } + /** * Creates a new stack frame and pushes it on top of the call stack. * @@ -199,7 +230,7 @@ public void popStackFrame() { */ public void declareIdentifier(Identifier identifier) { if (this.inScope(identifier.name)) { - throw InvalidIdentifierException.alreadyDeclared(identifier.name); + throw InvalidIdentifierException.alreadyDeclared(this.currentContext(), identifier.name); } this.frames.peek().declareIdentifier(identifier); } @@ -211,21 +242,21 @@ public void declareIdentifier(Identifier identifier) { */ public void declareGlobalIdentifier(Identifier identifier) { if (this.inScope(identifier.name)) { - throw InvalidIdentifierException.alreadyDeclared(identifier.name); + throw InvalidIdentifierException.alreadyDeclared(this.currentContext(), identifier.name); } this.globalIdentifierRegistry.put(identifier.name, identifier); } public void declareFunction(Function function) { if (this.inScope(function.name)) { - throw InvalidIdentifierException.alreadyDeclared(function.name); + throw InvalidIdentifierException.alreadyDeclared(this.currentContext(), function.name); } this.globalIdentifierRegistry.put(function.name, function); } public void declareTemplate(Template template) { if (this.inScope(template.name)) { - throw InvalidIdentifierException.alreadyDeclared(template.name); + throw InvalidIdentifierException.alreadyDeclared(this.currentContext(), template.name); } this.globalIdentifierRegistry.put(template.name, template); } @@ -324,7 +355,7 @@ public Function getFunction(String functionName) { return Optional.ofNullable(this.globalIdentifierRegistry.get(functionName)) .filter(Function.class::isInstance) .map(Function.class::cast) - .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(functionName)); + .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), functionName)); } /** @@ -339,7 +370,7 @@ public Template getTemplate(String templateName) { return Optional.ofNullable(this.globalIdentifierRegistry.get(templateName)) .filter(Template.class::isInstance) .map(Template.class::cast) - .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(templateName)); + .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), templateName)); } /** @@ -354,7 +385,7 @@ public Dictionary getDictionary(String dictionaryName) { return Optional.ofNullable(this.globalIdentifierRegistry.get(dictionaryName)) .filter(Dictionary.class::isInstance) .map(Dictionary.class::cast) - .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(dictionaryName)); + .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), dictionaryName)); } /** @@ -368,7 +399,7 @@ public ParsedParameters getFunctionParameters(String functionName) { return Optional.ofNullable(this.globalIdentifierRegistry.get(functionName)) .filter(Function.class::isInstance) .map(identifier -> ((Function) identifier).parameters) - .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(functionName)); + .orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), functionName)); } /** @@ -387,7 +418,7 @@ public Class getTypeOfIdentifier(String identifierName) { Optional identifier = this.getIdentifier(identifierName) .or(() -> Optional.ofNullable((Identifier) this.getFunction(identifierName))); if (!identifier.isPresent()) { - throw InvalidIdentifierException.undeclaredIdentifier(identifierName); + throw InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), identifierName); } return identifier.get().dataType; } @@ -406,7 +437,7 @@ public void pushIdentifierReference(String identifierName) { () -> getVariable(identifierName).ifPresentOrElse( variable -> this.push(variable.referenceExpression), () -> { - throw InvalidIdentifierException.undeclaredIdentifier(identifierName); + throw InvalidIdentifierException.undeclaredIdentifier(this.currentContext(), identifierName); })); } @@ -459,7 +490,7 @@ public synchronized TypedExpression peekType(int offset) { if (item instanceof TypedExpression) { return (TypedExpression) item; } - throw TypeMismatchException.cannotConvert(TypedExpression.class, item.getClass()); + throw TypeMismatchException.cannotConvert(this.currentContext(), TypedExpression.class, item.getClass()); } /** diff --git a/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DynamicExpression.java b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DynamicExpression.java new file mode 100644 index 00000000..3c419eef --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/expressions/scalar/DynamicExpression.java @@ -0,0 +1,32 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.efx.model.expressions.scalar; + +import eu.europa.ted.efx.model.types.EfxDataType; +import eu.europa.ted.efx.model.types.EfxDataTypeAssociation; + +/** + * A dynamic-typed scalar AST node, extending {@link BooleanExpression}. + * + * Represents the result of a dynamic function call (tri-state: 1 = true, 0 = false, -1 = error). + * Can be used wherever a boolean expression is expected, similar to how + * {@link MultilingualStringExpression} can be used wherever a string is expected. + */ +@EfxDataTypeAssociation(dataType = EfxDataType.DynamicScalar.class) +public class DynamicExpression extends BooleanExpression { + + public DynamicExpression(final String script) { + super(script, EfxDataType.DynamicScalar.class); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/rules/AssertRule.java b/src/main/java/eu/europa/ted/efx/model/rules/AssertRule.java index c41fb366..b9c26e68 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/AssertRule.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/AssertRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -14,14 +14,13 @@ package eu.europa.ted.efx.model.rules; public class AssertRule extends ValidationRule { - public AssertRule() { - super(); - } - public AssertRule(ValidationRule other) { + public AssertRule(final ValidationRule other) { + super(other.ruleSet); this.id = other.id; this.subject = other.subject; this.severity = other.severity; + this.nature = other.nature; this.condition = other.condition; this.invertedCondition = other.invertedCondition; this.expression = other.expression; @@ -29,5 +28,6 @@ public AssertRule(ValidationRule other) { this.noticeSubtypeRange = other.noticeSubtypeRange; this.flag = other.flag; this.scope = other.scope; + this.autoGeneratedVariables = other.autoGeneratedVariables; } } diff --git a/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java b/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java index 15e03a53..71e9b07e 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/NoticeSubtypeRange.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/model/rules/ReportRule.java b/src/main/java/eu/europa/ted/efx/model/rules/ReportRule.java index e5128f93..dc53065e 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/ReportRule.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/ReportRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -14,13 +14,13 @@ package eu.europa.ted.efx.model.rules; public class ReportRule extends ValidationRule { - public ReportRule() { - super(); - } - public ReportRule(ValidationRule other) { + + public ReportRule(final ValidationRule other) { + super(other.ruleSet); this.id = other.id; this.subject = other.subject; this.severity = other.severity; + this.nature = other.nature; this.condition = other.condition; this.invertedCondition = other.invertedCondition; this.expression = other.expression; @@ -28,5 +28,6 @@ public ReportRule(ValidationRule other) { this.noticeSubtypeRange = other.noticeSubtypeRange; this.flag = other.flag; this.scope = other.scope; + this.autoGeneratedVariables = other.autoGeneratedVariables; } } diff --git a/src/main/java/eu/europa/ted/efx/model/rules/RuleNature.java b/src/main/java/eu/europa/ted/efx/model/rules/RuleNature.java index 87cc18a5..7e0e087c 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/RuleNature.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/RuleNature.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/model/rules/RuleScope.java b/src/main/java/eu/europa/ted/efx/model/rules/RuleScope.java index fb540ae9..535c994c 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/RuleScope.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/RuleScope.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2026 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/model/rules/RuleSet.java b/src/main/java/eu/europa/ted/efx/model/rules/RuleSet.java index ef7c3c6d..35b7e76a 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/RuleSet.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/RuleSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -16,24 +16,29 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.ParsedEntity; +import eu.europa.ted.efx.model.variables.DynamicVariable; import eu.europa.ted.efx.model.variables.Variable; public class RuleSet implements ParsedEntity, Iterable { + ValidationStage validationStage; Context context; List rules; ValidationRule fallbackRule; - List stageVariables; + List promotedVariables; List localVariables; - public RuleSet() { + public RuleSet(final ValidationStage validationStage) { + this.validationStage = validationStage; this.context = null; this.rules = new ArrayList<>(); - this.stageVariables = new ArrayList<>(); + this.promotedVariables = new ArrayList<>(); this.localVariables = new ArrayList<>(); this.fallbackRule = null; } @@ -55,7 +60,7 @@ public void setFallbackRule(ValidationRule fallbackRule) { public void addVariable(Variable variable) { if (this.context == null) { - this.stageVariables.add(variable); + this.promotedVariables.add(variable); } else { this.localVariables.add(variable); } @@ -66,18 +71,40 @@ public Iterator iterator() { return this.rules.iterator(); } + public Stream stream() { + return this.rules.stream(); + } + + public ValidationStage getValidationStage() { + return this.validationStage; + } + public Context getContext() { return this.context; } - public List getStageVariables() { - return this.stageVariables; + public List getPromotedVariables() { + return this.promotedVariables; + } + + public List getDynamicPromotedVariables() { + return this.promotedVariables.stream() + .filter(DynamicVariable.class::isInstance) + .map(DynamicVariable.class::cast) + .collect(Collectors.toList()); } public List getLocalVariables() { return this.localVariables; } + public List getAllVariables() { + List allVariables = new ArrayList<>(this.validationStage.getAllVariables()); + allVariables.addAll(this.promotedVariables); + allVariables.addAll(this.localVariables); + return allVariables; + } + public ValidationRule getFallbackRule() { return this.fallbackRule; } diff --git a/src/main/java/eu/europa/ted/efx/model/rules/RuleSeverity.java b/src/main/java/eu/europa/ted/efx/model/rules/RuleSeverity.java index d75a8cec..ade15fb1 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/RuleSeverity.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/RuleSeverity.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/model/rules/RuleStack.java b/src/main/java/eu/europa/ted/efx/model/rules/RuleStack.java new file mode 100644 index 00000000..3f2a4bfa --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/rules/RuleStack.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.efx.model.rules; + +import java.util.ArrayDeque; +import java.util.Deque; + +import eu.europa.ted.efx.exceptions.TranslatorConfigurationException; +import eu.europa.ted.efx.model.ParsedEntity; + +/** + * A typed stack for building the rules intermediate model during translation. + * + * Holds ValidationStage, RuleSet, and ValidationRule instances, providing + * typed pop/peek methods that give clear error messages instead of ClassCastExceptions. + */ +public class RuleStack { + + private final Deque stack = new ArrayDeque<>(); + + public void push(ParsedEntity item) { + this.stack.push(item); + } + + public T pop(Class expectedType) { + if (this.stack.isEmpty()) { + throw TranslatorConfigurationException.rulesStackEmpty(expectedType); + } + ParsedEntity item = this.stack.pop(); + if (!expectedType.isInstance(item)) { + throw TranslatorConfigurationException.rulesStackError(expectedType, item.getClass()); + } + return expectedType.cast(item); + } + + public void clear() { + this.stack.clear(); + } + + public boolean contains(final Class type) { + return this.stack.stream().anyMatch(type::isInstance); + } + + public T peek(Class expectedType) { + if (this.stack.isEmpty()) { + throw TranslatorConfigurationException.rulesStackEmpty(expectedType); + } + ParsedEntity item = this.stack.peek(); + if (!expectedType.isInstance(item)) { + throw TranslatorConfigurationException.rulesStackError(expectedType, item.getClass()); + } + return expectedType.cast(item); + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/rules/CompleteValidation.java b/src/main/java/eu/europa/ted/efx/model/rules/ValidationPlan.java similarity index 71% rename from src/main/java/eu/europa/ted/efx/model/rules/CompleteValidation.java rename to src/main/java/eu/europa/ted/efx/model/rules/ValidationPlan.java index 94d62e61..ce38e0aa 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/CompleteValidation.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/ValidationPlan.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -14,19 +14,26 @@ package eu.europa.ted.efx.model.rules; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; + +import java.util.stream.Collectors; import eu.europa.ted.efx.model.ParsedEntity; +import eu.europa.ted.efx.model.variables.DynamicVariable; import eu.europa.ted.efx.model.variables.Variable; -public class CompleteValidation implements ParsedEntity { +public class ValidationPlan implements ParsedEntity { + + List variables = new ArrayList<>(); - List globalVariables = new ArrayList<>(); - List stages = new ArrayList<>(); List noticeSubtypes = new ArrayList<>(); + Map endpoints = new LinkedHashMap<>(); + public List getNoticeSubtypes() { return new ArrayList<>(this.noticeSubtypes); } @@ -59,12 +66,27 @@ public void addNoticeSubtypes(List noticeSubtypes) { } } - public void addGlobalVariable(Variable variable) { - this.globalVariables.add(variable); + public void addVariable(Variable variable) { + this.variables.add(variable); + } + + public List getVariables() { + return new ArrayList<>(this.variables); + } + + public List getDynamicVariables() { + return this.variables.stream() + .filter(DynamicVariable.class::isInstance) + .map(DynamicVariable.class::cast) + .collect(Collectors.toList()); + } + + public void declareEndpoint(String name, String url) { + this.endpoints.put(name, url); } - public List getGlobalVariables() { - return new ArrayList<>(this.globalVariables); + public Map getEndpoints() { + return new LinkedHashMap<>(this.endpoints); } private void sortNoticeSubtypes() { diff --git a/src/main/java/eu/europa/ted/efx/model/rules/ValidationRule.java b/src/main/java/eu/europa/ted/efx/model/rules/ValidationRule.java index 5efc7147..fed2116b 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/ValidationRule.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/ValidationRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -13,12 +13,21 @@ */ package eu.europa.ted.efx.model.rules; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + import eu.europa.ted.efx.model.Context; import eu.europa.ted.efx.model.ParsedEntity; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.variables.DynamicVariable; +import eu.europa.ted.efx.model.variables.Variable; public class ValidationRule implements ParsedEntity { + RuleSet ruleSet; String id; Context subject; RuleSeverity severity; @@ -30,6 +39,15 @@ public class ValidationRule implements ParsedEntity { NoticeSubtypeRange noticeSubtypeRange; String flag; RuleScope scope = RuleScope.ANY; + List autoGeneratedVariables = new ArrayList<>(); + + public ValidationRule(final RuleSet ruleSet) { + this.ruleSet = ruleSet; + } + + public RuleSet getRuleSet() { + return this.ruleSet; + } public void setId(String id) { this.id = id; @@ -107,4 +125,58 @@ public void setScope(RuleScope scope) { public RuleScope getScope() { return this.scope; } + + public void addAutoGeneratedVariable(DynamicVariable.AutoGenerated variable) { + this.autoGeneratedVariables.add(variable); + this.nature = RuleNature.DYNAMIC; + } + + public List getAutoGeneratedVariables() { + return this.autoGeneratedVariables; + } + + public boolean hasAutoGeneratedVariables() { + return !this.autoGeneratedVariables.isEmpty(); + } + + public List findReferencedVariables(final List variables) { + if (variables.isEmpty()) { + return variables; + } + String script = this.invertedConditionOrExpression.getScript(); + return variables.stream() + .filter(var -> Pattern.compile("\\$" + Pattern.quote(var.name) + "(?![\\w-])") + .matcher(script).find()) + .collect(Collectors.toList()); + } + + public List findReferencedDynamicVariables() { + return this.findReferencedVariables(this.ruleSet.getAllVariables()).stream() + .flatMap(var -> { + if (var instanceof DynamicVariable) { + return Stream.of((DynamicVariable) var); + } + return var.getDynamicDependencies().stream(); + }) + .distinct() + .collect(Collectors.toList()); + } + + public boolean isUniversal() { + return this.noticeSubtypeRange != null && this.noticeSubtypeRange.isUniversal(); + } + + public boolean isForNoticeSubtype(String noticeSubtype) { + return this.noticeSubtypeRange != null + && this.noticeSubtypeRange.asList().contains(noticeSubtype); + } + + public boolean isForPostValidation() { + return this.scope != RuleScope.PRE; + } + + public boolean isForPreValidation() { + return this.scope != RuleScope.POST; + } + } diff --git a/src/main/java/eu/europa/ted/efx/model/rules/ValidationStage.java b/src/main/java/eu/europa/ted/efx/model/rules/ValidationStage.java index 4892788e..b8e6ec1a 100644 --- a/src/main/java/eu/europa/ted/efx/model/rules/ValidationStage.java +++ b/src/main/java/eu/europa/ted/efx/model/rules/ValidationStage.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -18,17 +18,22 @@ import java.util.List; import java.util.Set; +import java.util.stream.Collectors; + import eu.europa.ted.efx.model.ParsedEntity; +import eu.europa.ted.efx.model.variables.DynamicVariable; import eu.europa.ted.efx.model.variables.Variable; public class ValidationStage implements ParsedEntity { String name; + ValidationPlan validationPlan; List ruleSets; List variables; - public ValidationStage(String name) { + public ValidationStage(final String name, final ValidationPlan validationPlan) { this.name = name; + this.validationPlan = validationPlan; this.ruleSets = new ArrayList<>(); this.variables = new ArrayList<>(); } @@ -45,6 +50,10 @@ public String getName() { return this.name; } + public ValidationPlan getValidationPlan() { + return this.validationPlan; + } + public List getRuleSets() { return this.ruleSets; } @@ -53,6 +62,23 @@ public List getVariables() { return this.variables; } + public List getInheritedVariables() { + return this.validationPlan.getVariables(); + } + + public List getAllVariables() { + List allVariables = new ArrayList<>(this.validationPlan.getVariables()); + allVariables.addAll(this.variables); + return allVariables; + } + + public List getDynamicVariables() { + return this.variables.stream() + .filter(DynamicVariable.class::isInstance) + .map(DynamicVariable.class::cast) + .collect(Collectors.toList()); + } + public boolean containsUniversalRules() { for (RuleSet ruleSet : this.ruleSets) { for (ValidationRule rule : ruleSet) { diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java b/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java index 3a7f9545..bcc30b0c 100644 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java +++ b/src/main/java/eu/europa/ted/efx/model/types/EfxDataType.java @@ -50,6 +50,7 @@ interface ConcreteSequence extends EfxDataType {} // EFX primitive types (extend Primitive marker) interface Boolean extends Primitive {} + interface Dynamic extends Boolean {} interface String extends Primitive {} interface MultilingualString extends String {} interface Number extends Primitive {} @@ -62,6 +63,7 @@ interface Void extends EfxDataType {} // Concrete scalar types (primitive + Cardinality.Scalar + ConcreteScalar) interface BooleanScalar extends Boolean, Cardinality.Scalar, ConcreteScalar {} + interface DynamicScalar extends BooleanScalar, Dynamic {} interface StringScalar extends String, Cardinality.Scalar, ConcreteScalar {} interface MultilingualStringScalar extends StringScalar, MultilingualString {} interface NumberScalar extends Number, Cardinality.Scalar, ConcreteScalar {} @@ -72,6 +74,7 @@ interface NodeScalar extends Node, Cardinality.Scalar, ConcreteScalar {} // Concrete sequence types (primitive + Cardinality.Sequence + ConcreteSequence) interface BooleanSequence extends Boolean, Cardinality.Sequence, ConcreteSequence {} + interface DynamicSequence extends BooleanSequence, Dynamic {} interface StringSequence extends String, Cardinality.Sequence, ConcreteSequence {} interface MultilingualStringSequence extends StringSequence, MultilingualString {} interface NumberSequence extends Number, Cardinality.Sequence, ConcreteSequence {} diff --git a/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java b/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java index ea3cc0bf..92ef8369 100644 --- a/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java +++ b/src/main/java/eu/europa/ted/efx/model/types/EfxTypeLattice.java @@ -67,6 +67,9 @@ private static final class TypeVariants { new TypeVariants(EfxDataType.Number.class, EfxDataType.NumberScalar.class, EfxDataType.NumberSequence.class), + new TypeVariants(EfxDataType.Dynamic.class, + EfxDataType.DynamicScalar.class, + EfxDataType.DynamicSequence.class), new TypeVariants(EfxDataType.Boolean.class, EfxDataType.BooleanScalar.class, EfxDataType.BooleanSequence.class), @@ -173,7 +176,8 @@ public static boolean isSequence(Class type) { */ public static boolean isRegistered(Class type) { for (TypeVariants variants : TYPE_VARIANTS) { - if (variants.scalar.equals(type) || variants.sequence.equals(type)) { + if (variants.scalar != null && variants.scalar.equals(type) + || variants.sequence != null && variants.sequence.equals(type)) { return true; } } diff --git a/src/main/java/eu/europa/ted/efx/model/variables/DynamicFunction.java b/src/main/java/eu/europa/ted/efx/model/variables/DynamicFunction.java new file mode 100644 index 00000000..0c6e5f98 --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/DynamicFunction.java @@ -0,0 +1,41 @@ +/* + * Copyright 2025 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.efx.model.variables; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.scalar.DynamicExpression; +import eu.europa.ted.efx.model.rules.RuleSeverity; + +/** + * A dynamic function declared in EFX rules. + * + * Unlike a regular {@link Function}, a dynamic function has no body expression. + * It delegates to an external API endpoint at validation runtime. The endpoint + * is identified by name and resolved by the consumer. + * Dynamic functions return a tri-state result (1 = true, 0 = false, -1 = error). + */ +public class DynamicFunction extends Function { + + public final String endpointName; + public final RuleSeverity errorSeverity; + public final String errorLabel; + + public DynamicFunction(String name, ParsedParameters parameters, String endpointName, + RuleSeverity errorSeverity, String errorLabel) { + super(name, parameters, Expression.empty(DynamicExpression.class)); + this.endpointName = endpointName; + this.errorSeverity = errorSeverity; + this.errorLabel = errorLabel; + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/variables/DynamicVariable.java b/src/main/java/eu/europa/ted/efx/model/variables/DynamicVariable.java new file mode 100644 index 00000000..daee61ca --- /dev/null +++ b/src/main/java/eu/europa/ted/efx/model/variables/DynamicVariable.java @@ -0,0 +1,84 @@ +/* + * Copyright 2025 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.efx.model.variables; + +import eu.europa.ted.efx.model.expressions.Expression; +import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.expressions.scalar.DynamicExpression; +import eu.europa.ted.efx.model.rules.RuleSeverity; + +/** + * A variable whose value is computed by a dynamic function call. + * + * The initialization expression is the raw dynamic function call (e.g., {@code efx:call-api(...)}). + * The reference expression compares the result to 1 (e.g., {@code $varName = 1}), + * since the dynamic function returns a tri-state integer: 1 = true, 0 = false, -1 = error. + * + * Carries the API metadata needed for error handling (endpoint, error severity, label). + */ +public class DynamicVariable extends Variable { + + public final String endpointName; + private final RuleSeverity errorSeverity; + private final String errorLabel; + + public DynamicVariable(String name, Expression declarationExpression, + DynamicExpression initializationExpression, BooleanExpression referenceExpression, + String endpointName, RuleSeverity errorSeverity, String errorLabel) { + super(name, declarationExpression, initializationExpression, referenceExpression); + this.endpointName = endpointName; + this.errorSeverity = errorSeverity; + this.errorLabel = errorLabel; + } + + public RuleSeverity errorSeverity() { + return this.errorSeverity; + } + + public String errorLabel() { + return this.errorLabel; + } + + public String identity() { + return this.name; + } + + /** + * An auto-generated {@link DynamicVariable} created when a dynamic function is called + * inline in a rule expression (e.g., {@code ?check-buyer(BT-00-Identifier)}). + * + * Unlike a user-declared {@link DynamicVariable}, this has an auto-generated + * name (e.g., {@code __apiResult1}) and needs its own {@code } declaration + * in the Schematron rule output. + */ + public static class AutoGenerated extends DynamicVariable { + + private final int index; + + public AutoGenerated(int index, String name, Expression declarationExpression, + DynamicExpression initializationExpression, BooleanExpression referenceExpression, + String endpointName, RuleSeverity errorSeverity, String errorLabel) { + super(name, declarationExpression, initializationExpression, referenceExpression, + endpointName, errorSeverity, errorLabel); + this.index = index; + } + + public int index() { return this.index; } + + @Override + public String identity() { + return String.valueOf(this.index); + } + } +} diff --git a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java index 37128ee9..db522733 100644 --- a/src/main/java/eu/europa/ted/efx/model/variables/Variable.java +++ b/src/main/java/eu/europa/ted/efx/model/variables/Variable.java @@ -13,6 +13,9 @@ */ package eu.europa.ted.efx.model.variables; +import java.util.ArrayList; +import java.util.List; + import eu.europa.ted.efx.model.expressions.DeclarationExpression; import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.TypedExpression; @@ -32,6 +35,7 @@ public class Variable extends Identifier { public final Expression declarationExpression; public final TypedExpression initializationExpression; public final TypedExpression referenceExpression; + private final List dynamicDependencies = new ArrayList<>(); public Variable(String variableName, TypedExpression initializationExpression, TypedExpression referenceExpression) { this(variableName, DeclarationExpression.empty(), initializationExpression, referenceExpression); @@ -48,6 +52,18 @@ public Variable(String variableName, Expression declarationExpression, TypedExpr .isAssignableFrom(EfxTypeLattice.toPrimitive(initializationExpression.getDataType())); } + public void addDynamicDependencies(final List dependencies) { + this.dynamicDependencies.addAll(dependencies); + } + + public List getDynamicDependencies() { + return this.dynamicDependencies; + } + + public boolean hasDynamicDependencies() { + return !this.dynamicDependencies.isEmpty(); + } + @Override public boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java index 65305bb8..5f1ff96a 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1.java @@ -134,6 +134,16 @@ public EfxExpressionTranslatorV1(final SymbolResolver symbolResolver, this.efxContext = new ContextStack(symbols); } + @Override + public void enterEveryRule(final ParserRuleContext ctx) { + this.stack.pushContext(ctx); + } + + @Override + public void exitEveryRule(final ParserRuleContext ctx) { + this.stack.popContext(); + } + @Override public String translateExpression(final String expression, final String... parameters) { this.expressionParameters.addAll(Arrays.asList(parameters)); diff --git a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java index 03223f80..38391384 100644 --- a/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java +++ b/src/main/java/eu/europa/ted/efx/sdk1/xpath/XPathScriptGeneratorV1.java @@ -21,6 +21,7 @@ import eu.europa.ted.efx.model.expressions.Expression; import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.expressions.scalar.NodePath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.StringExpression; import eu.europa.ted.efx.model.expressions.scalar.StringLiteral; @@ -98,4 +99,16 @@ public PathExpression composeFieldValueReference(PathExpression fieldReference) } return super.composeFieldValueReference(fieldReference); } + + @Override + public PathExpression composeExternalReference(StringExpression externalReference) { + return new NodePath( + "fn:doc(concat($urlPrefix, " + externalReference.getScript() + "))"); + } + + @Override + public PathExpression composeFieldInExternalReference(PathExpression externalReference, + PathExpression fieldReference) { + return Expression.instantiate(externalReference.getScript() + fieldReference.getScript(), fieldReference.getClass()); + } } diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java index 9749acf5..d5aa75c8 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxErrorStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 European Union + * Copyright 2026 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -90,7 +90,6 @@ public class EfxErrorStrategy extends DefaultErrorStrategy { Map.entry(RULE_absoluteNodeReference, "node reference"), Map.entry(RULE_nodeReferenceWithPredicate, "node reference"), Map.entry(RULE_nodeContext, "node reference"), - Map.entry(RULE_noticeReference, "notice reference"), Map.entry(RULE_codelistReference, "codelist reference"), Map.entry(RULE_variableReference, "variable reference"), @@ -144,7 +143,12 @@ public class EfxErrorStrategy extends DefaultErrorStrategy { Map.entry(RULE_inClause, "in clause"), Map.entry(RULE_noticeTypeList, "notice type list"), Map.entry(RULE_noticeTypeRange, "notice type range"), - Map.entry(RULE_noticeType, "notice type") + Map.entry(RULE_noticeType, "notice type"), + + // API declarations + Map.entry(RULE_apiEndpointDeclaration, "API endpoint declaration"), + Map.entry(RULE_dynamicFunctionDeclaration, "dynamic function declaration"), + Map.entry(RULE_onErrorClause, "ON ERROR clause") ); @Override diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java index d6d504b7..c0616eb2 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2.java @@ -151,6 +151,16 @@ public EfxExpressionTranslatorV2(final SymbolResolver symbolResolver, this.efxContext = new ContextStack(symbols); } + @Override + public void enterEveryRule(final ParserRuleContext ctx) { + this.stack.pushContext(ctx); + } + + @Override + public void exitEveryRule(final ParserRuleContext ctx) { + this.stack.popContext(); + } + @Override public String translateExpression(final String expression, final String... arguments) { this.expressionArguments.addAll(Arrays.asList(arguments)); @@ -258,8 +268,8 @@ protected String getFieldId(FieldReferenceContext ctx) { return getFieldId(ctx.absoluteFieldReference()); } - if (ctx.fieldReferenceInOtherNotice() != null) { - return getFieldId(ctx.fieldReferenceInOtherNotice()); + if (ctx.fieldReferenceWithVariableContextOverride() != null) { + return getFieldId(ctx.fieldReferenceWithVariableContextOverride()); } assert false : "Unexpected context type for field reference: " + ctx.getClass().getSimpleName(); return null; @@ -272,11 +282,11 @@ protected String getFieldId(AbsoluteFieldReferenceContext ctx) { return getFieldId(ctx.reference.reference); } - protected String getFieldId(FieldReferenceInOtherNoticeContext ctx) { + protected String getFieldId(FieldReferenceWithVariableContextOverrideContext ctx) { if (ctx == null) { return null; } - return getFieldId(ctx.reference.reference.reference.reference.reference); + return getFieldId(ctx.reference.reference.reference.reference); } protected String getFieldId(FieldContextContext ctx) { @@ -312,8 +322,8 @@ protected static String getNodeId(NodeReferenceContext ctx) { return getNodeId(ctx.absoluteNodeReference().nodeReferenceWithPredicate()); } - if (ctx.nodeReferenceInOtherNotice() != null) { - return getNodeId(ctx.nodeReferenceInOtherNotice().nodeReferenceWithPredicate()); + if (ctx.nodeReferenceWithPredicate() != null) { + return getNodeId(ctx.nodeReferenceWithPredicate()); } assert false : "Unexpected context type for node reference: " + ctx.getClass().getSimpleName(); @@ -1799,7 +1809,7 @@ public void exitFieldContext(FieldContextContext ctx) { NumericExpression index = this.stack.pop(NumericExpression.class); final TypedExpression top = this.stack.peekType(); if (!(top instanceof PathExpression)) { - throw TypeMismatchException.cannotConvert(PathExpression.class, top.getClass()); + throw TypeMismatchException.cannotConvert(ctx, PathExpression.class, top.getClass()); } PathExpression fieldPath = (PathExpression) this.stack.pop(top.getClass()); this.stack.push(this.script.composeIndexer(fieldPath.asSequence(), index, @@ -1873,35 +1883,6 @@ public void exitPredicate(PredicateContext ctx) { // #endregion References with Predicates ------------------------------------ - // #region External References ---------------------------------------------- - - @Override - public void exitNoticeReference(NoticeReferenceContext ctx) { - this.stack.push(this.script.composeExternalReference(this.stack.pop(StringExpression.class))); - } - - @Override - public void enterFieldReferenceInOtherNotice(FieldReferenceInOtherNoticeContext ctx) { - if (ctx.noticeReference() != null) { - // We push a null context as we switch to an external notice and we need XPaths to be absolute - this.efxContext.push(null); - } - } - - @Override - public void exitFieldReferenceInOtherNotice(FieldReferenceInOtherNoticeContext ctx) { - if (ctx.noticeReference() != null) { - PathExpression field = this.stack.pop(PathExpression.class); - PathExpression notice = this.stack.pop(PathExpression.class); - this.stack.push(this.script.composeFieldInExternalReference(notice, field)); - - // Finally, pop the null context we pushed during enterFieldReferenceInOtherNotice - this.efxContext.pop(); - } - } - - // #endregion External References ------------------------------------------- - // #region Value References ------------------------------------------------- @Override @@ -2158,7 +2139,7 @@ public void exitSequenceFromVariableReference(SequenceFromVariableReferenceConte * * @param ctx the variable reference context */ - private void resolveAndPushVariableReference(VariableReferenceContext ctx) { + protected void resolveAndPushVariableReference(VariableReferenceContext ctx) { String variableName = ctx.variableName.getText(); Context variableContext = this.efxContext.getContextFromVariable(variableName); @@ -2173,7 +2154,7 @@ private void resolveAndPushVariableReference(VariableReferenceContext ctx) { Optional refExpr = this.stack.getParameter(variableName) .or(() -> this.stack.getVariable(variableName).map(v -> v.referenceExpression)); boolean isSequence = EfxTypeLattice.isSequence( - refExpr.orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(variableName)).getDataType()); + refExpr.orElseThrow(() -> InvalidIdentifierException.undeclaredIdentifier(ctx, variableName)).getDataType()); switch (this.currentCardinalityResolutionContext()) { case RESOLVE_SEQUENCE: if (!isSequence) { @@ -2279,7 +2260,7 @@ private Class resolveScalarType(TypedExpression expr return efxDataTypeToScalarExpressionMap.get(primitive).asSubclass(ScalarExpression.class); } - private Class resolveScalarType(Class primitiveType) { + protected Class resolveScalarType(Class primitiveType) { Class expressionType = efxDataTypeToScalarExpressionMap.get(primitiveType); if (expressionType == null || !ScalarExpression.class.isAssignableFrom(expressionType)) { throw TranslatorConfigurationException.missingTypeMapping(primitiveType, "resolveScalarType"); @@ -2493,7 +2474,7 @@ public void exitDurationSequenceParameterDeclaration(DurationSequenceParameterDe this.exitParameterDeclaration(ctx, ctx.parameterName.getText(), DurationSequenceExpression.class); } - private void exitParameterDeclaration(ParserRuleContext ctx, String parameterName, Class parameterType) { + protected void exitParameterDeclaration(ParserRuleContext ctx, String parameterName, Class parameterType) { if (this.expressionArguments.isEmpty()) { throw InvalidArgumentException.missingArgument(ctx, parameterName); } @@ -2618,7 +2599,7 @@ public void exitFieldPrivacyCodeProperty(FieldPrivacyCodePropertyContext ctx) { public void exitRawValueReference(RawValueReferenceContext ctx) { final TypedExpression top = this.stack.peekType(); if (!(top instanceof PathExpression)) { - throw TypeMismatchException.cannotConvert(PathExpression.class, top.getClass()); + throw TypeMismatchException.cannotConvert(ctx, PathExpression.class, top.getClass()); } if (top instanceof SequenceExpression && this.currentCardinalityResolutionContext() == CardinalityResolutionContext.RESOLVE_SCALAR) { @@ -2853,7 +2834,7 @@ public void exitLateBoundToNumberFunction(LateBoundToNumberFunctionContext ctx) } else if (BooleanExpression.class.isAssignableFrom(type)) { this.stack.push(this.script.composeToNumberConversion(this.stack.pop(BooleanExpression.class))); } else { - throw TypeMismatchException.cannotConvert(StringExpression.class, type); + throw TypeMismatchException.cannotConvert(ctx, StringExpression.class, type); } } @@ -2975,7 +2956,7 @@ public void exitLateBoundToStringFunction(LateBoundToStringFunctionContext ctx) } else if (DurationExpression.class.isAssignableFrom(type)) { this.stack.push(this.script.composeToStringConversion(this.stack.pop(DurationExpression.class))); } else { - throw TypeMismatchException.cannotConvert(NumericExpression.class, type); + throw TypeMismatchException.cannotConvert(ctx, NumericExpression.class, type); } } @@ -3942,7 +3923,7 @@ private void dispatchComposeLateBoundAdditiveExpression(LateBoundAdditiveExpress } else if (this.isNumeric(right) && this.isNumeric(left)) { this.exitNumericOperation(operator); } else { - throw TypeMismatchException.incompatibleOperands(operator, (Expression) left, (Expression) right); + throw TypeMismatchException.incompatibleOperands(ctx, operator, (Expression) left, (Expression) right); } } else if (ctx.operator.getType() == EfxLexer.Minus) { if (this.isDuration(right) && this.isDuration(left)) { @@ -3954,7 +3935,7 @@ private void dispatchComposeLateBoundAdditiveExpression(LateBoundAdditiveExpress } else if (this.isNumeric(right) && this.isNumeric(left)) { this.exitNumericOperation(operator); } else { - throw TypeMismatchException.incompatibleOperands(operator, (Expression) left, (Expression) right); + throw TypeMismatchException.incompatibleOperands(ctx, operator, (Expression) left, (Expression) right); } } else { throw TranslatorConfigurationException.unhandledOperator(operator, @@ -3974,7 +3955,7 @@ private void dispatchComposeLateBoundMultiplication(LateBoundMultiplicativeExpre } else if (this.isNumeric(right) && this.isNumeric(left)) { this.exitNumericOperation(operator); } else { - throw TypeMismatchException.incompatibleOperands(operator, (Expression) left, (Expression) right); + throw TypeMismatchException.incompatibleOperands(ctx, operator, (Expression) left, (Expression) right); } } diff --git a/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java b/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java index 0cd5c5cd..c9eee08f 100644 --- a/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java +++ b/src/main/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 European Union + * Copyright 2025 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in @@ -13,6 +13,7 @@ */ package eu.europa.ted.efx.sdk2; +import java.util.ArrayList; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; @@ -22,10 +23,10 @@ import java.util.stream.Collectors; import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.ParserRuleContext; import org.antlr.v4.runtime.CharStream; import org.antlr.v4.runtime.CharStreams; import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.misc.ParseCancellationException; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.ParseTreeWalker; import org.slf4j.Logger; @@ -39,11 +40,15 @@ import eu.europa.ted.efx.interfaces.SymbolResolver; import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.interfaces.ValidatorGenerator; +import eu.europa.ted.efx.exceptions.InvalidIdentifierException; +import eu.europa.ted.efx.exceptions.InvalidUsageException; +import eu.europa.ted.efx.model.CallStack; import eu.europa.ted.efx.model.Context.FieldContext; import eu.europa.ted.efx.model.Context.NodeContext; import eu.europa.ted.efx.model.expressions.PathExpression; import eu.europa.ted.efx.model.expressions.TypedExpression; import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.expressions.scalar.DynamicExpression; import eu.europa.ted.efx.model.expressions.scalar.DateExpression; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; @@ -58,14 +63,20 @@ import eu.europa.ted.efx.model.expressions.sequence.StringSequenceExpression; import eu.europa.ted.efx.model.expressions.sequence.TimeSequenceExpression; import eu.europa.ted.efx.model.rules.AssertRule; -import eu.europa.ted.efx.model.rules.CompleteValidation; +import eu.europa.ted.efx.model.rules.ValidationPlan; import eu.europa.ted.efx.model.rules.NoticeSubtypeRange; import eu.europa.ted.efx.model.rules.ReportRule; import eu.europa.ted.efx.model.rules.RuleSet; import eu.europa.ted.efx.model.rules.RuleScope; import eu.europa.ted.efx.model.rules.RuleSeverity; +import eu.europa.ted.efx.model.rules.RuleStack; import eu.europa.ted.efx.model.rules.ValidationRule; import eu.europa.ted.efx.model.rules.ValidationStage; +import eu.europa.ted.efx.model.variables.DynamicFunction; +import eu.europa.ted.efx.model.variables.DynamicVariable; +import eu.europa.ted.efx.model.variables.ParsedParameter; +import eu.europa.ted.efx.model.variables.ParsedArguments; +import eu.europa.ted.efx.model.variables.ParsedParameters; import eu.europa.ted.efx.model.variables.Variable; import eu.europa.ted.efx.sdk2.EfxParser.*; @@ -94,10 +105,18 @@ public class EfxRulesTranslatorV2 extends EfxExpressionTranslatorV2 * List of validation stages collected during parsing. * This is the intermediate model passed to the validator generator. */ - private CompleteValidation completeValidation = new CompleteValidation(); + private ValidationPlan validationPlan = new ValidationPlan(); private List cachedSortedNoticeSubtypeIds; + private final RuleStack ruleStack = new RuleStack(); + + /** + * Tracks dynamic variable dependencies during variable initializer processing. + * Set to non-null when inside a variable initializer, null otherwise. + */ + private List currentDynamicDependencies = null; + /** * Constructor for EfxRulesTranslatorV2. * @@ -179,26 +198,57 @@ private Map translateRulesFromCharStream(CharStream input, logger.debug("Walking parse tree to build intermediate model"); - // Initialize the stages list for this translation - this.completeValidation = new CompleteValidation(); + // Initialize per-translation state + this.validationPlan = new ValidationPlan(); + this.apiCallCounter = 0; + this.stack = new CallStack(); + this.ruleStack.clear(); // Walk the parse tree - this translator IS the listener ParseTreeWalker walker = new ParseTreeWalker(); walker.walk(this, tree); - if (this.completeValidation.getStages().isEmpty()) { - throw new ParseCancellationException( - "Rules file must contain at least one validation stage"); + if (this.validationPlan.getStages().isEmpty()) { + throw InvalidUsageException.emptyRulesFile(); } // Generate output using the validator generator - return this.validatorGenerator.generateOutput(this.completeValidation); + return this.validatorGenerator.generateOutput(this.validationPlan); } // #region ANTLR Listener Methods for EFX Rules Grammar // #region Variable Initializers + @Override + protected void resolveAndPushVariableReference(VariableReferenceContext ctx) { + if (this.currentDynamicDependencies != null) { + String variableName = ctx.variableName.getText(); + this.stack.getVariable(variableName).ifPresent(var -> { + if (var instanceof DynamicVariable) { + this.currentDynamicDependencies.add((DynamicVariable) var); + } else if (var.hasDynamicDependencies()) { + this.currentDynamicDependencies.addAll(var.getDynamicDependencies()); + } + }); + } + super.resolveAndPushVariableReference(ctx); + } + + private void beginTrackingDynamicDependencies() { + this.currentDynamicDependencies = new ArrayList<>(); + } + + private void endTrackingDynamicDependencies() { + this.currentDynamicDependencies = null; + } + + private void attachDynamicDependencies(final Variable variable) { + if (this.currentDynamicDependencies != null && !this.currentDynamicDependencies.isEmpty()) { + variable.addDynamicDependencies(this.currentDynamicDependencies); + } + } + /** * Helper method to handle variable initializers. * Pops the expression, creates a Variable, and pushes it back. @@ -211,43 +261,108 @@ private void exitVariableInitializer(String variableName, this.script.composeVariableDeclaration(variableName, expression.getClass()), expression, this.script.composeVariableReference(variableName, expression.getClass())); + this.attachDynamicDependencies(variable); this.stack.push(variable); } + @Override + public void enterStringVariableInitializer(StringVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); + } + @Override public void exitStringVariableInitializer( StringVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), StringExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterBooleanVariableInitializer(BooleanVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitBooleanVariableInitializer( BooleanVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), BooleanExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void exitDynamicFunctionInvocation(DynamicFunctionInvocationContext ctx) { + String funcName = ctx.functionInvocation().functionName.getText(); + var func = this.stack.getFunction(funcName); + if (!(func instanceof DynamicFunction)) { + throw InvalidUsageException.notADynamicFunction(ctx, funcName); + } + DynamicFunction apiFunc = (DynamicFunction) func; + List args = this.stack.pop(ParsedArguments.class).getArgumentValues(); + var rawCall = this.script.composeDynamicFunction(apiFunc.endpointName, funcName, args); + this.stack.push(new DynamicExpression(rawCall.getScript())); + this.stack.push(apiFunc); + } + + @Override + public void exitDynamicVariableInitializer(DynamicVariableInitializerContext ctx) { + String variableName = ctx.variableName.getText(); + var apiFunc = this.stack.pop(DynamicFunction.class); + var rawCall = this.stack.pop(DynamicExpression.class); + var declaration = this.script.composeVariableDeclaration(variableName, DynamicExpression.class); + var varRef = this.script.composeVariableReference(variableName, NumericExpression.class); + var one = this.script.getNumericLiteralEquivalent("1"); + var referenceExpression = this.script.composeComparisonOperation(varRef, "==", one); + var variable = new DynamicVariable(variableName, declaration, rawCall, referenceExpression, + apiFunc.endpointName, apiFunc.errorSeverity, apiFunc.errorLabel); + this.stack.push(variable); + } + + @Override + public void enterNumericVariableInitializer(NumericVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitNumericVariableInitializer( NumericVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), NumericExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterDateVariableInitializer(DateVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitDateVariableInitializer( DateVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), DateExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterTimeVariableInitializer(TimeVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitTimeVariableInitializer( TimeVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), TimeExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterDurationVariableInitializer(DurationVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitDurationVariableInitializer( DurationVariableInitializerContext ctx) { this.exitVariableInitializer(ctx.variableName.getText(), DurationExpression.class); + this.endTrackingDynamicDependencies(); } /** @@ -261,43 +376,80 @@ private void exitSequenceVariableInitializer(String variableName, this.script.composeVariableDeclaration(variableName, expression.getClass()), expression, this.script.composeVariableReference(variableName, expression.getClass())); + this.attachDynamicDependencies(variable); this.stack.push(variable); } + @Override + public void enterStringSequenceVariableInitializer(StringSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); + } + @Override public void exitStringSequenceVariableInitializer( StringSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), StringSequenceExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterBooleanSequenceVariableInitializer(BooleanSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitBooleanSequenceVariableInitializer( BooleanSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), BooleanSequenceExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterNumericSequenceVariableInitializer(NumericSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitNumericSequenceVariableInitializer( NumericSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), NumericSequenceExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterDateSequenceVariableInitializer(DateSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitDateSequenceVariableInitializer( DateSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), DateSequenceExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterTimeSequenceVariableInitializer(TimeSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitTimeSequenceVariableInitializer( TimeSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), TimeSequenceExpression.class); + this.endTrackingDynamicDependencies(); + } + + @Override + public void enterDurationSequenceVariableInitializer(DurationSequenceVariableInitializerContext ctx) { + this.beginTrackingDynamicDependencies(); } @Override public void exitDurationSequenceVariableInitializer( DurationSequenceVariableInitializerContext ctx) { this.exitSequenceVariableInitializer(ctx.variableName.getText(), DurationSequenceExpression.class); + this.endTrackingDynamicDependencies(); } // #endregion Variable Initializers @@ -321,7 +473,7 @@ public void exitGlobalVariableDeclaration(GlobalVariableDeclarationContext ctx) // Declare the variable in the stack so it can be referenced later // and retrieved via stack.getGlobals() for the validator generator this.stack.declareGlobalIdentifier(variable); - this.completeValidation.addGlobalVariable(variable); + this.validationPlan.addVariable(variable); logger.debug("Declared global variable: {}", variable.name); } } @@ -335,7 +487,7 @@ public void exitGlobalVariableDeclaration(GlobalVariableDeclarationContext ctx) public void enterValidationStage(ValidationStageContext ctx) { this.stack.pushStackFrame(); - this.stack.push(new ValidationStage(ctx.StageIdentifier().getText())); + this.ruleStack.push(new ValidationStage(ctx.StageIdentifier().getText(), this.validationPlan)); } /** @@ -346,8 +498,8 @@ public void enterValidationStage(ValidationStageContext ctx) { public void exitValidationStage(ValidationStageContext ctx) { logger.debug("Exiting STAGE section"); - var stage = this.stack.pop(ValidationStage.class); - this.completeValidation.addStage(stage); + var stage = this.ruleStack.pop(ValidationStage.class); + this.validationPlan.addStage(stage); this.stack.popStackFrame(); } @@ -367,7 +519,7 @@ public void exitStageVariableDeclaration(StageVariableDeclarationContext ctx) { // Declare the variable in the stack so it can be referenced within this stage this.stack.declareIdentifier(variable); - this.stack.peek(ValidationStage.class).addVariable(variable); + this.ruleStack.peek(ValidationStage.class).addVariable(variable); } } @@ -379,7 +531,7 @@ public void exitStageVariableDeclaration(StageVariableDeclarationContext ctx) { @Override public void enterRuleSet(RuleSetContext ctx) { this.stack.pushStackFrame(); - this.stack.push(new RuleSet()); + this.ruleStack.push(new RuleSet(this.ruleStack.peek(ValidationStage.class))); } /** @@ -391,9 +543,9 @@ public void enterRuleSet(RuleSetContext ctx) { public void exitRuleSet(RuleSetContext ctx) { logger.debug("Exiting rule block"); - var ruleSet = this.stack.pop(RuleSet.class); + var ruleSet = this.ruleStack.pop(RuleSet.class); this.stack.popStackFrame(); - var stage = this.stack.peek(ValidationStage.class); + var stage = this.ruleStack.peek(ValidationStage.class); stage.addRuleSet(ruleSet); assert !this.efxContext.isEmpty() : "Expected context to be set in rule block (WITH clause should have pushed it)"; this.efxContext.pop(); @@ -403,7 +555,7 @@ public void exitRuleSet(RuleSetContext ctx) { public void exitVariableInitializer(VariableInitializerContext ctx) { var variable = this.stack.pop(Variable.class); this.stack.declareIdentifier(variable); - this.stack.peek(RuleSet.class).addVariable(variable); + this.ruleStack.peek(RuleSet.class).addVariable(variable); } @Override @@ -449,7 +601,7 @@ private void exitRootContextDeclaration() { private void exitFieldContextDeclaration(String fieldId, PathExpression contextPath, Variable contextVariable) { var context = new FieldContext(fieldId, contextPath, contextVariable); - this.stack.peek(RuleSet.class).setContext(context, contextVariable); + this.ruleStack.peek(RuleSet.class).setContext(context, contextVariable); this.efxContext.push(context); if (contextVariable != null) { this.stack.declareIdentifier(contextVariable); @@ -459,7 +611,7 @@ private void exitFieldContextDeclaration(String fieldId, PathExpression contextP private void exitNodeContextDeclaration(String nodeId, PathExpression contextPath, Variable contextVariable) { var context = new NodeContext(nodeId, contextPath, contextVariable); - this.stack.peek(RuleSet.class).setContext(context, contextVariable); + this.ruleStack.peek(RuleSet.class).setContext(context, contextVariable); this.efxContext.push(context); if (contextVariable != null) { this.stack.declareIdentifier(contextVariable); @@ -503,18 +655,16 @@ public void exitWithClause(WithClauseContext ctx) { @Override public void exitWhenClause(WhenClauseContext ctx) { var condition = this.stack.pop(BooleanExpression.class); - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.peek(ValidationRule.class); var invertedCondition = this.script.composeLogicalNot(condition); rule.setCondition(condition, invertedCondition, this.combineWithOrParenthesized(rule.getExpression(), invertedCondition)); - this.stack.push(rule); } @Override public void exitAsClause(AsClauseContext ctx) { - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.peek(ValidationRule.class); rule.setSeverity(RuleSeverity.fromString(ctx.severity().getText())); rule.setId(ctx.ruleId().getText().replaceAll("^\"|\"$", "")); - this.stack.push(rule); } /** @@ -528,10 +678,10 @@ public void exitForClause(ForClauseContext ctx) { if (ctx.simpleFieldReference() != null) { var fieldId = ctx.simpleFieldReference().FieldId().getText(); - this.stack.peek(ValidationRule.class).setSubject(new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId))); + this.ruleStack.peek(ValidationRule.class).setSubject(new FieldContext(fieldId, this.symbols.getAbsolutePathOfField(fieldId))); } else if (ctx.simpleNodeReference() != null) { var nodeId = ctx.simpleNodeReference().NodeId().getText(); - this.stack.peek(ValidationRule.class).setSubject(new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId))); + this.ruleStack.peek(ValidationRule.class).setSubject(new NodeContext(nodeId, this.symbols.getAbsolutePathOfNode(nodeId))); } else { assert false : "The grammar should prevent reaching this point without a field or node reference"; } @@ -553,13 +703,13 @@ public void exitInClause(InClauseContext ctx) { .collect(Collectors.toUnmodifiableList()); } var noticeSubtypes = new NoticeSubtypeRange(compressedList, this.cachedSortedNoticeSubtypeIds); - this.stack.peek(ValidationRule.class).setNoticeSubtypeRange(noticeSubtypes); - this.completeValidation.addNoticeSubtypes(noticeSubtypes.asList()); + this.ruleStack.peek(ValidationRule.class).setNoticeSubtypeRange(noticeSubtypes); + this.validationPlan.addNoticeSubtypes(noticeSubtypes.asList()); } @Override public void exitScopeClause(ScopeClauseContext ctx) { - var rule = this.stack.peek(ValidationRule.class); + var rule = this.ruleStack.peek(ValidationRule.class); if (ctx.flag() != null) { rule.setFlag(ctx.flag().flagName.getText()); } @@ -579,19 +729,19 @@ public void exitScopeClause(ScopeClauseContext ctx) { @Override public void exitAssertClause(AssertClauseContext ctx) { var test = this.stack.pop(BooleanExpression.class); - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.pop(ValidationRule.class); var invertedCondition = rule.getInvertedCondition(); rule.setExpression(test, invertedCondition == null ? test : this.combineWithOrParenthesized(test, invertedCondition)); - this.stack.push(new AssertRule(rule)); + this.ruleStack.push(new AssertRule(rule)); } @Override public void exitReportClause(ReportClauseContext ctx) { var test = this.stack.pop(BooleanExpression.class); - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.pop(ValidationRule.class); var invertedCondition = rule.getInvertedCondition(); rule.setExpression(test, invertedCondition == null ? test : this.combineWithOrParenthesized(test, invertedCondition)); - this.stack.push(new ReportRule(rule)); + this.ruleStack.push(new ReportRule(rule)); } /** @@ -601,19 +751,19 @@ public void exitReportClause(ReportClauseContext ctx) { @Override public void exitOtherwiseAssertClause(OtherwiseAssertClauseContext ctx) { var test = this.stack.pop(BooleanExpression.class); - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.pop(ValidationRule.class); var invertedCondition = rule.getInvertedCondition(); rule.setExpression(test, invertedCondition == null ? test : this.combineWithOrParenthesized(test, invertedCondition)); - this.stack.push(new AssertRule(rule)); + this.ruleStack.push(new AssertRule(rule)); } @Override public void exitOtherwiseReportClause(OtherwiseReportClauseContext ctx) { var test = this.stack.pop(BooleanExpression.class); - var rule = this.stack.pop(ValidationRule.class); + var rule = this.ruleStack.pop(ValidationRule.class); var invertedCondition = rule.getInvertedCondition(); rule.setExpression(test, invertedCondition == null ? test : this.combineWithOrParenthesized(test, invertedCondition)); - this.stack.push(new ReportRule(rule)); + this.ruleStack.push(new ReportRule(rule)); } @@ -631,35 +781,35 @@ private BooleanExpression combineWithOrParenthesized(BooleanExpression left, Boo @Override public void enterSimpleRule(SimpleRuleContext ctx) { - this.stack.push(new ValidationRule()); + this.ruleStack.push(new ValidationRule(this.ruleStack.peek(RuleSet.class))); } @Override public void exitSimpleRule(SimpleRuleContext ctx) { - var rule = this.stack.pop(ValidationRule.class); - this.stack.peek(RuleSet.class).addRule(rule); + var rule = this.ruleStack.pop(ValidationRule.class); + this.ruleStack.peek(RuleSet.class).addRule(rule); } @Override public void enterConditionalRule(ConditionalRuleContext ctx) { - this.stack.push(new ValidationRule()); + this.ruleStack.push(new ValidationRule(this.ruleStack.peek(RuleSet.class))); } @Override public void exitConditionalRule(ConditionalRuleContext ctx) { - var rule = this.stack.pop(ValidationRule.class); - this.stack.peek(RuleSet.class).addRule(rule); + var rule = this.ruleStack.pop(ValidationRule.class); + this.ruleStack.peek(RuleSet.class).addRule(rule); } @Override public void enterFallbackRule(FallbackRuleContext ctx) { - this.stack.push(new ValidationRule()); + this.ruleStack.push(new ValidationRule(this.ruleStack.peek(RuleSet.class))); } @Override public void exitFallbackRule(FallbackRuleContext ctx) { - var fallbackRule = this.stack.pop(ValidationRule.class); - var ruleSet = this.stack.pop(RuleSet.class); + var fallbackRule = this.ruleStack.pop(ValidationRule.class); + var ruleSet = this.ruleStack.pop(RuleSet.class); BooleanExpression combined = null; for (ValidationRule rule : ruleSet) { @@ -671,9 +821,125 @@ public void exitFallbackRule(FallbackRuleContext ctx) { fallbackRule.setCondition(combined, invertedCombined, this.combineWithOrParenthesized(fallbackRule.getExpression(), invertedCombined)); ruleSet.setFallbackRule(fallbackRule); - this.stack.push(ruleSet); + this.ruleStack.push(ruleSet); + } + + // #region Dynamic Function Declarations + + private static final java.util.regex.Pattern VALID_ENDPOINT_NAME = java.util.regex.Pattern.compile("[a-zA-Z][a-zA-Z0-9_-]*"); + + @Override + public void exitApiEndpointDeclaration(ApiEndpointDeclarationContext ctx) { + String name = unquote(ctx.endpointName.getText()); + if (!VALID_ENDPOINT_NAME.matcher(name).matches()) { + throw InvalidUsageException.invalidEndpointName(ctx, name); + } + String url = ctx.endpointUrl != null ? unquote(ctx.endpointUrl.getText()) : null; + this.validationPlan.declareEndpoint(name, url); + } + + @Override + public void enterDynamicFunctionDeclaration(DynamicFunctionDeclarationContext ctx) { + this.stack.pushStackFrame(); + this.stack.push(new ParsedParameters()); + } + + @Override + public void exitDynamicFunctionDeclaration(DynamicFunctionDeclarationContext ctx) { + ParsedParameters params = this.stack.pop(ParsedParameters.class); + this.stack.popStackFrame(); + String apiName = ctx.functionName.getText(); + String endpointName = ctx.endpointName != null ? unquote(ctx.endpointName.getText()) : "default"; + if (!this.validationPlan.getEndpoints().containsKey(endpointName)) { + throw InvalidIdentifierException.undeclaredEndpoint(ctx, apiName, endpointName); + } + RuleSeverity errorSeverity = ctx.onErrorClause() != null && ctx.onErrorClause().Warn() != null + ? RuleSeverity.WARNING : RuleSeverity.ERROR; + String errorLabel = ctx.onErrorClause() != null && ctx.onErrorClause().errorLabel != null + ? unquote(ctx.onErrorClause().errorLabel.getText()) : null; + this.stack.declareFunction(new DynamicFunction(apiName, params, endpointName, errorSeverity, errorLabel)); + } + + @Override + public void exitBooleanFunctionInvocation(BooleanFunctionInvocationContext ctx) { + if (this.isApiFunction(ctx.functionInvocation())) { + this.composeAutoGeneratedDynamicVariable(ctx.functionInvocation()); + } else { + super.exitBooleanFunctionInvocation(ctx); + } + } + + @Override + public void exitScalarFromFunctionInvocation(ScalarFromFunctionInvocationContext ctx) { + if (this.isApiFunction(ctx.functionInvocation())) { + this.composeAutoGeneratedDynamicVariable(ctx.functionInvocation()); + } else { + super.exitScalarFromFunctionInvocation(ctx); + } } + private boolean isApiFunction(FunctionInvocationContext ctx) { + return this.stack.getFunction(ctx.functionName.getText()) instanceof DynamicFunction; + } + + private int apiCallCounter = 0; + + /** + * Handles an inline dynamic function call in a rule expression. + * Creates an auto-generated dynamic variable on the current ValidationRule and pushes + * a boolean reference expression ($varName = 1) onto the stack. + */ + private void composeAutoGeneratedDynamicVariable(FunctionInvocationContext ctx) { + String funcName = ctx.functionName.getText(); + if (!this.ruleStack.contains(ValidationRule.class)) { + throw InvalidUsageException.dynamicFunctionOutsideRule(ctx, funcName); + } + + // Resolve the dynamic function and compose the raw dynamic function call. + DynamicFunction apiFunc = (DynamicFunction) this.stack.getFunction(funcName); + List args = this.stack.pop(ParsedArguments.class).getArgumentValues(); + var rawCall = this.script.composeDynamicFunction(apiFunc.endpointName, funcName, args); + + // Compose the variable's expressions: declaration, initialization, and success check ($var == 1). + String varName = "__apiResult" + (++this.apiCallCounter); + var declaration = this.script.composeVariableDeclaration(varName, DynamicExpression.class); + var initExpression = new DynamicExpression(rawCall.getScript()); + var varRef = this.script.composeVariableReference(varName, NumericExpression.class); + var one = this.script.getNumericLiteralEquivalent("1"); + var referenceExpression = this.script.composeComparisonOperation(varRef, "==", one); + + // Create the auto-generated variable and register it on the current rule. + var rule = this.ruleStack.peek(ValidationRule.class); + int index = rule.getAutoGeneratedVariables().size() + 1; + var variable = new DynamicVariable.AutoGenerated(index, varName, declaration, + initExpression, referenceExpression, apiFunc.endpointName, apiFunc.errorSeverity, + apiFunc.errorLabel); + rule.addAutoGeneratedVariable(variable); + + // Push the success check so it can be used in the rule's condition. + this.stack.push(referenceExpression); + } + + @Override + protected void exitParameterDeclaration(ParserRuleContext ctx, String parameterName, + Class parameterType) { + ParsedParameter parameter = new ParsedParameter(parameterName, + this.script.composeParameterReference(parameterName, parameterType)); + this.stack.declareIdentifier(parameter); + this.stack.peek(ParsedParameters.class).add(parameter); + } + + private static String unquote(String literal) { + if (literal.length() >= 2 + && ((literal.startsWith("'") && literal.endsWith("'")) + || (literal.startsWith("\"") && literal.endsWith("\"")))) { + return literal.substring(1, literal.length() - 1); + } + return literal; + } + + // #endregion Dynamic Function Declarations + // #endregion ANTLR Listener Methods } diff --git a/src/main/java/eu/europa/ted/efx/util/EfxRegexValidator.java b/src/main/java/eu/europa/ted/efx/util/EfxRegexValidator.java index f5878f79..aad81641 100644 --- a/src/main/java/eu/europa/ted/efx/util/EfxRegexValidator.java +++ b/src/main/java/eu/europa/ted/efx/util/EfxRegexValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2025 European Union + * Copyright 2026 European Union * * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in diff --git a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java index e8a7afb9..803dfee6 100644 --- a/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java +++ b/src/main/java/eu/europa/ted/efx/xpath/XPathScriptGenerator.java @@ -41,7 +41,6 @@ import eu.europa.ted.efx.model.expressions.scalar.DateLiteral; import eu.europa.ted.efx.model.expressions.scalar.DurationExpression; import eu.europa.ted.efx.model.expressions.scalar.DurationLiteral; -import eu.europa.ted.efx.model.expressions.scalar.NodePath; import eu.europa.ted.efx.model.expressions.scalar.NumericExpression; import eu.europa.ted.efx.model.expressions.scalar.NumericLiteral; import eu.europa.ted.efx.model.expressions.scalar.ScalarExpression; @@ -296,19 +295,6 @@ public T composeParenthesizedExpression(T expression, Cla } } - @Override - public PathExpression composeExternalReference(StringExpression externalReference) { - return new NodePath( - "fn:doc(concat($urlPrefix, " + externalReference.getScript() + "))"); - } - - - @Override - public PathExpression composeFieldInExternalReference(PathExpression externalReference, - PathExpression fieldReference) { - return Expression.instantiate(externalReference.getScript() + fieldReference.getScript(), fieldReference.getClass()); - } - @Override public PathExpression joinPaths(final PathExpression first, final PathExpression second) { @@ -958,6 +944,16 @@ public T composeFunctionInvocation(String functionNa return Expression.instantiate(qualifiedFunctionName + "(" + parameters.stream().map(p -> p.getScript()).collect(Collectors.joining(", ")) + ")", type); } + @Override + public NumericExpression composeDynamicFunction(String endpointName, String apiName, + List arguments) { + StringBuilder sb = new StringBuilder("efx:call-api('"); + sb.append(endpointName).append("', '").append(apiName).append("', ("); + sb.append(arguments.stream().map(TypedExpression::getScript).collect(Collectors.joining(", "))); + sb.append("))"); + return new NumericExpression(sb.toString()); + } + //#region Helpers ----------------------------------------------------------- diff --git a/src/main/resources/freemarker/schematron/complete-validation.ftl b/src/main/resources/freemarker/schematron/complete-validation.ftl index 21a1b9bb..65a09300 100644 --- a/src/main/resources/freemarker/schematron/complete-validation.ftl +++ b/src/main/resources/freemarker/schematron/complete-validation.ftl @@ -4,12 +4,13 @@ Parameters: title - Schema title (e.g., "eForms validation (dynamic)") - globalVariables - List of schema-level variables + params - List of API endpoint parameters + letElements - List of schema-level let element declarations phases - List defining validation phases per notice type diagnostics - List for subject path information includes - List of pattern file paths to include --> - + xmlns:efx="http://eforms.ted.europa.eu/efx" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" queryBinding="xslt2"> ${title} @@ -26,10 +27,45 @@ +<#if params?has_content> + + + <#-- API endpoint parameters (schema-level becomes via SchXSLT, overridable at runtime) --> +<#list params as param> + + + + + + + + + +<#list params as param> + + + + + + + + + + + + <#-- Global variables from schema-level LET statements --> -<#list globalVariables as variable> - +<#list letElements as letElement> + <#-- Phases for each notice type --> diff --git a/src/main/resources/freemarker/schematron/pattern.ftl b/src/main/resources/freemarker/schematron/pattern.ftl index 680bbd0a..7aafa950 100644 --- a/src/main/resources/freemarker/schematron/pattern.ftl +++ b/src/main/resources/freemarker/schematron/pattern.ftl @@ -4,22 +4,27 @@ Parameters: id - Pattern identifier (e.g., "validation-stage-1a-1") - variables - List of pattern-level variables - rules - List containing the validation rules - tags - List of tags specifying which tests to include + letElements - List of pattern-level let element declarations + rules - List containing the validation rules + tags - List of tags specifying which tests to include --> -<#list variables as variable> - +<#list letElements as letElement> + <#list rules as rule> -<#if rule.hasTestsForTags(tags)> +<#if rule.hasTestsFor(tags)> - <#list rule.variables as variable> - + <#list rule.letElements as letElement> + <#if tags?seq_contains(letElement.tag)> + + <#list rule.tests as test> <#if tags?seq_contains(test.tag)> + <#if test.letElement?? && tags?seq_contains(test.letElement.tag)> + + <${test.elementName} id="${test.id}" role="${test.role}"<#if test.flag??> flag="${test.flag}"<#if test.diagnostic??> diagnostics="${test.diagnostic.id}" test="${test.test?xml?replace("'", "'")}">${test.message} diff --git a/src/test/java/eu/europa/ted/efx/model/rules/ValidationRuleTest.java b/src/test/java/eu/europa/ted/efx/model/rules/ValidationRuleTest.java new file mode 100644 index 00000000..90750ccd --- /dev/null +++ b/src/test/java/eu/europa/ted/efx/model/rules/ValidationRuleTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2026 European Union + * + * Licensed under the EUPL, Version 1.2 or – as soon they will be approved by the European + * Commission – subsequent versions of the EUPL (the "Licence"); You may not use this work except in + * compliance with the Licence. You may obtain a copy of the Licence at: + * https://joinup.ec.europa.eu/software/page/eupl + * + * Unless required by applicable law or agreed to in writing, software distributed under the Licence + * is distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the Licence for the specific language governing permissions and limitations under + * the Licence. + */ +package eu.europa.ted.efx.model.rules; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import eu.europa.ted.efx.model.expressions.scalar.BooleanExpression; +import eu.europa.ted.efx.model.expressions.scalar.DynamicExpression; +import eu.europa.ted.efx.model.variables.DynamicVariable; + +class ValidationRuleTest { + + private DynamicVariable createDynamicVariable(final String name) { + return new DynamicVariable(name, + new BooleanExpression(""), + new DynamicExpression(""), + new BooleanExpression("$" + name + " = 1"), + "default", RuleSeverity.ERROR, null); + } + + private final RuleSet testRuleSet = new RuleSet(new ValidationStage("test", new ValidationPlan())); + + private ValidationRule createRuleWithExpression(final String script) { + ValidationRule rule = new ValidationRule(this.testRuleSet); + rule.setExpression(new BooleanExpression(script), new BooleanExpression(script)); + return rule; + } + + @Test + void findReferencedVariables_exactMatch() { + ValidationRule rule = this.createRuleWithExpression("$foo = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertEquals(List.of(foo), rule.findReferencedVariables(List.of(foo))); + } + + @Test + void findReferencedVariables_noMatch() { + ValidationRule rule = this.createRuleWithExpression("$bar = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertTrue(rule.findReferencedVariables(List.of(foo)).isEmpty()); + } + + @Test + void findReferencedVariables_doesNotMatchPrefix() { + ValidationRule rule = this.createRuleWithExpression("$foobar = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertTrue(rule.findReferencedVariables(List.of(foo)).isEmpty()); + } + + @Test + void findReferencedVariables_doesNotMatchWithTrailingDigit() { + ValidationRule rule = this.createRuleWithExpression("$foo10 = 1"); + DynamicVariable foo1 = this.createDynamicVariable("foo1"); + + assertTrue(rule.findReferencedVariables(List.of(foo1)).isEmpty()); + } + + @Test + void findReferencedVariables_doesNotMatchWithTrailingUnderscore() { + ValidationRule rule = this.createRuleWithExpression("$foo_bar = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertTrue(rule.findReferencedVariables(List.of(foo)).isEmpty()); + } + + @Test + void findReferencedVariables_doesNotMatchWithTrailingDash() { + ValidationRule rule = this.createRuleWithExpression("$foo-bar = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertTrue(rule.findReferencedVariables(List.of(foo)).isEmpty()); + } + + @Test + void findReferencedVariables_matchesAtEndOfScript() { + ValidationRule rule = this.createRuleWithExpression("something or $foo"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertEquals(List.of(foo), rule.findReferencedVariables(List.of(foo))); + } + + @Test + void findReferencedVariables_matchesFollowedByOperator() { + ValidationRule rule = this.createRuleWithExpression("($foo=1) or ($bar=1)"); + DynamicVariable foo = this.createDynamicVariable("foo"); + DynamicVariable bar = this.createDynamicVariable("bar"); + + assertEquals(List.of(foo, bar), rule.findReferencedVariables(List.of(foo, bar))); + } + + @Test + void findReferencedVariables_matchesFollowedByParenthesis() { + ValidationRule rule = this.createRuleWithExpression("not($foo)"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertEquals(List.of(foo), rule.findReferencedVariables(List.of(foo))); + } + + @Test + void findReferencedVariables_matchesFollowedBySpace() { + ValidationRule rule = this.createRuleWithExpression("$foo = 1"); + DynamicVariable foo = this.createDynamicVariable("foo"); + + assertEquals(List.of(foo), rule.findReferencedVariables(List.of(foo))); + } + + @Test + void findReferencedVariables_autoGeneratedNamePartialMatch() { + ValidationRule rule = this.createRuleWithExpression("($__apiResult10 = -1) or ($__apiResult1 = 1)"); + DynamicVariable result1 = this.createDynamicVariable("__apiResult1"); + DynamicVariable result10 = this.createDynamicVariable("__apiResult10"); + + assertEquals(List.of(result1, result10), rule.findReferencedVariables(List.of(result1, result10))); + } + + @Test + void findReferencedVariables_emptyList() { + ValidationRule rule = this.createRuleWithExpression("$foo = 1"); + + assertTrue(rule.findReferencedVariables(List.of()).isEmpty()); + } + + @Test + void findReferencedVariables_selectsOnlyReferenced() { + ValidationRule rule = this.createRuleWithExpression("$alpha = 1 and $gamma = 1"); + DynamicVariable alpha = this.createDynamicVariable("alpha"); + DynamicVariable beta = this.createDynamicVariable("beta"); + DynamicVariable gamma = this.createDynamicVariable("gamma"); + + assertEquals(List.of(alpha, gamma), rule.findReferencedVariables(List.of(alpha, beta, gamma))); + } +} 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 6ec00e8f..e7f79261 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxExpressionTranslatorV2Test.java @@ -1239,19 +1239,6 @@ void testFieldReferenceWithPredicate_WithFieldReferenceInPredicate() { "BT-00-Indicator[BT-00-Code == 'a']"); } - @Test - void testFieldReferenceInOtherNotice() { - testExpressionTranslationWithContext( - "fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/TextField/normalize-space(text())", - "ND-Root", "notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Text"); - } - - @Test - void testFieldReferenceInOtherNotice_UsingAReference() { - testExpressionTranslationWithContext( - "fn:doc(concat($urlPrefix, /*/PathNode/IdField/normalize-space(text())))/*/PathNode/TextField/normalize-space(text())", - "ND-Root", "notice(BT-00-Identifier)/BT-00-Text"); - } @Test void testFieldReferenceWithFieldContextOverride() { @@ -3897,33 +3884,6 @@ void testVariableContextOverride_InAttributeContext() { "for context:$n in ND-Root return $n::BT-00-Code/@listName"); } - // Row 12 SEQ - @Test - void testFieldReferenceInOtherNotice_InSequenceContext() { - testExpressionTranslationWithContext( - "'test' = fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/TextField/normalize-space(text())", - "ND-Root", - "'test' in notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Text"); - } - - // Row 12 PRES - @Test - void testFieldReferenceInOtherNotice_InPresenceCondition() { - testExpressionTranslationWithContext( - "fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/TextField", - "ND-Root", - "notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Text is present"); - } - - // Row 12 ATTR - @Test - void testFieldReferenceInOtherNotice_InAttributeContext() { - testExpressionTranslationWithContext( - "fn:doc(concat($urlPrefix, 'da4d46e9-490b-41ff-a2ae-8166d356a619'))/*/PathNode/CodeField/@listName", - "ND-Root", - "notice('da4d46e9-490b-41ff-a2ae-8166d356a619')/BT-00-Code/@listName"); - } - // Row 13 SEQ @Test void testStackedOverride_NodeAndField_InSequenceContext() { diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java index 97d2feea..1720831f 100644 --- a/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java +++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test.java @@ -39,9 +39,13 @@ import org.junit.jupiter.api.io.TempDir; import eu.europa.ted.efx.EfxTestsBase; import eu.europa.ted.efx.EfxTranslatorOptions; +import eu.europa.ted.efx.exceptions.InvalidArgumentException; +import eu.europa.ted.efx.exceptions.InvalidIdentifierException; import eu.europa.ted.efx.exceptions.InvalidUsageException; import eu.europa.ted.efx.exceptions.ThrowingErrorListener; +import eu.europa.ted.efx.exceptions.TypeMismatchException; import eu.europa.ted.efx.interfaces.IncludedFileResolver; +import eu.europa.ted.efx.interfaces.TranslatorOptions; import eu.europa.ted.efx.mock.DependencyFactoryMock; import eu.europa.ted.efx.model.DecimalFormat; import eu.europa.ted.efx.model.rules.NoticeSubtypeRange; @@ -255,31 +259,7 @@ void testInClause_MixedAllAndSpecific() throws IOException { String testName = "testInClause_MixedAllAndSpecific"; Map outputFiles = translator.translateRules(readInput(testName)); - // 1 shared + 2 specific per config (dynamic + static) + 2 complete-validation + schematrons.json assertEquals(9, outputFiles.size(), "Should generate 9 files"); - - // Shared pattern exists (no subtype suffix) - assertTrue(outputFiles.containsKey("dynamic/validation-stage-1a.sch")); - // Subtype-specific patterns exist - assertTrue(outputFiles.containsKey("dynamic/validation-stage-1a-1.sch")); - assertTrue(outputFiles.containsKey("dynamic/validation-stage-1a-2.sch")); - - // Shared pattern should contain R-K7P-M2Q (the IN * rule) - String sharedPattern = outputFiles.get("dynamic/validation-stage-1a.sch"); - assertTrue(sharedPattern.contains("R-K7P-M2Q")); - assertFalse(sharedPattern.contains("R-X3F-N8W"), "Specific rule should not be in shared pattern"); - - // Specific patterns should contain R-X3F-N8W but not R-K7P-M2Q - String specific1 = outputFiles.get("dynamic/validation-stage-1a-1.sch"); - assertTrue(specific1.contains("R-X3F-N8W")); - assertFalse(specific1.contains("R-K7P-M2Q"), "Shared rule should not be in specific pattern"); - - // Complete validation should reference both shared and specific patterns in phases - String completeValidation = outputFiles.get("dynamic/complete-validation.sch"); - String phase1 = extractPhase(completeValidation, "eforms-1"); - assertTrue(phase1.contains("EFORMS-validation-stage-1a\""), "Phase 1 should include shared pattern"); - assertTrue(phase1.contains("EFORMS-validation-stage-1a-1"), "Phase 1 should include specific pattern"); - assertAllOutputs(testName, outputFiles); } @@ -341,13 +321,6 @@ void testVariable_Global_AppearsBeforeIncludes() throws IOException { assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); assertAllOutputs(testName, outputFiles); - - // Verify global variables appear before elements in complete-validation.sch - String completeValidation = outputFiles.get("dynamic/complete-validation.sch"); - int letIndex = completeValidation.indexOf(" elements"); } @Test @@ -357,16 +330,6 @@ void testVariable_StageLevel() throws IOException { assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); assertAllOutputs(testName, outputFiles); - - // Verify each pattern has its own variable with the correct value - String stage1a = outputFiles.get("dynamic/validation-stage-1a-1.sch"); - String stage1b = outputFiles.get("dynamic/validation-stage-1b-1.sch"); - - assertTrue(stage1a.contains("name=\"stageVar\""), "Stage 1a should have stageVar variable"); - assertTrue(stage1a.contains(""first""), "Stage 1a stageVar should have value 'first'"); - - assertTrue(stage1b.contains("name=\"stageVar\""), "Stage 1b should have stageVar variable"); - assertTrue(stage1b.contains(""second""), "Stage 1b stageVar should have value 'second'"); } //#endregion Variable tests (output verification) @@ -518,35 +481,6 @@ void testInClause_PhaseGeneration() throws IOException { assertEquals(19, outputFiles.size(), "Should generate exactly 19 files"); assertAllOutputs(testName, outputFiles); - - // Verify phase generation logic - String completeValidation = outputFiles.get("dynamic/complete-validation.sch"); - - assertTrue(completeValidation.contains("")); - assertTrue(completeValidation.contains("")); - assertTrue(completeValidation.contains("")); - assertFalse(completeValidation.contains("")); - - String phase1 = extractPhase(completeValidation, "eforms-1"); - assertTrue(phase1.contains("validation-stage-1")); - assertTrue(phase1.contains("validation-stage-2")); - assertTrue(phase1.contains("validation-stage-3")); - - String phase2 = extractPhase(completeValidation, "eforms-2"); - assertTrue(phase2.contains("validation-stage-1")); - assertTrue(phase2.contains("validation-stage-2")); - assertFalse(phase2.contains("validation-stage-3")); - - String phase3 = extractPhase(completeValidation, "eforms-3"); - assertTrue(phase3.contains("validation-stage-1")); - assertTrue(phase3.contains("validation-stage-2")); - assertTrue(phase3.contains("validation-stage-3")); - } - - private String extractPhase(String content, String phaseId) { - int start = content.indexOf(""); - int end = content.indexOf("", start); - return content.substring(start, end); } //#region Comprehensive/Integration tests @@ -556,35 +490,8 @@ void testOutput_FromSampleRulesFile() throws IOException { String testName = "testOutput_FromSampleRulesFile"; Map outputFiles = translator.translateRules(readInput(testName)); - assertFalse(outputFiles.isEmpty(), "Output files should not be empty"); assertEquals(15, outputFiles.size(), "Should generate exactly 15 files"); - - // Verify we have the expected files - assertTrue(outputFiles.containsKey("dynamic/complete-validation.sch")); - assertTrue(outputFiles.containsKey("static/complete-validation.sch")); - assertTrue(outputFiles.containsKey("schematrons.json")); - - // Stages with only IN * rules produce shared patterns (no subtype suffix) - assertTrue(outputFiles.containsKey("dynamic/validation-stage-1a.sch")); - assertTrue(outputFiles.containsKey("dynamic/validation-stage-2a.sch")); - assertTrue(outputFiles.containsKey("dynamic/validation-stage-3a.sch")); - assertTrue(outputFiles.containsKey("static/validation-stage-1a.sch")); - - // Stage 1b has subtype-specific rules - assertTrue(outputFiles.keySet().stream().anyMatch(f -> f.startsWith("dynamic/validation-stage-1b-"))); - - // Verify XML well-formedness for all .sch files - for (Map.Entry entry : outputFiles.entrySet()) { - if (entry.getKey().endsWith(".sch")) { - assertValidXml(entry.getValue(), entry.getKey()); - } - } - - // Verify schematrons.json structure - String schematronsJson = outputFiles.get("schematrons.json"); - assertTrue(schematronsJson.contains("\"schematrons\"")); - assertTrue(schematronsJson.contains("\"type\" : \"dynamic\"")); - assertTrue(schematronsJson.contains("\"type\" : \"static\"")); + assertAllOutputs(testName, outputFiles); } @Test @@ -694,4 +601,207 @@ void testInclude_FromString_IOFailurePropagates() { } //#endregion Include directive tests + + //#region API call tests + + @Test + void testApiCall_SimpleEndpointAndFunction() throws IOException { + String testName = "testApiCall_SimpleEndpointAndFunction"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_DefaultEndpointName() throws IOException { + String testName = "testApiCall_DefaultEndpointName"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size(), "Should generate exactly 5 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_NamedEndpoint() throws IOException { + String testName = "testApiCall_NamedEndpoint"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size(), "Should generate exactly 5 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_EndpointWithoutUrl() throws IOException { + String testName = "testApiCall_EndpointWithoutUrl"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size(), "Should generate exactly 5 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_MultipleArguments() throws IOException { + String testName = "testApiCall_MultipleArguments"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size(), "Should generate exactly 5 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_UndeclaredFunction_ThrowsError() throws IOException { + String rules = readInput("testApiCall_UndeclaredFunction_ThrowsError"); + InvalidIdentifierException exception = assertThrows(InvalidIdentifierException.class, + () -> translator.translateRules(rules)); + assertEquals(InvalidIdentifierException.ErrorCode.UNDECLARED_IDENTIFIER, exception.getErrorCode()); + } + + @Test + void testApiCall_UndeclaredEndpoint_ThrowsError() throws IOException { + String rules = readInput("testApiCall_UndeclaredEndpoint_ThrowsError"); + InvalidIdentifierException exception = assertThrows(InvalidIdentifierException.class, + () -> translator.translateRules(rules)); + assertEquals(InvalidIdentifierException.ErrorCode.UNDECLARED_ENDPOINT, exception.getErrorCode()); + } + + @Test + void testApiCall_CompositeVariableInitializer_ThrowsError() throws IOException { + // The grammar enforces that dynamicVariableInitializer only accepts a single function + // invocation, so a compound expression is rejected at parse time. + String rules = readInput("testApiCall_CompositeVariableInitializer_ThrowsError"); + assertThrows(Exception.class, () -> translator.translateRules(rules)); + } + + @Test + void testApiCall_WrongArgumentCount_ThrowsError() throws IOException { + String rules = readInput("testApiCall_WrongArgumentCount_ThrowsError"); + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> translator.translateRules(rules)); + assertEquals(InvalidArgumentException.ErrorCode.ARGUMENT_NUMBER_MISMATCH, exception.getErrorCode()); + } + + @Test + void testApiCall_WrongArgumentType_ThrowsError() throws IOException { + String rules = readInput("testApiCall_WrongArgumentType_ThrowsError"); + InvalidArgumentException exception = assertThrows(InvalidArgumentException.class, + () -> translator.translateRules(rules)); + assertEquals(InvalidArgumentException.ErrorCode.ARGUMENT_TYPE_MISMATCH, exception.getErrorCode()); + } + + @Test + void testApiCall_TranslatorReuse() throws IOException { + // Verify that calling translateRules twice on the same translator instance works. + String rules = readInput("testApiCall_Comprehensive"); + translator.translateRules(rules); + // Second call should not fail with "already declared" errors. + Map outputFiles = translator.translateRules(rules); + assertEquals(11, outputFiles.size()); + } + + @Test + void testApiCall_ViaInclude() throws IOException { + String testName = "testApiCall_ViaInclude"; + IncludedFileResolver resolver = path -> readExpected(testName, path); + TranslatorOptions options = TranslatorOptions.withResolver(EfxTranslatorOptions.DEFAULT, resolver); + + Map outputFiles = translator.translateRules(readInput(testName), options); + + assertEquals(4, outputFiles.size(), "Should generate exactly 4 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_InBooleanExpression() throws IOException { + String testName = "testApiCall_InBooleanExpression"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size(), "Should generate exactly 5 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_TypeMismatch_InArithmeticExpression() throws IOException { + String rules = readInput("testApiCall_TypeMismatch_InArithmeticExpression"); + TypeMismatchException exception = assertThrows(TypeMismatchException.class, + () -> translator.translateRules(rules)); + assertEquals(TypeMismatchException.ErrorCode.CANNOT_CONVERT, exception.getErrorCode()); + } + + @Test + void testApiCall_TwoApiCallsInExpression() throws IOException { + String testName = "testApiCall_TwoApiCallsInExpression"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_OnErrorReject() throws IOException { + String testName = "testApiCall_OnErrorReject"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_CustomErrorLabels() throws IOException { + String testName = "testApiCall_CustomErrorLabels"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_ReportRule() throws IOException { + String testName = "testApiCall_ReportRule"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(7, outputFiles.size(), "Should generate exactly 7 files"); + assertAllOutputs(testName, outputFiles); + } + + /** + * Comprehensive test exercising all API call features together: + * - 3 endpoints (with URL, with URL, without URL) + * - 4 API functions across multiple endpoints + * - 2 stages with multiple rules per stage + * - Mix of static and dynamic rules + * - Mix of ASSERT and REPORT + * - Mix of ON ERROR WARN / REJECT + * - Mix of default and custom error labels + * - Single and multiple API calls per rule + * - Multiple function arguments + */ + @Test + void testApiCall_Comprehensive() throws IOException { + String testName = "testApiCall_Comprehensive"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(11, outputFiles.size(), "Should generate exactly 11 files"); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_ResultInVariable() throws IOException { + String testName = "testApiCall_ResultInVariable"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(9, outputFiles.size()); + assertAllOutputs(testName, outputFiles); + } + + @Test + void testApiCall_IndirectDynamicDependency() throws IOException { + String testName = "testApiCall_IndirectDynamicDependency"; + Map outputFiles = translator.translateRules(readInput(testName)); + + assertEquals(5, outputFiles.size()); + assertAllOutputs(testName, outputFiles); + } + + //#endregion API call tests } diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CompositeVariableInitializer_ThrowsError/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CompositeVariableInitializer_ThrowsError/input.efx new file mode 100644 index 00000000..4f04c668 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CompositeVariableInitializer_ThrowsError/input.efx @@ -0,0 +1,14 @@ +// Intentionally invalid: API call nested inside a compound boolean expression +// in a variable initializer. This is not supported because the tri-state error +// handling requires the raw efx:call-api(...) as the entire variable value. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +WITH dynamic : $ok = ?check(BT-00-Identifier) and BT-00-Indicator is present, BT-00-Text + ASSERT $ok + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/complete-validation.sch new file mode 100644 index 00000000..1645b49c --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/complete-validation.sch @@ -0,0 +1,71 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..46b400f0 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,43 @@ + + + + + + rule|text|R-S1A-001 + rule|text|R-S1A-002 + + rule|text|api-error + rule|text|R-S1A-003 + + rule|text|api-error + + rule|text|api-warning + rule|text|R-S1A-004 + + auxiliary|text|status-unavailable + rule|text|R-S1A-005 + rule|text|api-error + + rule|text|api-warning + rule|text|R-S1A-006 + + + rule|text|api-error + auxiliary|text|status-unavailable + rule|text|R-S1A-007 + rule|text|api-error + auxiliary|text|status-unavailable + rule|text|R-S1A-008 + auxiliary|text|status-unavailable + + rule|text|api-error + rule|text|R-S1A-009 + + rule|text|api-error + rule|text|R-S1A-010 + auxiliary|text|status-unavailable + rule|text|R-S1A-011 + rule|text|api-error + rule|text|R-S1A-012 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..3d6bdf47 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,27 @@ + + + + + + rule|text|R-S1A-002 + + rule|text|api-error + + rule|text|api-warning + rule|text|R-S1A-004 + + auxiliary|text|status-unavailable + rule|text|R-S1A-005 + + + rule|text|api-error + auxiliary|text|status-unavailable + rule|text|R-S1A-008 + auxiliary|text|status-unavailable + + rule|text|api-error + rule|text|R-S1A-009 + auxiliary|text|status-unavailable + rule|text|R-S1A-011 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..cb4db298 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,23 @@ + + + + rule|text|R-S2A-001 + + auxiliary|text|ref-check-failed + + rule|text|api-error + rule|text|R-S2A-002 + + rule|text|api-warning + + auxiliary|text|status-unavailable + rule|text|R-S2A-003 + + + rule|text|R-S2A-004 + rule|text|api-error + + auxiliary|text|ref-check-failed + rule|text|R-S2A-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-2.sch new file mode 100644 index 00000000..8bdf15b2 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/dynamic/validation-stage-2a-2.sch @@ -0,0 +1,17 @@ + + + + rule|text|R-S2A-001 + + rule|text|api-warning + + auxiliary|text|status-unavailable + rule|text|R-S2A-003 + + + rule|text|api-error + + auxiliary|text|ref-check-failed + rule|text|R-S2A-005 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/input.efx new file mode 100644 index 00000000..4cce31dd --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/input.efx @@ -0,0 +1,130 @@ +// Comprehensive test: exercises all API call features in a realistic scenario. +// - 3 endpoints: default (with URL), ext-db (with URL), staging (no URL) +// - 5 API functions across multiple endpoints +// - 2 stages, 2 contexts per stage, static + dynamic mixed per context +// - Mix of ASSERT and REPORT +// - Mix of ON ERROR WARN / REJECT, default and custom error labels +// - Single and multiple API calls per rule +// - Multiple arguments +// - API variables at global, stage, and WITH levels +// - WHEN clauses with API calls and API variables as conditions +// - Report with API variable guards +// - Guard filtering: only referenced API variables appear in guards +// - Varied IN clauses: single subtype (IN 1), range (IN 1-2), multiple subtypes (IN 1, 2) + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; +ENDPOINT 'ext-db' AT 'https://other.api.eu/v2'; +ENDPOINT 'staging'; + +LET dynamic : ?check-buyer(text : $buyerId) CALL API; +LET dynamic : ?confirm-status(text : $statusId) CALL API, ON ERROR WARN 'auxiliary|text|status-unavailable'; +LET dynamic : ?verify-code(text : $codeId, text : $code) CALL API 'ext-db', ON ERROR WARN; +LET dynamic : ?validate-ref(text : $ref) CALL API 'staging', ON ERROR REJECT 'auxiliary|text|ref-check-failed'; +LET dynamic : ?check-global(text : $val) CALL API; + +// Global API variable +LET dynamic : $globalCheck = ?check-global(BT-00-Code); + +---- STAGE 1a ---- + +// Stage-level API variable +LET dynamic : $stageStatus = ?confirm-status(BT-00-Code); + +// Context 1: WITH-level API variable + inline calls, mixed with static rules +WITH dynamic : $buyerOk = ?check-buyer(BT-00-Identifier), BT-00-Text + // Static assert — single subtype + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Static report — range of subtypes + REPORT empty(BT-00-Text) + AS WARNING R-S1A-002 + FOR BT-00-Text IN 1-2 + + // Dynamic assert — single inline API call, single subtype + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert — two inline API calls, range + ASSERT ?check-buyer(BT-00-Identifier) and ?verify-code(BT-00-Identifier, BT-00-Code) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1-2 + + // Dynamic report — inline API call with custom warning label, multiple subtypes + REPORT not(?confirm-status(BT-00-Identifier)) + AS WARNING R-S1A-005 + FOR BT-00-Text IN 1, 2 + + // Dynamic assert — WITH-level API variable + inline call, single subtype + ASSERT $buyerOk and ?verify-code(BT-00-Identifier, BT-00-Code) + AS ERROR R-S1A-006 + FOR BT-00-Text IN 1 + +// Context 2: outer API variables, WHEN clauses, report guards +WITH BT-00-Text + // Dynamic assert — stage + global API variables only, single subtype + ASSERT $stageStatus and $globalCheck + AS ERROR R-S1A-007 + FOR BT-00-Text IN 1 + + // Dynamic report — API variable guards, range + REPORT not($stageStatus) and not($globalCheck) + AS WARNING R-S1A-008 + FOR BT-00-Text IN 1-2 + + // Dynamic assert with WHEN — combined variable + inline call, multiple subtypes + WHEN BT-00-Indicator is present + ASSERT $stageStatus and ?check-buyer(BT-00-Identifier) + AS ERROR R-S1A-009 + FOR BT-00-Text IN 1, 2 + + // Dynamic assert with WHEN condition that IS an API call + WHEN ?check-buyer(BT-00-Identifier) + ASSERT BT-00-Text is present + AS ERROR R-S1A-010 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN condition that references an API variable + WHEN $stageStatus + ASSERT BT-00-Text is present + AS ERROR R-S1A-011 + FOR BT-00-Text IN 1-2 + + // Dynamic assert — only globalCheck guard (tests filtering), single subtype + ASSERT $globalCheck + AS ERROR R-S1A-012 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1: cross-endpoint calls, static + dynamic mixed +WITH BT-00-Text + // Static assert — range + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1-2 + + // Dynamic assert — two API calls from different endpoints, single subtype + ASSERT ?validate-ref(BT-00-Identifier) and ?check-buyer(BT-00-Identifier) + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + + // Dynamic report — two API calls, multiple subtypes + REPORT not(?verify-code(BT-00-Identifier, BT-00-Code)) and not(?confirm-status(BT-00-Identifier)) + AS INFO R-S2A-003 + FOR BT-00-Text IN 1, 2 + +// Context 2: WHEN clause with API call from different context node +WITH ND-SubNode + // Static report — single subtype + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert with WHEN — API call + global var, range + WHEN BT-00-Indicator is present + ASSERT ?validate-ref(BT-00-Identifier) and $globalCheck + AS ERROR R-S2A-005 + FOR BT-01-SubLevel-Text IN 1-2 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/schematrons.json new file mode 100644 index 00000000..e0b05d13 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/schematrons.json @@ -0,0 +1,51 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "validation-stage-2a-2", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-2.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + }, { + "name" : "validation-stage-2a-2", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-2.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/complete-validation.sch new file mode 100644 index 00000000..03184687 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/complete-validation.sch @@ -0,0 +1,38 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..ddc3930c --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-1.sch @@ -0,0 +1,7 @@ + + + + rule|text|R-S1A-001 + rule|text|R-S1A-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..1ea97c05 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-002 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..bcb5087a --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-2.sch new file mode 100644 index 00000000..914f7e49 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_Comprehensive/static/validation-stage-2a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S2A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/complete-validation.sch new file mode 100644 index 00000000..606811bc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/complete-validation.sch @@ -0,0 +1,60 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..e7da903b --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,19 @@ + + + + rule|text|R-S1A-001 + + rule|text|api-error + + auxiliary|text|custom-warning + + auxiliary|text|custom-error + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + + auxiliary|text|custom-warning + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..9f4a07c5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + + auxiliary|text|custom-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + + rule|text|api-error + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/input.efx new file mode 100644 index 00000000..bd26d41e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/input.efx @@ -0,0 +1,64 @@ +// Test: Custom error labels on API function declarations. +// Verifies that each companion error assert uses the custom label from the declaration: +// - No error clause: default label (rule|text|api-error) +// - ON ERROR WARN 'label': role=WARNING, message=label +// - ON ERROR REJECT 'label': role=ERROR, message=label + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-default(text : $id-a) CALL API; +LET dynamic : ?check-warn(text : $id-b) CALL API, ON ERROR WARN 'auxiliary|text|custom-warning'; +LET dynamic : ?check-reject(text : $id-c) CALL API, ON ERROR REJECT 'auxiliary|text|custom-error'; + +---- STAGE 1a ---- + +// Context 1: all three error modes in one expression +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert — three API calls with different error labels + ASSERT ?check-default(BT-00-Identifier) and ?check-warn(BT-00-Identifier) and ?check-reject(BT-00-Identifier) + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: WHEN clause with custom-label API call +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN — custom warn label + WHEN BT-00-Indicator is present + ASSERT ?check-warn(BT-00-Identifier) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1 +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert — custom reject label + ASSERT ?check-reject(BT-00-Identifier) + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2 +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert — default label + ASSERT ?check-default(BT-00-Identifier) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/schematrons.json new file mode 100644 index 00000000..af577a36 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/complete-validation.sch new file mode 100644 index 00000000..bd58d1ed --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/complete-validation.sch @@ -0,0 +1,32 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_CustomErrorLabels/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/complete-validation.sch new file mode 100644 index 00000000..70d43caf --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..c6d131db --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,11 @@ + + + + rule|text|R-S1A-001 + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/input.efx new file mode 100644 index 00000000..06b27032 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/input.efx @@ -0,0 +1,18 @@ +// Test: API function without explicit endpoint name defaults to 'default'. +// Verifies that CALL API (without endpoint) binds to the 'default' endpoint. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/schematrons.json new file mode 100644 index 00000000..c8f0b9a5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..84e0fd29 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_DefaultEndpointName/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/complete-validation.sch new file mode 100644 index 00000000..c6117a08 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..dfc18ef1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,11 @@ + + + + rule|text|R-S1A-001 + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/input.efx new file mode 100644 index 00000000..f130db63 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/input.efx @@ -0,0 +1,18 @@ +// Test: Endpoint declared without a URL. +// Verifies that ENDPOINT without AT clause produces an empty apiUrl param value. + +ENDPOINT 'staging'; + +LET dynamic : ?check(text : $id) CALL API 'staging'; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/schematrons.json new file mode 100644 index 00000000..c8f0b9a5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..84e0fd29 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_EndpointWithoutUrl/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/complete-validation.sch new file mode 100644 index 00000000..70d43caf --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..6dba3b5e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,11 @@ + + + + rule|text|R-S1A-001 + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/input.efx new file mode 100644 index 00000000..5af7c9b4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/input.efx @@ -0,0 +1,20 @@ +// Test: API call used as part of a larger boolean expression in an ASSERT. +// Verifies that an inline API call inside "... and not(?check(...))" is correctly +// extracted as a binding variable and guarded for errors. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + +// API call nested inside a compound boolean expression +WITH BT-00-Text + ASSERT BT-00-Text is present and not(?check(BT-00-Identifier)) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/schematrons.json new file mode 100644 index 00000000..c8f0b9a5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..84e0fd29 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_InBooleanExpression/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/complete-validation.sch new file mode 100644 index 00000000..70d43caf --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..fd2f0190 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,13 @@ + + + + + + + rule|text|api-error + rule|text|R-S1A-001 + rule|text|api-error + rule|text|R-S1A-002 + rule|text|R-S1A-003 + + \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/input.efx new file mode 100644 index 00000000..4ee8c051 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/input.efx @@ -0,0 +1,38 @@ +// Test: indirect dynamic dependencies through regular variable aliases. +// Verifies that guard asserts and DYNAMIC tagging propagate transitively. +// +// Scenarios: +// 1. Direct alias: LET indicator : $alias = $dynamicVar +// 2. Transitive alias: LET indicator : $alias2 = $alias (where $alias depends on $dynamicVar) +// 3. Combined: LET indicator : $combined = $dynamicVar and BT-00-Text is present +// 4. Static variable (no dynamic dependency) — should remain STATIC + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-buyer(text : $buyerId) CALL API; + +---- STAGE 1a ---- + +LET dynamic : $apiResult = ?check-buyer(BT-00-Identifier); + +// Combined expression with dynamic and static parts +LET indicator : $alias = $apiResult and BT-00-Text is present; + +// Transitive: depends on $alias which depends on $apiResult +LET indicator : $alias2 = $alias or BT-00-Indicator is present; + +WITH BT-00-Text + // Rule using direct alias — needs guard for $apiResult + ASSERT $alias + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Rule using transitive alias — also needs guard for $apiResult + ASSERT $alias2 + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + + // Static rule — no dynamic dependency, should appear in static bundle + ASSERT BT-00-Text is present + AS ERROR R-S1A-003 + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/schematrons.json new file mode 100644 index 00000000..4640e6e6 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..ec9fac6a --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_IndirectDynamicDependency/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-003 + + \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/complete-validation.sch new file mode 100644 index 00000000..70d43caf --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..3413542d --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,11 @@ + + + + rule|text|R-S1A-001 + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/input.efx new file mode 100644 index 00000000..a6692bdb --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/input.efx @@ -0,0 +1,19 @@ +// Test: API function with multiple parameters. +// Verifies that a function declared with two parameters correctly receives +// and passes both arguments through to the efx:call-api output. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-multi(text : $buyerId, text : $code) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + +WITH BT-00-Text + ASSERT ?check-multi(BT-00-Identifier, BT-00-Code) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/schematrons.json new file mode 100644 index 00000000..c8f0b9a5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..84e0fd29 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_MultipleArguments/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/complete-validation.sch new file mode 100644 index 00000000..f5a73dc0 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..5c8ddbc5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,11 @@ + + + + rule|text|R-S1A-001 + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/input.efx new file mode 100644 index 00000000..1dfeaff4 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/input.efx @@ -0,0 +1,18 @@ +// Test: API function bound to a named (non-default) endpoint. +// Verifies that CALL API 'ext-db' generates the correct endpoint reference in output. + +ENDPOINT 'ext-db' AT 'https://other.api.eu/v2'; + +LET dynamic : ?validate-ext(text : $id) CALL API 'ext-db'; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + +WITH BT-00-Text + ASSERT ?validate-ext(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/schematrons.json new file mode 100644 index 00000000..c8f0b9a5 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/schematrons.json @@ -0,0 +1,21 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/complete-validation.sch new file mode 100644 index 00000000..241de272 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/complete-validation.sch @@ -0,0 +1,26 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..84e0fd29 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_NamedEndpoint/static/validation-stage-1a-1.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-001 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/complete-validation.sch new file mode 100644 index 00000000..606811bc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/complete-validation.sch @@ -0,0 +1,60 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..651d993e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S1A-001 + + rule|text|api-error + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + + rule|text|api-error + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..8a48cb2c --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + + rule|text|api-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + + rule|text|api-error + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/input.efx new file mode 100644 index 00000000..be1b9ae8 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/input.efx @@ -0,0 +1,60 @@ +// Test: Explicit ON ERROR REJECT on an API function. +// Verifies that the companion error assert uses role="ERROR" (not WARNING) +// and uses the default error label (rule|text|api-error). + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API, ON ERROR REJECT; + +---- STAGE 1a ---- + +// Context 1: static + dynamic with ON ERROR REJECT +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert with explicit ON ERROR REJECT + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: WHEN clause +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN + WHEN BT-00-Indicator is present + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1 +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2 +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/schematrons.json new file mode 100644 index 00000000..af577a36 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/complete-validation.sch new file mode 100644 index 00000000..bd58d1ed --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/complete-validation.sch @@ -0,0 +1,32 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_OnErrorReject/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/complete-validation.sch new file mode 100644 index 00000000..606811bc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/complete-validation.sch @@ -0,0 +1,60 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..f25a8835 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S1A-001 + + rule|text|api-error + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + + rule|text|api-error + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..79f83f09 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + + rule|text|api-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + + rule|text|api-error + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/input.efx new file mode 100644 index 00000000..a32b7125 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/input.efx @@ -0,0 +1,61 @@ +// Test: REPORT rule with inline API call. +// Verifies that report guards use "not($var = -1) and" semantics (suppresses the report +// when API errors), unlike asserts which use "($var = -1) or" (suppresses the assert). + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +// Context 1: report + assert with same API call — compare guard semantics +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic report — guards use "not(...) and" + REPORT not(?check(BT-00-Identifier)) + AS WARNING R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: assert for contrast — guards use "(...) or" +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN — guards use "(...) or" + WHEN BT-00-Indicator is present + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1: report with WHEN clause +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic report with WHEN + WHEN BT-00-Indicator is present + REPORT not(?check(BT-00-Identifier)) + AS WARNING R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2 +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert + ASSERT ?check(BT-00-Identifier) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/schematrons.json new file mode 100644 index 00000000..af577a36 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/complete-validation.sch new file mode 100644 index 00000000..bd58d1ed --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/complete-validation.sch @@ -0,0 +1,32 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ReportRule/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/complete-validation.sch new file mode 100644 index 00000000..322e270d --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/complete-validation.sch @@ -0,0 +1,65 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..8071a0fc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,20 @@ + + + + + + + rule|text|R-S1A-001 + rule|text|api-warning + rule|text|api-error + + rule|text|api-error + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + rule|text|api-error + rule|text|api-warning + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-2.sch new file mode 100644 index 00000000..bc0b0f8e --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-1a-2.sch @@ -0,0 +1,10 @@ + + + + + rule|text|R-S1A-003 + rule|text|api-error + rule|text|api-warning + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..133e18a9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + rule|text|api-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + rule|text|api-error + + rule|text|api-error + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/input.efx new file mode 100644 index 00000000..d3eca0b8 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/input.efx @@ -0,0 +1,75 @@ +// Test: API variables at all declaration levels combined with inline calls. +// Exercises five ways to reach an API call: +// 1. Global LET → schema-level in complete-validation.sch +// 2. Stage LET → pattern-level +// 3. WITH before ctx → pattern-level (stage variable in RuleSet) +// 4. WITH after ctx → rule-level (local variable in RuleSet) +// 5. Inline in ASSERT → rule-level via ApiCallBinding +// Also tests that stage variables from inactive RuleSets are excluded from subtype patterns. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-buyer(text : $buyerId) CALL API; +LET dynamic : ?confirm-status(text : $code) CALL API, ON ERROR WARN; +LET dynamic : ?verify-code(text : $id, text : $code) CALL API; +LET dynamic : ?check-global(text : $val) CALL API; + +// Level 1: Global API variable +LET dynamic : $globalOk = ?check-global(BT-00-Code); + +---- STAGE 1a ---- + +// Level 2: Stage API variable +LET dynamic : $stageOk = ?confirm-status(BT-00-Code); + +// Context 1: WITH local API variables — tests levels 3, 4, and 5 +WITH dynamic : $statusOk = ?confirm-status(BT-00-Code), BT-00-Text, dynamic : $buyerOk = ?check-buyer(BT-00-Identifier) + // Static assert — appears in both static and dynamic output + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert: local + stage vars + inline call — guards for referenced vars only + ASSERT $buyerOk and $stageOk and ?verify-code(BT-00-Identifier, BT-00-Code) + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: no local API variables, IN 1-2 to create subtype 2 where context 1 is absent. +// In subtype 2, $statusOk and $buyerOk should NOT appear at pattern level. +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1-2 + + // Dynamic assert with WHEN: stage + global vars, no inline call + WHEN BT-00-Indicator is present + ASSERT $stageOk and $globalOk + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1-2 + +---- STAGE 2a ---- + +// Context 1: no stage API variable here — only global is in scope +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert: global var only — tests guard filtering across stages + ASSERT $globalOk + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2: different context node — global var + inline call +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert: global var + inline call from different context + ASSERT $globalOk and ?check-buyer(BT-00-Identifier) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/schematrons.json new file mode 100644 index 00000000..f9715d93 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/schematrons.json @@ -0,0 +1,41 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-2.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-1a-2", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-2.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} \ No newline at end of file diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/complete-validation.sch new file mode 100644 index 00000000..62e930fa --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/complete-validation.sch @@ -0,0 +1,36 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-2.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-2.sch new file mode 100644 index 00000000..c06a796f --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-1a-2.sch @@ -0,0 +1,6 @@ + + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ResultInVariable/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/complete-validation.sch new file mode 100644 index 00000000..606811bc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/complete-validation.sch @@ -0,0 +1,60 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..392ed233 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S1A-001 + + rule|text|api-error + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + + rule|text|api-error + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..f1c398b2 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + + rule|text|api-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + + rule|text|api-error + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/input.efx new file mode 100644 index 00000000..a4eb3a32 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/input.efx @@ -0,0 +1,60 @@ +// Test: Basic API call — one endpoint, one function. +// Verifies static/dynamic split, error guards, companion asserts, and WHEN clause +// interaction, all within a realistic rule structure. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-buyer(text : $buyerId) CALL API; + +---- STAGE 1a ---- + +// Context 1: static + dynamic mixed +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert — inline API call + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: WHEN clause with API call +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN + WHEN BT-00-Indicator is present + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1: static + dynamic mixed +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2: different context node +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert from different context + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/schematrons.json new file mode 100644 index 00000000..af577a36 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/complete-validation.sch new file mode 100644 index 00000000..bd58d1ed --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/complete-validation.sch @@ -0,0 +1,32 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_SimpleEndpointAndFunction/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/complete-validation.sch new file mode 100644 index 00000000..606811bc --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/complete-validation.sch @@ -0,0 +1,60 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..9bd6d42a --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,19 @@ + + + + rule|text|R-S1A-001 + + rule|text|api-error + + rule|text|api-warning + rule|text|R-S1A-002 + + + rule|text|R-S1A-003 + + rule|text|api-error + + rule|text|api-warning + rule|text|R-S1A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-2a-1.sch new file mode 100644 index 00000000..b089c722 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/dynamic/validation-stage-2a-1.sch @@ -0,0 +1,15 @@ + + + + rule|text|R-S2A-001 + + rule|text|api-error + rule|text|R-S2A-002 + + + rule|text|R-S2A-003 + + rule|text|api-warning + rule|text|R-S2A-004 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/input.efx new file mode 100644 index 00000000..17034c76 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/input.efx @@ -0,0 +1,62 @@ +// Test: Two inline API calls in the same ASSERT expression. +// Verifies that each call gets its own binding variable (__apiResult1, __apiResult2), +// both get error guards on the main assert, and each gets its own companion error assert. +// Also tests mixed error handling: default (ERROR) vs ON ERROR WARN. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check-buyer(text : $id) CALL API; +LET dynamic : ?check-code(text : $code) CALL API, ON ERROR WARN; + +---- STAGE 1a ---- + +// Context 1: two API calls in one expression +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S1A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert — two API calls combined + ASSERT ?check-buyer(BT-00-Identifier) and ?check-code(BT-00-Code) + AS ERROR R-S1A-002 + FOR BT-00-Text IN 1 + +// Context 2: WHEN clause with dual API calls +WITH BT-00-Text + // Static report + REPORT empty(BT-00-Text) + AS WARNING R-S1A-003 + FOR BT-00-Text IN 1 + + // Dynamic assert with WHEN — two API calls + WHEN BT-00-Indicator is present + ASSERT ?check-buyer(BT-00-Identifier) and ?check-code(BT-00-Code) + AS ERROR R-S1A-004 + FOR BT-00-Text IN 1 + +---- STAGE 2a ---- + +// Context 1: single API call per rule +WITH BT-00-Text + // Static assert + ASSERT BT-00-Text is present + AS ERROR R-S2A-001 + FOR BT-00-Text IN 1 + + // Dynamic assert — single API call for contrast + ASSERT ?check-buyer(BT-00-Identifier) + AS ERROR R-S2A-002 + FOR BT-00-Text IN 1 + +// Context 2 +WITH ND-SubNode + // Static report + REPORT empty(BT-01-SubLevel-Text) + AS WARNING R-S2A-003 + FOR BT-01-SubLevel-Text IN 1 + + // Dynamic assert + ASSERT ?check-code(BT-00-Code) + AS ERROR R-S2A-004 + FOR BT-01-SubLevel-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/schematrons.json new file mode 100644 index 00000000..af577a36 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/schematrons.json @@ -0,0 +1,31 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "dynamic", + "stage" : "2a", + "filename" : "dynamic/validation-stage-2a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "static", + "stage" : "1a", + "filename" : "static/validation-stage-1a-1.sch" + }, { + "name" : "validation-stage-2a-1", + "type" : "static", + "stage" : "2a", + "filename" : "static/validation-stage-2a-1.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/complete-validation.sch new file mode 100644 index 00000000..bd58d1ed --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/complete-validation.sch @@ -0,0 +1,32 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + ../PathNode/ChildNode/SubLevelTextField + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-1a-1.sch new file mode 100644 index 00000000..f6494991 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-1a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S1A-001 + + + rule|text|R-S1A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-2a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-2a-1.sch new file mode 100644 index 00000000..1b80c5b9 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TwoApiCallsInExpression/static/validation-stage-2a-1.sch @@ -0,0 +1,9 @@ + + + + rule|text|R-S2A-001 + + + rule|text|R-S2A-003 + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TypeMismatch_InArithmeticExpression/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TypeMismatch_InArithmeticExpression/input.efx new file mode 100644 index 00000000..27ce6cc7 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_TypeMismatch_InArithmeticExpression/input.efx @@ -0,0 +1,13 @@ +// Intentionally invalid: using a boolean API function in an arithmetic expression. +// The compiler should throw a type mismatch exception. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT 1 + ?check(BT-00-Identifier) == 2 + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredEndpoint_ThrowsError/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredEndpoint_ThrowsError/input.efx new file mode 100644 index 00000000..2ffd5136 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredEndpoint_ThrowsError/input.efx @@ -0,0 +1,15 @@ +// Test: Error — API function references an endpoint that was never declared. +// The grammar enforces that endpoints are declared before functions, but this test +// uses an endpoint name that simply doesn't exist anywhere. +// The compiler should throw a ParseCancellationException at declaration time. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API 'nonexistent'; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredFunction_ThrowsError/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredFunction_ThrowsError/input.efx new file mode 100644 index 00000000..da8fefc1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_UndeclaredFunction_ThrowsError/input.efx @@ -0,0 +1,11 @@ +// Intentionally invalid: calling a function that was never declared. +// The compiler should throw an exception. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT ?undeclared-func(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/api-defs.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/api-defs.efx new file mode 100644 index 00000000..62280c6b --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/api-defs.efx @@ -0,0 +1,3 @@ +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/complete-validation.sch new file mode 100644 index 00000000..70d43caf --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/complete-validation.sch @@ -0,0 +1,54 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/validation-stage-1a-1.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/validation-stage-1a-1.sch new file mode 100644 index 00000000..6a60b33b --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/dynamic/validation-stage-1a-1.sch @@ -0,0 +1,8 @@ + + + + + rule|text|api-error + rule|text|R-K7P-M2Q + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/input.efx new file mode 100644 index 00000000..c025d9c1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/input.efx @@ -0,0 +1,12 @@ +// Test: API declarations imported via #include. +// Verifies that endpoint and function declarations in an included file +// are available for use in the main rules file. + +#include "api-defs.efx" + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/schematrons.json b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/schematrons.json new file mode 100644 index 00000000..1ab611c7 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/schematrons.json @@ -0,0 +1,16 @@ +{ + "schematrons" : [ { + "name" : "complete-validation", + "type" : "dynamic", + "filename" : "dynamic/complete-validation.sch" + }, { + "name" : "validation-stage-1a-1", + "type" : "dynamic", + "stage" : "1a", + "filename" : "dynamic/validation-stage-1a-1.sch" + }, { + "name" : "complete-validation", + "type" : "static", + "filename" : "static/complete-validation.sch" + } ] +} diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/static/complete-validation.sch b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/static/complete-validation.sch new file mode 100644 index 00000000..a98e3507 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_ViaInclude/static/complete-validation.sch @@ -0,0 +1,22 @@ + + + + eForms schematron rules + + + + + + + + + + + + + + + + + + diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentCount_ThrowsError/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentCount_ThrowsError/input.efx new file mode 100644 index 00000000..0093ada1 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentCount_ThrowsError/input.efx @@ -0,0 +1,13 @@ +// Intentionally invalid: passing two arguments to a function that expects one. +// The compiler should throw an exception. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(text : $id) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier, BT-00-Code) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1 diff --git a/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentType_ThrowsError/input.efx b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentType_ThrowsError/input.efx new file mode 100644 index 00000000..24d4fba0 --- /dev/null +++ b/src/test/resources/eu/europa/ted/efx/sdk2/EfxRulesTranslatorV2Test/testApiCall_WrongArgumentType_ThrowsError/input.efx @@ -0,0 +1,13 @@ +// Intentionally invalid: passing a text argument where a number is expected. +// The compiler should throw an exception. + +ENDPOINT 'default' AT 'https://api.ted.europa.eu/v1'; + +LET dynamic : ?check(number : $num) CALL API; + +---- STAGE 1a ---- + +WITH BT-00-Text + ASSERT ?check(BT-00-Identifier) + AS ERROR R-K7P-M2Q + FOR BT-00-Text IN 1