diff --git a/pom.xml b/pom.xml
index 7a5f4b9..525baf7 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
The reference tier has already left the path on the stack. Only an attribute reference
+ * needs work, because {@code attributeReference} has no exit handler of its own; composing the
+ * attribute step globally would double-compose it for the scalar and sequence positions.
+ */
+ @Override
+ public void exitSelection(final SelectionContext ctx) {
+ if (ctx.attributeReference() != null) {
+ this.stack.push(this.script.composeFieldAttributeReference(
+ this.stack.pop(PathExpression.class),
+ ctx.attributeReference().attributeName.getText(), StringPath.class));
+ }
+ }
+
@Override
public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) {
PathExpression result = this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class),
diff --git a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
index b9647b6..dc0586b 100644
--- a/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
+++ b/src/test/java/eu/europa/ted/efx/sdk1/EfxExpressionTranslatorV1Test.java
@@ -1653,4 +1653,131 @@ void testParameterizedExpression_WithDurationParameter() {
// #endregion: Compare sequences
// #endregion Sequence Functions
+
+ // #region: Selectors -------------------------------------------------------
+
+ /**
+ * A selector yields the same path an expression would use, without the value step that an
+ * expression appends. It is relative or absolute according to how the reference was written,
+ * exactly as in any other position in the language.
+ */
+ @Test
+ void testSelector_YieldsTheSamePathAsTheEquivalentExpression() {
+ assertEquals(translateExpression("{ND-Root} ${BT-00-Text}"),
+ translateExpression("{ND-Root} &{BT-00-Text}") + "/normalize-space(text())");
+ assertEquals(translateExpression("{ND-SubNode} ${BT-00-Text}"),
+ translateExpression("{ND-SubNode} &{BT-00-Text}") + "/normalize-space(text())");
+ assertEquals(translateExpression("{ND-Root} ${/BT-00-Text}"),
+ translateExpression("{ND-Root} &{/BT-00-Text}") + "/normalize-space(text())");
+ assertEquals(translateExpression("{ND-Root} ${BT-00-Integer}"),
+ translateExpression("{ND-Root} &{BT-00-Integer}") + "/number()");
+ }
+
+ @Test
+ void testSelector_FieldReference() {
+ testExpressionTranslation("PathNode/TextField", "{ND-Root} &{BT-00-Text}");
+ }
+
+ @Test
+ void testSelector_AbsoluteFieldReference() {
+ testExpressionTranslation("/*/PathNode/TextField", "{ND-Root} &{/BT-00-Text}");
+ }
+
+ @Test
+ void testSelector_WithPredicate() {
+ testExpressionTranslation(
+ "/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x']",
+ "{ND-Root} &{/BT-00-Text[BT-00-Code == 'x']}");
+ }
+
+ @Test
+ void testSelector_NumericFieldHasNoValueStep() {
+ testExpressionTranslation("PathNode/IntegerField", "{ND-Root} &{BT-00-Integer}");
+ }
+
+ @Test
+ void testSelector_DurationFieldHasNoValueStep() {
+ testExpressionTranslation("PathNode/DurationField", "{ND-Root} &{BT-00-Duration}");
+ }
+
+ @Test
+ void testSelector_IndicatorField() {
+ testExpressionTranslation("PathNode/IndicatorField", "{ND-Root} &{BT-00-Indicator}");
+ }
+
+ @Test
+ void testSelector_MultilingualField() {
+ testExpressionTranslation("PathNode/TextMultilingualField",
+ "{ND-Root} &{BT-00-Text-Multilingual}");
+ }
+
+ @Test
+ void testSelector_AttributeField() {
+ testExpressionTranslation("PathNode/TextField/@Attribute", "{ND-Root} &{BT-00-Attribute}");
+ }
+
+ @Test
+ void testSelector_ExplicitAttributeReference() {
+ testExpressionTranslation("/*/PathNode/TextField/@Attribute",
+ "{ND-Root} &{/BT-00-Text/@Attribute}");
+ }
+
+ @Test
+ void testSelector_NodeReference() {
+ testExpressionTranslation("SubNode", "{ND-Root} &{ND-SubNode}");
+ }
+
+ @Test
+ void testSelector_AbsoluteNodeReference() {
+ testExpressionTranslation("/*/SubNode", "{ND-Root} &{/ND-SubNode}");
+ }
+
+ @Test
+ void testSelector_FieldUnderAnotherNode() {
+ testExpressionTranslation("SubNode/SubTextField", "{ND-Root} &{BT-01-SubNode-Text}");
+ }
+
+ @Test
+ void testSelector_ContextIsTheReferencedFieldItself() {
+ testExpressionTranslation(".", "{BT-00-Text} &{BT-00-Text}");
+ }
+
+ @Test
+ void testSelector_ContextIsAnotherNode() {
+ testExpressionTranslation("SubTextField", "{ND-SubNode} &{BT-01-SubNode-Text}");
+ testExpressionTranslation("../PathNode/TextField", "{ND-SubNode} &{BT-00-Text}");
+ }
+
+ @Test
+ void testSelector_ContextIsAField() {
+ testExpressionTranslation("../CodeField", "{BT-00-Text} &{BT-00-Code}");
+ testExpressionTranslation("../../SubNode/SubTextField", "{BT-00-Text} &{BT-01-SubNode-Text}");
+ }
+
+ @Test
+ void testSelector_ContextIsAFieldWithPredicate() {
+ testExpressionTranslation(".[../CodeField/normalize-space(text()) = 'x']",
+ "{BT-00-Text} &{BT-00-Text[BT-00-Code == 'x']}");
+ testExpressionTranslation(
+ "SubTextField[../../PathNode/CodeField/normalize-space(text()) = 'x']",
+ "{ND-SubNode} &{BT-01-SubNode-Text[BT-00-Code == 'x']}");
+ }
+
+ /**
+ * An absolute selector is obtained the same way as an absolute reference anywhere else: by
+ * writing the leading slash. Consumers that evaluate against a whole document need that.
+ */
+ @Test
+ void testSelector_IsAbsoluteOnlyWhenWrittenAsAbsolute() {
+ testExpressionTranslation("/*/PathNode/TextField", "{ND-SubNode} &{/BT-00-Text}");
+ testExpressionTranslation("/*/PathNode/CodeField", "{BT-00-Text} &{/BT-00-Code}");
+ }
+
+ @Test
+ void testSelector_RejectsValueExpression() {
+ assertThrows(ParseCancellationException.class,
+ () -> translateExpression("{ND-Root} &{BT-00-Text == 'x'}"));
+ }
+
+ // #endregion: Selectors ----------------------------------------------------
}
diff --git a/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java
index 89545dc..37b51b1 100644
--- a/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java
+++ b/src/test/java/eu/europa/ted/efx/sdk2/EfxComputeDependencyExtractorTest.java
@@ -132,4 +132,41 @@ void testDuplicateFieldReferences() {
}
// #endregion: Deduplication
+
+ // #region: Selectors -------------------------------------------------------
+
+ /**
+ * The extractor parses the same top-level rule as the translators, so it sees the selector
+ * alternative added to the grammar although it has no handler of its own for it. It nevertheless
+ * derives the correct dependencies, because it listens to the reference exits rather than to the
+ * top-level block. These tests record that, so that the behaviour is not lost inadvertently.
+ */
+ @Test
+ void testSelector_YieldsTheSameDependenciesAsTheEquivalentExpression() {
+ assertEquals(extract("WITH ND-Root COMPUTE BT-00-Text"),
+ extract("WITH ND-Root SELECT BT-00-Text"));
+ }
+
+ @Test
+ void testSelector_IncludesReferencesFromThePredicate() {
+ Set Do not "correct" this into a parenthesised form. Indexing at node level is deliberate and is
+ * shared with {@code :rawValue}, context iterators and context overrides; changing it here alone
+ * would make the selector the odd one out, and changing it everywhere would redefine the language.
+ */
+ @Test
+ void testSelector_IndexerAppliesAtNodeLevel() {
+ testExpressionTranslation("PathNode/TextField[1]", "WITH ND-Root SELECT BT-00-Text[1]");
+ testExpressionTranslation("(PathNode/TextField/normalize-space(text()))[1]",
+ "WITH ND-Root COMPUTE BT-00-Text[1]");
+ }
+
+ @Test
+ void testSelector_IndexerAfterPredicate() {
+ testExpressionTranslation(
+ "/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x'][1]",
+ "WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x'][1]");
+ }
+
+ // #endregion: Selectors ----------------------------------------------------
}