-
Notifications
You must be signed in to change notification settings - Fork 13
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
[Edits] Support for deprecatedID
and instanceID
edit semantics
#352
base: main
Are you sure you want to change the base?
[Edits] Support for deprecatedID
and instanceID
edit semantics
#352
Conversation
🦋 Changeset detectedLatest commit: edca125 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
13a1be0
to
c0be03e
Compare
deprecatedId
and instanceId
edit semanticsdeprecatedID
and instanceID
edit semantics
638f156
to
4794cc8
Compare
… children This splits the current `children.ts` module roughly into: - Branchy node-specific construction logic that already existed prior to #336. - Coordination of input to those node constructors introduced in #336. This logic is hopefully a little bit clearer now too. The latter module’s logic will be expanded in the next commit, to include said special case logic for instance meta children.
3e7244c
to
87b49f7
Compare
assertMetaNamespaceOptions(value); | ||
assertScenario(value.sourceScenario); | ||
}; | ||
|
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.
Most of the above has been extracted from submission.test.ts
, where Past Me was sorely lacking in foresight:
Normally this might be implemented as a custom "matcher" (assertion). But it's so specific to this sub-suite that it would be silly to sprawl it out into other parts of the codebase!
The intent of implementing these assertion extensions now is because Past Me was obviously wrong about how specific the logic was/would be to that sub-suite. With the benefit of hindsight, custom assertions are exactly how we usually avoid "sprawl[ing] out into other parts of [scenario
]". They are our primary mechanism for sharing test logic across modules/suites.
There are probably going to be a bunch of new concepts to take in during review. I'm happy to answer any questions to help build context and confidence in these changes. As a starting point:
I tried my best to leverage as much from existing concepts as I could (either existing as foundations of our many other custom assertions, or existing as the logic previously shared across tests in submission.test.ts
). So this is mostly just moving stuff around, even if it looks like a lot is new.
The substantive changes from submission.test.ts
to here are either...
-
mechanical: adapting the logic to shared assertion extension APIs we've been using since the
scenario
package's early days; these APIs have been pretty well battle tested against the scope of many other assertion extensions, across a wide variety of tests (and providing consistency with assertions derived from JavaRosa) -
semantic clarification: splitting out the original assertion logic to identify inputs and outputs more clearly, and to apply that clarity for a wider range of assertions
@@ -0,0 +1,2 @@ | |||
export const PRELOAD_UID_PATTERN = | |||
/^uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; |
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.
This module is named generally, because I anticipate at least a few other patterns moving here.
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.
Question:
What other patterns do you anticipate? Are those related to the Edit feature?
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.
Not related to edit, at least that I'm aware of. I would probably have moved those now if I expected to use them right away.
A few that come to mind:
- XML/XPath names
- Date/time (I think you've linked others at some point as well)
There may be others that aren't bubbling up in my memory, my recall isn't exactly at its best right now!
toHaveComputedPreloadInstanceID: new AsymmetricTypedExpectExtension( | ||
assertScenario, | ||
assertMetaNamespaceOptions, | ||
(scenario, options): SimpleAssertionResult => { |
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.
Hopefully a helpful hint for new concepts...
Each of our assertion extensions follow a similar pattern:
- Treat the assertion's inputs (e.g. arg0 and arg1 corresponding to the call site shape:
expect(arg0).assertionName(arg1)
) asunknown
- Parse-validate each input from
unknown
to whatever actual type (runtime and static in tandem) is expected for the actual assertion logic - Perform assertion logic, returning:
true
-> Vitest considers this a passing assertion, test proceedsError
-> Vitest considers this a failed assertion, reports as test failure
- Static types are derived from the parse-validate logic, and used to extend the static types of Vitest itself. This is the best way we've found to ensure
expect(...).toWhatever(...)
type checks correctly (and stays in sync with the custom assertion if/whenever it changes)
* already have private constructors). Then downstream isn't even a switch | ||
* statement, it's just a lookup table. | ||
*/ | ||
export interface InstanceNodeChildInput { |
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.
Naming: everything to do with "child input"s felt right at the start (as a naming convention deriving from "instance input"). In hindsight, this name and many of the others following from it are confusing!
I'm not going to delay other aspects of review for this, but I want to make sure to highlight that: I haven't addressed this, I know it's awkward, I want to change it, and I'm open to suggestions!
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.
Ok, sounds good, thanks for the highlight
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.
So I've put in some off-screen time letting this marinate, and I think the shape of a compelling answer is forming.
-
Hypothesis: what this concept wants to be named is something with an "options" suffix. This would be along the lines of the very common naming convention we use for "an object of named properties used to provide a set of multiple named arguments to thing". Here it might be named something more like
DescendantNodeOptions
, reflecting the (abstract) "options" parameter general across all child node types (concrete subclasses ofDescendantNode
). -
Not immediately: playing that out to its logical conclusion would probably come to look like taking on many of the ideas outlined in this interface's JSDoc. Maybe all of them. That suggests a bunch of refactoring which would inflate the size of this PR, and burden it with changes well outside its scope.
-
Probably immediately: if we're generally satisfied with this PR's approach aside from naming, we can almost certainly live with an intermediate solution moving toward/preparing for the more ideal refactored solution.
So then, an intermediate solution would go something like:
- Agree that the outlined approach sounds good in principle.
- Revise names in this PR to reflect it as a starting point for that approach, to be pursued as priorities reasonably allow.
- Expand the JSDoc here to reflect these decisions.
- Add a JSDoc link from
DescendantNode
to this same interface as an additional reinforcement.
I think that last point would be valuable because this is all starting to solidify what I was already wanting to start doing when each of the concrete DescendantNode
subclasses' signatures changed in previous edit-supporting PRs. This was all more vague in my mind in the rush to build out the feature, and these signatures have shifted a lot over several recent changes. I think we may finally be getting a clearer picture of what their stable (internal) interface should eventually be.I think now would be a great time to capture that at a place where we'd start looking to slow down the churn, next time we want to revise the same signatures.
I'm going to go ahead and start down this intermediate-solution path now. If I'm happy with where that goes, I'll push it up in a single commit, so we can reference that commit, which we can then evaluate together for whether it accomplishes the near-term goals we want for this PR and establishes a good foundation for the longer-term goals discussed above. If it doesn't scratch those itches, we can trivially drop the commit/explore other ways to improve this!
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.
Yeah, this definitely feels like the right way to go. I've pushed a commit which looks more or less as I described above. A minor difference is that I decided to use DescendantNodeInitOptions
as a (temporary) name, to disambiguate from a current interface already named DescendantNodeOptions
(local to DescendantNode.ts
).
After I made the changes I took a quick sample of the current node constructor signatures, and I wasn't surprised to see that the object looks almost exactly like what those nodes already take, only they're currently positional. The main exception is repeats, which take N instanceNodes
rather than one optional instanceNode
. It wouldn't be too big a stretch to unify the signatures on a shared options parameter now, while keeping the other improvements for a later scope.
But I'm already much happier with the naming changes, and some other improvements to code clarity along the way. So I'd also be happy to leave it pretty much as it stands now, if that's where we land in the next review pass!
87b49f7
to
4a8cadd
Compare
): StaticLeafElement => { | ||
const { qualifiedName, nodeset } = parent.definition; | ||
const { namespaceURI, prefix } = qualifiedName; | ||
const { root } = new StaticDocument({ |
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.
Note: it's a bit weird to new up a StaticDocument
and immediately discard it in favor of its root
. The alternatives would be:
- New up a
StaticElement
directly, referencingparent
... as itsparent
. But this would create a hierarchical relationship which doesn't make sense! The newly created element would not be a child of itsparent
. - Create a concept in "static-dom" similar to
DocumentFragment
. This is compelling, but I didn't want to stray into new concepts. If we did ever do this, we'll have to make some choices about how it would interop withxpath
(because it wouldn't be a valid context node in strict XPath semantics).
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.
I couldn't finished it today, there are couple of pieces I need to dig more to fully understand them but that'll be on Monday.
@@ -0,0 +1,2 @@ | |||
export const PRELOAD_UID_PATTERN = | |||
/^uuid:[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/; |
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.
Question:
What other patterns do you anticipate? Are those related to the Edit feature?
* modes; and for I/O which is exercised as a prerequisite for the "edit" subset | ||
* of that suite. | ||
*/ | ||
describe('Instance edit semantics', () => { |
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.
Comment:
This test suite helped me to put some pieces together from the new code! It was easy to follow
@@ -9,6 +9,15 @@ const isInstanceFirstLoad = (context: InstanceValueContext) => { | |||
return context.rootDocument.initializationMode === 'create'; |
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.
Suggestion:
When reading the new code in this file for edit feature, I was wondering about what the difference is between first load
vs initial load
What about renaming isInstanceFirstLoad
to isCreateInitialLoad
? It'd help to understand better the code for the different modes (edit, create)
isEditInitialLoad
isCreateInitialLoad
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.
This is actually an intentional naming dichotomy.
-
isInstanceFirstLoad
is a reference to the ODK XForms specifiedodk-instance-first-load
event. While we haven't yet implemented actions and events, the same event is referenced bypreload="uid"
. In the spec, this is a cue to its ordering semantics on form/instance load. In our code, it's a cue to the spec.The intent of this name is to express the spec semantics, both as they've been implemented and as the remaining aspects of actions/events will be introduced1.
-
isEditInitialLoad
is a special case, which doesn't correspond to any specified event (as its JSDoc suggests). That special case ispreload="uid"
. This isn't actually expressed anywhere in the ODK XForms spec (as far as I know), but it's the core assumption for the second half of this PR's semantics (first:instanceID
->deprecatedID
; second: new UUID ->instanceID
).The intent of this name is to express... the absence of
isInstanceFirstLoad
's intent. In other words, the intent is to specifically call it out as a special case, and disconnect the concept from any implication that it has a basis in spec event naming, or any parity withisInstanceFirstLoad
2.
Footnotes
-
Note: I think it's likely many of these aspects will be moved out of this module when we do implement actions/events. The naming as prep for that is still valuable because it leaves a clear breadcrumb for ordering of computations, which is an important detail we'll want to preserve, especially as the code is refactored. ↩
-
Other than their shared
is
prefix: they do have parity in being predicates, but any other parity between them is incidental. ↩
* - Update this type to be a union of those interfaces | ||
* - Implement that in {@link collectChildInputs} | ||
* - Update downstream construction to switch over whatever narrows the union | ||
* - Bonus points: revise eeach concrete {@link DescendantNode} to use a common |
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.
* - Bonus points: revise eeach concrete {@link DescendantNode} to use a common | |
* - Bonus points: revise each concrete {@link DescendantNode} to use a common |
* already have private constructors). Then downstream isn't even a switch | ||
* statement, it's just a lookup table. | ||
*/ | ||
export interface InstanceNodeChildInput { |
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.
Ok, sounds good, thanks for the highlight
|
||
const isMetaParent = (parent: GeneralParentNode): parent is MetaParent => { | ||
const { nodeType } = parent; | ||
switch (parent.nodeType) { |
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.
Comment:
I started to get a bit lost around here and couple of pieces below. I'll need to read this file again.
Quick (maybe silly) question to double check I'm not misunderstanding this. Does the "meta" in this file refer to this section from XML? Or a different meta object ?
<meta>
<instanceID/>
</meta>
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.
Yes. To be precise, the "meta" naming pattern throughout the module refers to that <meta>
element subtree, including its descendants, as "metadata about an instance" broadly. E.g.
<meta>
->MetaParent
(more in next paragraph)<meta><instanceID/>
(and<meta><deprecatedID/>
) -> "meta child"
I guess I should add to my "names I'm not thrilled about" list: MetaParent
. What I was looking for here was something referring to the <meta>
element... which is a subtree, but at this point in the flow we use the name "subtree" to distinguish from "group". The existing naming is probably a mistake (or at least, probably isn't clear enough), and my reluctance to address that now (out of scope, keeping scope smaller) leads straight to the awkward naming here.
Aside(?): "meta" naming convention thoughts
Another candidate for the MetaParent
name, probably the most obvious, would be MetaElement
(or MetaNode
). Unfortunately, those create ambiguity where the "meta" naming pattern applies also to its descendants.
Since I'm starting to ponder other names this morning, I'll start here: MetaSubroot
might be more clear.
Some other options:
- Accept that upstream naming is the problem and fix it now:
- Rename
SubtreeNode
(client interface) andSubtree
(internal implementation) toModelSubtreeNode
andModelSubtree
respectively - Rename
MetaParent
to its more natural name:MetaSubtree
(which could be either a "model subtree" or a "group", doesn't matter, it's still a "subtree")
- Rename
MetaParent
->MetaElement
+ some other naming for descendants
Aside(?): useless switch statement
I'm not sure if the confusion here began with this useless switch
statement that I forgot to delete. But that sure confused me seeing it with fresh eyes! I'm removing it now. In the future, I wouldn't be surprised if eslint-plugin-unicorn/no-useless-switch-case
could have caught this.
I don't know if there are other (erm) cases like this in the project. Seems unlikely, but there might be! I often try out a switch
first, before starting to write an if
statement like the one below (like the one below == "I know it will need to narrow cases of a union type" + "has a set of additional constraints on some narrowed subset of that union"). I just usually remember to back out of that when it doesn't seem like it'll add clarity. This is definitely an artifact of my brain being exhausted!
Note: the (recommended) orx-namespaced case passes. The unprefixed (supported default) case fails. Fixed in next commit.
…ris form’s This is a broad (correct) but hacky fix for the narrow failure in the previous commit
4a8cadd
to
2262b23
Compare
This prepares for testing 2+ sequenced edits of the same base instance. The tests themselves (to be committed next) exercise edit **chaining** semantics (by relating input -> output identifiers) at a high level. This change itself allows those high level tests to claim that they also exercise the lower level mechanical concern that `deprecatedID` is appended only once, and its value updated in subsequent edits.
2262b23
to
fc02d78
Compare
// Typical pattern: chained sequential edits form a linked list | ||
it('creates a chain of edited deprecatedID -> source instanceID references over subsequent edits', () => { | ||
const { source, edit1, edit2 } = scenarios; | ||
|
||
// Prerequisite | ||
assertChained(source, edit1); | ||
|
||
// Sanity/meaningfulness of test: edit2 is not chained from source | ||
assertNotChained(source, edit2); | ||
|
||
// Assert: edit2 is chained from edit1 | ||
assertChained(edit1, edit2); | ||
}); | ||
|
||
// Spec design/intent: chained branched edits form a tree | ||
it('creates a tree of edited deprecatedID -> source instanceID references over subsequent edits of a common ancestor instance', async () => { | ||
const { source, edit1: branch1, edit2: leaf1 } = scenarios; | ||
|
||
const branch2 = await source.proposed_editCurrentInstanceState(); | ||
const leaf2 = await branch2.proposed_editCurrentInstanceState(); | ||
|
||
assertChained(source, branch1); | ||
assertChained(source, branch2); | ||
assertChained(branch1, leaf1); | ||
assertChained(branch2, leaf2); | ||
|
||
assertNotChained(branch1, leaf2); | ||
assertNotChained(branch2, leaf1); | ||
}); |
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.
When I first wrote up this PR, I noticed I had forgotten to test that we don't append multiple deprecatedID
elements. As I went to test that, I realized that what was really lacking was this pair of spec semantics:
- sequenced edits produces a linked list
- concurrent edits produces a tree
I'm pretty sure this was discussed in a team meeting, around the time we started queueing up edit feature work. I hope these two tests clarify those cases!
I have verified this PR works in these browsers (latest versions):
I'm not entirely sure how I could validate this in-browser, without either a host app integration or a simulation of one. In any case, I don't think there's anything that should be browser specific here!
What else has been done to verify that this works as intended?
Leans heavily on testing (which caught a slightly embarrassing mistake dealing with namespaces!) and to some extent types (which guided most of the
deprecatedID
logic).Why is this the best possible solution? Were any other approaches considered?
Most of this is expanding on prior work (#349 and prior), so there wasn't much new to consider.
There's a big open question about a naming pattern introduced in this change. I'd like us to address it in review.
How does this change affect users? Describe intentional changes to behavior and behavior that could have accidentally been affected by code changes. In other words, what are the regression risks?
I can't think of any meaningful regression risks off the top of my head, but I might update this if any come to mind later.
Do we need any specific form for testing your changes? If so, please attach one.
N/A
What's changed
When
editInstance
is used to initialize instance state:instanceID
metadata element, its value is populated in adeprecatedID
metadata element...meta
parent andinstanceID
siblingpreload="uid"
[implied: for theinstanceID
metadata element], a new value is computed on load...instanceID
value is captured asdeprecatedID
(implicitly covered by the combined set of tests covering the above)