Skip to content

Commit

Permalink
engine (fix): include namespace declarations in submission XML
Browse files Browse the repository at this point in the history
  • Loading branch information
eyelidlessness committed Jan 28, 2025
1 parent a08e77b commit 0287a16
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 80 deletions.
7 changes: 7 additions & 0 deletions .changeset/healthy-jobs-look.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@getodk/xforms-engine': patch
'@getodk/web-forms': patch
'@getodk/scenario': patch
---

Fix: include namespace declarations in submission XML
74 changes: 53 additions & 21 deletions packages/scenario/src/serialization/ComparableXMLSerialization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { XFORMS_NAMESPACE_URI } from '@getodk/common/constants/xmlns.ts';
import {
XFORMS_NAMESPACE_URI,
XMLNS_NAMESPACE_URI,
XMLNS_PREFIX,
} from '@getodk/common/constants/xmlns.ts';
import { InspectableComparisonError } from '@getodk/common/test/assertions/helpers.ts';
import type { SimpleAssertionResult } from '@getodk/common/test/assertions/vitest/shared-extension-types.ts';
import { ComparableAssertableValue } from '../comparable/ComparableAssertableValue.ts';
Expand All @@ -8,38 +12,53 @@ class ComparableXMLQualifiedName {

constructor(
readonly namespaceURI: string | null,
readonly nodeName: string,
readonly localName: string
) {
this.sortKey = JSON.stringify({ namespaceURI, localName });
let namespaceDeclarationType: string;

if (namespaceURI === XMLNS_NAMESPACE_URI) {
if (nodeName === XMLNS_PREFIX) {
namespaceDeclarationType = 'default';
} else {
namespaceDeclarationType = 'non-default';
}
} else {
namespaceDeclarationType = 'none';
}

this.sortKey = JSON.stringify({
namespaceDeclarationType,
namespaceURI,
localName,
});
}

/**
* @todo prefix re-serialization
*/
toString(): string {
const { namespaceURI } = this;
const { namespaceURI, nodeName } = this;

if (namespaceURI == null || namespaceURI === XFORMS_NAMESPACE_URI) {
return this.localName;
}

return this.sortKey;
return nodeName;
}
}

class ComparableXMLAttribute {
static from(attr: Attr): ComparableXMLAttribute {
return new this(attr.namespaceURI, attr.localName, attr.value);
return new this(attr.namespaceURI, attr.nodeName, attr.localName, attr.value);
}

readonly qualifiedName: ComparableXMLQualifiedName;

private constructor(
namespaceURI: string | null,
nodeName: string,
localName: string,
readonly value: string
) {
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, localName);
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, nodeName, localName);
}

