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
32 changes: 32 additions & 0 deletions codecs/xml-codec/model/test.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -272,3 +272,35 @@ structure AllListsStruct {
blobs: BlobList
timestamps: TimestampList
}

structure DenseListStruct {
structs: StructItemList
strings: PlainStringList
}

structure SparseListStruct {
structs: SparseStructItemList
strings: SparsePlainStringList
}

structure StructItem {
id: String
}

list StructItemList {
member: StructItem
}

@sparse
list SparseStructItemList {
member: StructItem
}

list PlainStringList {
member: String
}

@sparse
list SparsePlainStringList {
member: String
}
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,12 @@ public Instant readTimestamp(Schema schema) {
@Override
public boolean isNull() {
try {
return reader.getText().isEmpty();
// An element is null only when it is genuinely empty: no text content AND no child
// elements. getText() returns "" both for a truly empty element (<x/> or <x></x>)
// and for an element whose content is entirely child elements, e.g. a structure
// (<x><child>..</child></x>). To tell them apart, require that consuming the text left
// us on this element's own end tag rather than on a nested child node.
return reader.getText().isEmpty() && reader.atEndElement();
} catch (XMLStreamException e) {
throw error("Failed to determine if value is null", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ final String getText() throws XMLStreamException {
return textReader.toString();
}

final boolean atEndElement() throws XMLStreamException {
nextIfNeeded();
return getEventType() == XMLStreamConstants.END_ELEMENT;
}

private static boolean readNextString(int event) {
return switch (event) {
case XMLStreamReader.CHARACTERS, XMLStreamConstants.CDATA -> true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import smithy.java.xml.test.model.BlobStruct;
import smithy.java.xml.test.model.Color;
import smithy.java.xml.test.model.ComplexStruct;
import smithy.java.xml.test.model.DenseListStruct;
import smithy.java.xml.test.model.FlattenedListStruct;
import smithy.java.xml.test.model.FlattenedMapStruct;
import smithy.java.xml.test.model.InnerStruct;
Expand All @@ -32,7 +33,9 @@
import smithy.java.xml.test.model.NumericStruct;
import smithy.java.xml.test.model.RecursiveStruct;
import smithy.java.xml.test.model.SimpleStruct;
import smithy.java.xml.test.model.SparseListStruct;
import smithy.java.xml.test.model.StringStruct;
import smithy.java.xml.test.model.StructItem;
import smithy.java.xml.test.model.TimestampStruct;
import smithy.java.xml.test.model.XmlAttributeStruct;
import smithy.java.xml.test.model.XmlNameStruct;
Expand Down Expand Up @@ -739,6 +742,69 @@ void selfClosingElementsInListSkippedAsNull() {
assertThat(nativeResult.getTags()).containsExactly(null, "hello", null);
}

@PerProvider
void denseStructListDeserializes(boolean useNative) {
String xml = "<DenseListStruct><structs><member><id>abcd</id></member></structs></DenseListStruct>";
var result = deserialize(useNative, xml, DenseListStruct.builder());
assertThat(result.getStructs()).hasSize(1);
assertThat(result.getStructs().get(0).getId()).isEqualTo("abcd");
}

@PerProvider
void prettyPrintedStructListDeserializes(boolean useNative) {
String xml = """
<DenseListStruct>
<structs>
<member>
<id>abcd</id>
</member>
</structs>
</DenseListStruct>
""";
var result = deserialize(useNative, xml, DenseListStruct.builder());
assertThat(result.getStructs()).hasSize(1);
assertThat(result.getStructs().get(0).getId()).isEqualTo("abcd");
}

// Both empty forms, <member/> and <member></member>, are null and rejected by a dense list.
@PerProvider
void denseStructListNullMemberThrows(boolean useNative) {
String singleTag = "<DenseListStruct><structs><member/></structs></DenseListStruct>";
assertThatThrownBy(() -> deserialize(useNative, singleTag, DenseListStruct.builder()))
.isInstanceOf(SerializationException.class);
String separateTag = "<DenseListStruct><structs><member></member></structs></DenseListStruct>";
assertThatThrownBy(() -> deserialize(useNative, separateTag, DenseListStruct.builder()))
.isInstanceOf(SerializationException.class);
}

@PerProvider
void sparseStructListNullMemberIsNull(boolean useNative) {
String xml = "<SparseListStruct><structs><member/></structs></SparseListStruct>";
var result = deserialize(useNative, xml, SparseListStruct.builder());
assertThat(result.getStructs()).containsExactly((StructItem) null);
}

@PerProvider
void densePopulatedStringListDeserializes(boolean useNative) {
String xml = "<DenseListStruct><strings><member>a</member><member>x</member></strings></DenseListStruct>";
var result = deserialize(useNative, xml, DenseListStruct.builder());
assertThat(result.getStrings()).containsExactly("a", "x");
}

@PerProvider
void denseStringListEmptyMemberThrows(boolean useNative) {
String xml = "<DenseListStruct><strings><member></member><member>x</member></strings></DenseListStruct>";
assertThatThrownBy(() -> deserialize(useNative, xml, DenseListStruct.builder()))
.isInstanceOf(SerializationException.class);
}

@PerProvider
void sparseStringListEmptyMemberIsNull(boolean useNative) {
String xml = "<SparseListStruct><strings><member></member><member>x</member></strings></SparseListStruct>";
var result = deserialize(useNative, xml, SparseListStruct.builder());
assertThat(result.getStrings()).containsExactly(null, "x");
}

@PerProvider
void trailingContentAfterRootIsRejected(boolean useNative) {
String xml = "<SimpleStruct><name>hi</name><age>1</age></SimpleStruct>extra";
Expand Down
Loading