-
Notifications
You must be signed in to change notification settings - Fork 9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
xpath: [incorrect] support for indexed-repeat
function
#150
Changes from all commits
9aad48b
9fdf3e2
56df0ea
66aa8c9
a781d92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@getodk/scenario": patch | ||
"@getodk/xforms-engine": patch | ||
"@getodk/xpath": patch | ||
--- | ||
|
||
xpath: support for `indexed-repeat` function |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -64,6 +64,10 @@ interface CreateNewRepeatAssertedReferenceOptions { | |
readonly assertCurrentReference: string; | ||
} | ||
|
||
interface SetPositionalStateOptions { | ||
readonly createMissingRepeatInstances?: boolean; | ||
} | ||
|
||
// prettier-ignore | ||
type GetQuestionAtIndexParameters< | ||
ExpectedQuestionType extends QuestionNodeType | ||
|
@@ -140,7 +144,8 @@ export class Scenario { | |
readonly formName: string; | ||
readonly instanceRoot: RootNode; | ||
|
||
private readonly getPositionalEvents: Accessor<PositionalEvents>; | ||
protected readonly getPositionalEvents: Accessor<PositionalEvents>; | ||
protected readonly getEventPosition: Accessor<number>; | ||
private readonly setEventPosition: Setter<number>; | ||
|
||
protected readonly getSelectedPositionalEvent: Accessor<AnyPositionalEvent>; | ||
|
@@ -151,14 +156,15 @@ export class Scenario { | |
this.formName = formName; | ||
this.instanceRoot = instanceRoot; | ||
|
||
const [eventPosition, setEventPosition] = createSignal(0); | ||
const [getEventPosition, setEventPosition] = createSignal(0); | ||
|
||
this.getPositionalEvents = () => getPositionalEvents(instanceRoot); | ||
this.getEventPosition = getEventPosition; | ||
this.setEventPosition = setEventPosition; | ||
|
||
this.getSelectedPositionalEvent = createMemo(() => { | ||
const events = getPositionalEvents(instanceRoot); | ||
const position = eventPosition(); | ||
const position = getEventPosition(); | ||
const event = events[position]; | ||
|
||
if (event == null) { | ||
|
@@ -288,13 +294,73 @@ export class Scenario { | |
return this.setNonTerminalEventPosition(increment, expectReference); | ||
} | ||
|
||
private setPositionalStateToReference(reference: string): AnyPositionalEvent { | ||
private createMissingRepeatInstances(reference: string): void { | ||
let tempReference = reference; | ||
let indexedReference: string | null = null; | ||
|
||
const trailingPositionalPredicatePattern = /\[\d+\]$/; | ||
|
||
do { | ||
if (trailingPositionalPredicatePattern.test(tempReference)) { | ||
indexedReference = tempReference; | ||
} else { | ||
tempReference = tempReference.replace(/\/[^/]+$/, ''); | ||
|
||
if (tempReference === '') { | ||
break; | ||
} | ||
} | ||
} while (indexedReference == null); | ||
|
||
if (indexedReference == null) { | ||
return; | ||
} | ||
|
||
const repeatRangeReference = indexedReference.replace(trailingPositionalPredicatePattern, ''); | ||
|
||
const positionalPredicate = indexedReference.replace(/^.*\[(\d+)\]$/, '$1'); | ||
const count = parseInt(positionalPredicate, 10); | ||
|
||
if (count < 1) { | ||
return; | ||
} | ||
|
||
const repeatRange = this.getInstanceNode(repeatRangeReference); | ||
|
||
if (repeatRange.nodeType !== 'repeat-range') { | ||
throw 'todo'; | ||
} | ||
|
||
const repeatBodyDefinition = repeatRange.definition.bodyElement; | ||
|
||
if (repeatBodyDefinition.countExpression != null || repeatBodyDefinition.isFixedCount) { | ||
return; | ||
} | ||
|
||
const instances = repeatRange.currentState.children.length; | ||
const delta = count - instances; | ||
|
||
for (let i = 0; i < delta; i += 1) { | ||
this.createNewRepeat(repeatRangeReference); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The algorithm here is something like find the last positional predicate, strip it off, use that reference to find the repeat range, add instances up to the position accessed, right? So it won't work for nested repeats that all have missing instances (e.g. /data/repeat1[2]/repeat2[5]/foo)? I think maybe there are tests that use that in JR but I'm not entirely sure. Fine to leave as-is for now but wanted to check my understanding. I don't think this requires immediate actions either but I wanted to mention that I expected some slightly higher-level building blocks here! Specifically, I'm surprised to see some regex for getting references without predicates and for getting sub references. I imagine at some point it will be worth having some reusable support for those concepts. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The comment about building blocks is partially addressed at 66aa8c9#diff-f3ac6c9869393022cd23375d383ded81f1d35c2c962125846cb288792ee28d52R15 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been mulling this since I saw these comments yesterday, and my instinct is that we're probably better off not porting this aspect of
I already wanted to make repeat instance creation more explicit before this discussion, but I hesitated in this PR on the basis that it would increase divergence from JavaRosa. But that divergence is much more appealing to me now, in light of some of the implications this has raised. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought your linked comment meant in the engine. Agreed it’s the engine that eventually should have that functionality so good to see it’s coming. 👍 As for not porting the implicit repeat addition, then I think we should probably change the JR tests. I don’t know off the top of my head how many are affected and will look. |
||
} | ||
|
||
private setPositionalStateToReference( | ||
reference: string, | ||
options: SetPositionalStateOptions = {} | ||
): AnyPositionalEvent { | ||
const events = this.getPositionalEvents(); | ||
const index = events.findIndex(({ node }) => { | ||
return node?.currentState.reference === reference; | ||
}); | ||
|
||
if (index === -1) { | ||
if (options?.createMissingRepeatInstances) { | ||
this.createMissingRepeatInstances(reference); | ||
|
||
return this.setPositionalStateToReference(reference); | ||
} | ||
|
||
throw new Error( | ||
`Setting answer to ${reference} failed: could not locate question/positional event with that reference.` | ||
); | ||
|
@@ -304,7 +370,9 @@ export class Scenario { | |
} | ||
|
||
private answerSelect(reference: string, ...selectionValues: string[]): ComparableAnswer { | ||
const event = this.setPositionalStateToReference(reference); | ||
const event = this.setPositionalStateToReference(reference, { | ||
createMissingRepeatInstances: true, | ||
}); | ||
|
||
if (!isQuestionEventOfType(event, 'select')) { | ||
throw new Error( | ||
|
@@ -315,6 +383,15 @@ export class Scenario { | |
return event.answerQuestion(new SelectValuesAnswer(selectionValues)); | ||
} | ||
|
||
/** | ||
* **PORTING NOTES** | ||
* | ||
* Per JavaRosa: | ||
* | ||
* > This method has side effects: | ||
* > - It will create all the required middle and end repeat group instances | ||
* > - It changes the current form index | ||
*/ | ||
answer(...args: AnswerParameters): unknown { | ||
if (isAnswerSelectParams(args)) { | ||
return this.answerSelect(...args); | ||
|
@@ -335,7 +412,9 @@ export class Scenario { | |
} else if (typeof arg0 === 'string') { | ||
const reference = arg0; | ||
|
||
event = this.setPositionalStateToReference(reference); | ||
event = this.setPositionalStateToReference(reference, { | ||
createMissingRepeatInstances: true, | ||
}); | ||
value = arg1; | ||
} else { | ||
throw new Error('Unsupported `answer` overload call'); | ||
|
@@ -371,7 +450,7 @@ export class Scenario { | |
const node = getNodeForReference(this.instanceRoot, reference); | ||
|
||
if (node == null) { | ||
throw new Error(`No "answer" node for reference: ${reference}`); | ||
throw new Error(`No instance node for reference: ${reference}`); | ||
} | ||
|
||
return node; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: new functionality feels like minor? Not a big deal either way!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree. I was partly basing this on precedent in 1ac6b10, which surprised me but seemed to be oriented around targeting some set of functionality for certain 0.x milestones. Not sure if that was the right inference, or if it's still pertinent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, I see! I think I was thinking of the API addition as internal but it’s not, really. So that one should have been minor for the engine and dependents?
This one feels unambiguous to me because it’s introducing user-facing functionality.