/**
Expand All @@ -59,17 +78,23 @@ const comparableXMLElementAttributes = (element: Element): readonly ComparableXM
return ComparableXMLAttribute.from(attr);
});

return attributes.sort(({ qualifiedName: a }, { qualifiedName: b }) => {
if (a > b) {
return 1;
return attributes.sort(
(
// prettier-ignore
{ qualifiedName: { sortKey: a } },
{ qualifiedName: { sortKey: b } }
) => {
if (a > b) {
return 1;
}

if (b > a) {
return -1;
}

return 0;
}

if (b > a) {
return -1;
}

return 0;
});
);
};

const isElement = (node: ChildNode): node is Element => {
Expand Down Expand Up @@ -118,18 +143,25 @@ class ComparableXMLElement {
const attributes = comparableXMLElementAttributes(element);
const children = comparableXMLElementChildren(element);

return new this(element.namespaceURI, element.localName, attributes, children);
return new this(
element.namespaceURI,
element.nodeName,
element.localName,
attributes,
children
);
}

readonly qualifiedName: ComparableXMLQualifiedName;

private constructor(
namespaceURI: string | null,
nodeName: string,
localName: string,
readonly attributes: readonly ComparableXMLAttribute[],
readonly children: readonly ComparableXMLElementChild[]
) {
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, localName);
this.qualifiedName = new ComparableXMLQualifiedName(namespaceURI, nodeName, localName);
}

toString(): string {
Expand Down
54 changes: 29 additions & 25 deletions packages/scenario/test/submission.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import {
OPENROSA_XFORMS_NAMESPACE_URI,
XFORMS_NAMESPACE_URI,
} from '@getodk/common/constants/xmlns.ts';
import {
bind,
body,
Expand Down Expand Up @@ -141,7 +145,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-basic-default-values"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-basic-default-values"`,
t('grp',
t('inp', defaults.inp ?? ''),
t('sel1', defaults.sel1 ?? ''),
Expand Down Expand Up @@ -186,7 +190,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t(`data id="${formId}" version="${version}"`,
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="${formId}" version="${version}"`,
t('inp', 'val'),
t('meta',
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
Expand Down Expand Up @@ -222,7 +226,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t(`data id="${formId}" orx:version="${version}"`,
t(`data xmlns="${XFORMS_NAMESPACE_URI}" xmlns:orx="${OPENROSA_XFORMS_NAMESPACE_URI}" id="${formId}" orx:version="${version}"`,
t('inp', 'val'),
t('meta',
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
Expand Down Expand Up @@ -258,7 +262,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t(`data id="${formId}" orx:version="${version}"`,
t(`data xmlns="${XFORMS_NAMESPACE_URI}" xmlns:orx="${OPENROSA_XFORMS_NAMESPACE_URI}" id="${formId}" orx:version="${version}"`,
t('inp', 'val'),
t('meta',
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
Expand Down Expand Up @@ -312,7 +316,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="unicode-normalization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="unicode-normalization"`,
t('rep',
t('inp', composed)),
t('meta',
Expand All @@ -327,7 +331,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="unicode-normalization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="unicode-normalization"`,
t('rep',
t('inp', composed)),
t('meta',
Expand Down Expand Up @@ -365,7 +369,7 @@ describe('Form submission', () => {
it('does not serialize an element for a repeat range', () => {
expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-repeats"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
t('meta',
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
);
Expand All @@ -379,7 +383,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-repeats"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
t('rep',
t('inp', 'a')),
t('rep',
Expand All @@ -392,7 +396,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-repeats"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-repeats"`,
t('rep',
t('inp', 'b')),
t('meta',
Expand Down Expand Up @@ -443,7 +447,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-relevance"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-relevance"`,
t('grp-rel', '1'),
t('inp-rel', '0'),
t('grp'),
Expand All @@ -457,7 +461,7 @@ describe('Form submission', () => {

expect(scenario).toHaveSerializedSubmissionXML(
// prettier-ignore
t('data id="xml-serialization-relevance"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="xml-serialization-relevance"`,
t('grp-rel', '0'),
t('inp-rel', '1'),
t('meta',
Expand Down Expand Up @@ -510,7 +514,7 @@ describe('Form submission', () => {
// Default serialization before any state change
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp')),
Expand All @@ -525,7 +529,7 @@ describe('Form submission', () => {
// After first value change
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp', `${i}`)),
Expand All @@ -545,7 +549,7 @@ describe('Form submission', () => {
// Default serialization before any state change
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp')),
Expand All @@ -558,7 +562,7 @@ describe('Form submission', () => {
// First repeat instance added
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp')),
Expand All @@ -573,7 +577,7 @@ describe('Form submission', () => {
// Second repeat instance added
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp')),
Expand All @@ -592,7 +596,7 @@ describe('Form submission', () => {
// Each of the above values set
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp', 'rep 1 inp')),
Expand All @@ -609,7 +613,7 @@ describe('Form submission', () => {
// Last repeat instance removed
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp', 'rep 1 inp')),
Expand All @@ -624,7 +628,7 @@ describe('Form submission', () => {
// First repeat instance removed
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp', 'rep 2 inp')),
Expand All @@ -637,7 +641,7 @@ describe('Form submission', () => {
// All repeat instances removed
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('meta',
t('instanceID', DEFAULT_INSTANCE_ID))).asXml()
Expand All @@ -660,7 +664,7 @@ describe('Form submission', () => {
// Current serialization before any relevance change
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel'),
t('rep',
t('inp', 'rep 1 inp')),
Expand All @@ -677,7 +681,7 @@ describe('Form submission', () => {
// Non-relevant /data/rep[position() != '1']/inp omitted
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel', '1'),
t('rep',
t('inp', 'rep 1 inp')),
Expand All @@ -692,7 +696,7 @@ describe('Form submission', () => {
// Non-relevant /data/rep[position() != '3']/inp omitted
expect(serialized).toBe(
// prettier-ignore
t('data id="reactive-xml-serialization"',
t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="reactive-xml-serialization"`,
t('rep-inp-rel', '3'),
t('rep'),
t('rep'),
Expand Down Expand Up @@ -842,7 +846,7 @@ describe('Form submission', () => {
expect(scenario.getValidationOutcome().outcome).toBe(ANSWER_OK);

// prettier-ignore
validSubmissionXML = t('data id="prepare-for-submission"',
validSubmissionXML = t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="prepare-for-submission"`,
t('rep',
t('inp', 'rep 1 inp')),
t('rep',
Expand Down Expand Up @@ -879,7 +883,7 @@ describe('Form submission', () => {
expect(scenario.getValidationOutcome().outcome).toBe(ANSWER_REQUIRED_BUT_EMPTY);

// prettier-ignore
invalidSubmissionXML = t('data id="prepare-for-submission"',
invalidSubmissionXML = t(`data xmlns="${XFORMS_NAMESPACE_URI}" id="prepare-for-submission"`,
t('rep',
t('inp', 'rep 1 inp')),
t('rep',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { serializeParentElementXML } from '../../xml-serialization.ts';
export const createRootSubmissionState = (node: Root): SubmissionState => {
return {
get submissionXML() {
const { namespaceDeclarations, attributes } = node.definition;
const serializedChildren = node.currentState.children.map((child) => {
return child.submissionState.submissionXML;
});

return serializeParentElementXML(node.definition.nodeName, serializedChildren, {
attributes: node.definition.attributes,
namespaceDeclarations,
attributes,
});
},
};
Expand Down
Loading

0 comments on commit 0287a16

Please sign in to comment.