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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
<sdk.antlr4.dir>${project.build.directory}/eforms-sdk/antlr4</sdk.antlr4.dir>

<!-- Versions - eForms -->
<version.eforms-sdk-1>1.13.0</version.eforms-sdk-1>
<version.eforms-sdk-1>1.16.0-SNAPSHOT</version.eforms-sdk-1>
<version.eforms-sdk-2>2.0.0-SNAPSHOT</version.eforms-sdk-2>
<version.eforms-core>1.8.0-SNAPSHOT</version.eforms-core>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1095,6 +1095,23 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx
}
}

/**
* A selector-block yields the reference itself rather than its value: the value step that every
* other reference position applies is deliberately not composed here. The path is otherwise
* resolved exactly as it would be in an expression-block, relative to the declared context
* unless the author wrote it as an absolute reference.
*/
@Override
public void exitSelection(final SelectionContext ctx) {
if (ctx.attributeReference() != null) {
// attributeReference has no exit handler of its own, so the attribute step is composed here
// rather than globally, which would double-compose it for the scalar and sequence positions.
this.stack.push(this.script.composeFieldAttributeReference(
this.stack.pop(PathExpression.class),
ctx.attributeReference().Identifier().getText(), StringPath.class));
}
}

@Override
public void exitScalarFromAttributeReference(ScalarFromAttributeReferenceContext ctx) {
this.stack.push(this.script.composeFieldAttributeReference(this.stack.pop(PathExpression.class),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1919,6 +1919,25 @@ public void exitSequenceFromFieldReference(SequenceFromFieldReferenceContext ctx
this.resolveAndPushFieldReference(ctx, result, fieldId);
}

/**
* A selector yields the reference itself rather than its value: the value step that every other
* reference position applies is deliberately not composed here. The path is otherwise resolved
* exactly as it would be in an expression, relative to the declared context unless the author
* wrote it as an absolute reference.
*
* <p>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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ----------------------------------------------------
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> deps = extract("WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']");
assertTrue(deps.contains("BT-00-Text"));
assertTrue(deps.contains("BT-00-Code"));
assertTrue(deps.contains("ND-Root"));
}

@Test
void testSelector_IncludesANodeReference() {
Set<String> deps = extract("WITH ND-Root SELECT ND-SubNode");
assertTrue(deps.contains("ND-SubNode"));
assertTrue(deps.contains("ND-Root"));
}

@Test
void testSelector_BraceSpellingBehavesTheSame() {
assertEquals(extract("WITH ND-Root SELECT /BT-00-Text"),
extract("{ND-Root} &{/BT-00-Text}"));
}

// #endregion: Selectors ----------------------------------------------------
}
Original file line number Diff line number Diff line change
Expand Up @@ -4674,4 +4674,113 @@ void testCompute_CaseInsensitive() {
}

// #endregion: EFX-2 COMPUTE syntax

// #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.
*/
@Test
void testSelector_YieldsTheSamePathAsTheEquivalentExpression() {
assertEquals(translateExpression("WITH ND-Root COMPUTE BT-00-Text"),
translateExpression("WITH ND-Root SELECT BT-00-Text") + "/normalize-space(text())");
assertEquals(translateExpression("WITH ND-Root COMPUTE BT-00-Integer"),
translateExpression("WITH ND-Root SELECT BT-00-Integer") + "/number()");
assertEquals(translateExpression("WITH ND-SubNode COMPUTE BT-00-Text"),
translateExpression("WITH ND-SubNode SELECT BT-00-Text") + "/normalize-space(text())");
}

/** Both spellings of the selector must produce the same result. */
@Test
void testSelector_BothSpellingsAgree() {
assertEquals(translateExpression("WITH ND-Root SELECT /BT-00-Text"),
translateExpression("{ND-Root} &{/BT-00-Text}"));
assertEquals(translateExpression("WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']"),
translateExpression("{ND-Root} &{/BT-00-Text[BT-00-Code == 'x']}"));
}

@Test
void testSelector_FieldReference() {
testExpressionTranslation("PathNode/TextField", "WITH ND-Root SELECT BT-00-Text");
}

@Test
void testSelector_AbsoluteFieldReference() {
testExpressionTranslation("/*/PathNode/TextField", "WITH ND-Root SELECT /BT-00-Text");
}

@Test
void testSelector_WithPredicate() {
testExpressionTranslation("/*/PathNode/TextField[../CodeField/normalize-space(text()) = 'x']",
"WITH ND-Root SELECT /BT-00-Text[BT-00-Code == 'x']");
}

@Test
void testSelector_NumericFieldHasNoValueStep() {
testExpressionTranslation("PathNode/IntegerField", "WITH ND-Root SELECT BT-00-Integer");
}

@Test
void testSelector_DurationFieldHasNoValueStep() {
testExpressionTranslation("PathNode/DurationField", "WITH ND-Root SELECT BT-00-Duration");
}

@Test
void testSelector_NodeReference() {
testExpressionTranslation("SubNode", "WITH ND-Root SELECT ND-SubNode");
testExpressionTranslation("/*/SubNode", "WITH ND-Root SELECT /ND-SubNode");
}

@Test
void testSelector_AttributeReference() {
testExpressionTranslation("/*/PathNode/TextField/@Attribute",
"WITH ND-Root SELECT /BT-00-Text/@Attribute");
}

@Test
void testSelector_IsRelativeToTheDeclaredContext() {
testExpressionTranslation("../PathNode/TextField", "WITH ND-SubNode SELECT BT-00-Text");
testExpressionTranslation("/*/PathNode/TextField", "WITH ND-SubNode SELECT /BT-00-Text");
}

@Test
void testSelector_KeywordIsCaseInsensitive() {
assertEquals(translateExpression("WITH ND-Root SELECT BT-00-Text"),
translateExpression("with ND-Root select BT-00-Text"));
}

@Test
void testSelector_RejectsValueExpression() {
assertThrows(ParseCancellationException.class,
() -> translateExpression("WITH ND-Root SELECT BT-00-Text == 'x'"));
}


/**
* A selector's indexer is a node-level indexer, like every other {@code fieldContext} in the
* language: it selects the nth occurrence within each parent, not the nth item of the sequence
* overall. This is why an indexed selector differs from an indexed expression, where the index
* applies to the sequence of values - the one case where a selector is not simply the expression
* without its value step.
*
* <p>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 ----------------------------------------------------
}
Loading