From 67dc524f717e613a0d5d59cdd86cd39416a23ea1 Mon Sep 17 00:00:00 2001 From: Mathis Beer Date: Thu, 8 Aug 2024 16:33:30 +0200 Subject: [PATCH] XML: Add `void encode(value, sink);` to encode to an output stream. --- src/text/xml/Encode.d | 39 +- unittest/text/xml/EncodeTest.d | 642 +++++++++++++++++---------------- 2 files changed, 366 insertions(+), 315 deletions(-) diff --git a/src/text/xml/Encode.d b/src/text/xml/Encode.d index c215891..1f09b6d 100644 --- a/src/text/xml/Encode.d +++ b/src/text/xml/Encode.d @@ -37,12 +37,31 @@ do alias attributes = AliasSeq!(__traits(getAttributes, T)); auto writer = xmlWriter(appender!string); - encodeNode!(T, attributes)(writer, value); + encodeNode!(T, Appender!string, attributes)(writer, value); return writer.output.data; } -private void encodeNode(T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value) +/// Ditto. +public void encode(T, Writer)(const T value, ref Writer writer) +in +{ + static if (is(T == class)) + { + assert(value !is null); + } +} +do +{ + mixin enforceTypeHasElementTag!(T, "type passed to text.xml.encode"); + + alias attributes = AliasSeq!(__traits(getAttributes, T)); + auto xmlWriter = .xmlWriter(writer); + + encodeNode!(T, Writer, attributes)(xmlWriter, value); +} + +private void encodeNode(T, Writer, attributes...)(ref XMLWriter!Writer writer, const T value) { enum elementName = Xml.elementName!attributes(typeName!T).get; @@ -130,7 +149,7 @@ private void encodeNode(T, attributes...)(ref XMLWriter!(Appender!string) writer { enum string nameGet__ = name.get; // work around for weird compiler bug - encodeNodeImpl!(nameGet__, PlainMemberT, useDefault, memberAttrs)(writer, memberValue); + encodeNodeImpl!(nameGet__, PlainMemberT, Writer, useDefault, memberAttrs)(writer, memberValue); } else static if (udaIndex!(Xml.Text, memberAttrs) != -1) { @@ -146,7 +165,7 @@ private void encodeNode(T, attributes...)(ref XMLWriter!(Appender!string) writer } } -private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T value) +private void encodeSumType(T, Writer)(ref XMLWriter!Writer writer, const T value) { value.match!(staticMap!((const value) { alias T = typeof(value); @@ -165,7 +184,7 @@ private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T va alias attributes = AliasSeq!(__traits(getAttributes, BaseType)); enum name = Xml.elementName!attributes(typeName!BaseType).get; - encodeNodeImpl!(name, T, false, attributes)(writer, value); + encodeNodeImpl!(name, T, Writer, false, attributes)(writer, value); }, T.Types)); } @@ -219,8 +238,8 @@ unittest static assert(attrFilter!(s, false, "T") == false); } -private void encodeNodeImpl(string name, T, bool useDefault, attributes...)(ref XMLWriter!(Appender!string) writer, - const T value) +private void encodeNodeImpl(string name, T, Writer, bool useDefault, attributes...)( + ref XMLWriter!Writer writer, const T value) { alias PlainT = typeof(cast() value); @@ -237,7 +256,7 @@ private void encodeNodeImpl(string name, T, bool useDefault, attributes...)(ref { if (!value.isNull) { - encodeNodeImpl!(name, Arg, false, attributes)(writer, value.get); + encodeNodeImpl!(name, Arg, Writer, false, attributes)(writer, value.get); } else if (!useDefault) { @@ -280,12 +299,12 @@ private void encodeNodeImpl(string name, T, bool useDefault, attributes...)(ref foreach (IterationType!PlainT a; value) { - encodeNodeImpl!(name, typeof(a), false, attributes)(writer, a); + encodeNodeImpl!(name, typeof(a), Writer, false, attributes)(writer, a); } } else { - encodeNode!(PlainT, attributes)(writer, value); + encodeNode!(PlainT, Writer, attributes)(writer, value); } } diff --git a/unittest/text/xml/EncodeTest.d b/unittest/text/xml/EncodeTest.d index 1a38048..eac3e0e 100644 --- a/unittest/text/xml/EncodeTest.d +++ b/unittest/text/xml/EncodeTest.d @@ -12,211 +12,399 @@ import text.xml.Tree; import text.xml.Writer; import text.xml.Xml; -@("fields tagged as Element are encoded as XML elements") -unittest +// All the tests are executed with both `encode(value)` (encodes to a string) +// and `encode(value, writer)` (encodes to a writer). +static foreach (bool streamEncode; [false, true]) { - const expected = - `` ~ - `23` ~ - `FOO` ~ - `true` ~ - `` ~ - `BAR` ~ - `` ~ - `1` ~ - `2` ~ - `3` ~ - `2000-01-02` ~ - `2000-01-02T10:00:00Z` ~ - `World` ~ - `` - ; - - // given - const value = (){ - import text.time.Convert : Convert; - - with (Value.Builder()) + mixin encodeTests!(streamEncode); +} + +template encodeTests(bool streamEncode) +{ + static if (streamEncode) + { + enum prefix = "stream encode"; + + string testEncode(T)(T value) { - intValue = 23; - stringValue = "FOO"; - boolValue = true; - nestedValue = NestedValue("BAR"); - arrayValue = [1, 2, 3]; - dateValue = Date(2000, 1, 2); - sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z"); - contentValue = ContentValue("hello", "World"); - return value; + auto writer = appender!string(); + + .encode(value, writer); + return writer[]; } - }(); + } + else + { + enum prefix = "string encode"; - // when - auto text = encode(value); + string testEncode(T)(T value) + { + return .encode(value); + } + } - // then - text.should.equal(expected); -} + @(prefix ~ ": fields tagged as Element are encoded as XML elements") + unittest + { + const expected = + `` ~ + `23` ~ + `FOO` ~ + `true` ~ + `` ~ + `BAR` ~ + `` ~ + `1` ~ + `2` ~ + `3` ~ + `2000-01-02` ~ + `2000-01-02T10:00:00Z` ~ + `World` ~ + `` + ; + + // given + const value = (){ + import text.time.Convert : Convert; + + with (Value.Builder()) + { + intValue = 23; + stringValue = "FOO"; + boolValue = true; + nestedValue = NestedValue("BAR"); + arrayValue = [1, 2, 3]; + dateValue = Date(2000, 1, 2); + sysTimeValue = SysTime.fromISOExtString("2000-01-02T10:00:00Z"); + contentValue = ContentValue("hello", "World"); + return value; + } + }(); + + // when + auto text = testEncode(value); + + // then + text.should.equal(expected); + } -@("fields tagged as Attribute are encoded as XML attributes") -unittest -{ - const expected = ``; + @(prefix ~ ": fields tagged as Attribute are encoded as XML attributes") + unittest + { + const expected = ``; - // given - const valueWithAttribute = ValueWithAttribute(23); + // given + const valueWithAttribute = ValueWithAttribute(23); - // when - auto text = encode(valueWithAttribute); + // when + auto text = testEncode(valueWithAttribute); - // then - text.should.equal(expected); -} + // then + text.should.equal(expected); + } -@("enum field with underscore") -unittest -{ - // given - enum Enum + @(prefix ~ ": enum field with underscore") + unittest { - void_, - foo_, + // given + enum Enum + { + void_, + foo_, + } + + @(Xml.Element("element")) + struct Element + { + @(Xml.Attribute("enum")) + public Enum enum1; + + @(Xml.Element("enum")) + public Enum enum2; + + mixin(GenerateAll); + } + + const expected = `void`; + + // when + auto text = testEncode(Element(Enum.void_, Enum.void_)); + + // then + text.should.equal(expected); } - @(Xml.Element("element")) - struct Element + @(prefix ~ ": custom encoders are used on fields") + unittest { - @(Xml.Attribute("enum")) - public Enum enum1; + // given + const value = ValueWithEncoders("bla", "bla"); + + // when + auto text = testEncode(value); - @(Xml.Element("enum")) - public Enum enum2; + // then + const expected = `bar`; - mixin(GenerateAll); + text.should.equal(expected); } - const expected = `void`; + @(prefix ~ ": custom encoders are used on types") + unittest + { + @(Xml.Element("root")) + struct Value + { + @(Xml.Element("foo")) + EncodeNodeTestType foo; + + @(Xml.Attribute("bar")) + EncodeAttributeTestType bar; + } - // when - auto text = encode(Element(Enum.void_, Enum.void_)); + // given + const value = Value(EncodeNodeTestType(), EncodeAttributeTestType()); - // then - text.should.equal(expected); -} + // when + auto text = testEncode(value); -@("custom encoders are used on fields") -unittest -{ - // given - const value = ValueWithEncoders("bla", "bla"); + // then + const expected = `123`; - // when - auto text = encode(value); + text.should.equal(expected); + } - // then - const expected = `bar`; + @(prefix ~ ": custom encoder on Nullable element") + unittest + { + @(Xml.Element("root")) + struct Value + { + @(Xml.Element("foo")) + @(Xml.Encode!encodeNodeTestType) + Nullable!EncodeNodeTestType foo; + } - text.should.equal(expected); -} + // given + const value = Value(Nullable!EncodeNodeTestType()); -@("custom encoders are used on types") -unittest -{ - @(Xml.Element("root")) - struct Value + // when + const text = testEncode(value); + + // then + const expected = ``; + + text.should.equal(expected); + } + + @(prefix ~ ": fields with characters requiring predefined entities") + unittest { - @(Xml.Element("foo")) - EncodeNodeTestType foo; + @(Xml.Element("root")) + struct Value + { + @(Xml.Attribute("foo")) + string foo; + + @(Xml.Element("bar")) + string bar; + } + + // given + enum invalidInAttr = `<&"`; + enum invalidInText = `<&]]>`; + const value = Value(invalidInAttr, invalidInText); + + // when + auto text = testEncode(value); + + // then + const expected = `<&]]>`; - @(Xml.Attribute("bar")) - EncodeAttributeTestType bar; + text.should.equal(expected); } - // given - const value = Value(EncodeNodeTestType(), EncodeAttributeTestType()); + @(prefix ~ ": regression: encodes optional elements with arrays") + unittest + { + struct Nested + { + @(Xml.Element("item")) + string[] items; + } + + @(Xml.Element("root")) + struct Root + { + @(Xml.Element("foo")) + Nullable!Nested nested; + } - // when - auto text = .encode(value); + // given + const root = Root(Nullable!Nested(Nested(["foo", "bar"]))); - // then - const expected = `123`; + // when + const text = root.testEncode; - text.should.equal(expected); -} + // then + text.should.equal(`foobar`); + } -@("custom encoder on Nullable element") -unittest -{ - @(Xml.Element("root")) - struct Value + @(prefix ~ ": struct with optional date attribute") + unittest { - @(Xml.Element("foo")) - @(Xml.Encode!encodeNodeTestType) - Nullable!EncodeNodeTestType foo; + @(Xml.Element("root")) + static struct NullableAttributes + { + @(Xml.Attribute("date")) + @(This.Default) + Nullable!Date date; + + mixin(GenerateThis); + } + + // given + const root = NullableAttributes(); + + // when + const text = root.testEncode; + + // then + text.should.equal(``); } - // given - const value = Value(Nullable!EncodeNodeTestType()); + @(prefix ~ ": struct with optional date element") + unittest + { + @(Xml.Element("root")) + static struct NullableAttributes + { + @(This.Default) + @(Xml.Element("date")) + Nullable!Date date; - // when - const text = .encode(value); + mixin(GenerateThis); + } - // then - const expected = ``; + // given + const root = NullableAttributes(); - text.should.equal(expected); -} + // when + const text = root.testEncode; -@("fields with characters requiring predefined entities") -unittest -{ - @(Xml.Element("root")) - struct Value + // then + text.should.equal(``); + } + + @(prefix ~ ": struct with empty date element") + unittest { - @(Xml.Attribute("foo")) - string foo; + @(Xml.Element("root")) + static struct NullableAttributes + { + @(Xml.Element("date")) + Nullable!Date date; + + mixin(GenerateThis); + } + + // given + const root = NullableAttributes(); - @(Xml.Element("bar")) - string bar; + // when + const text = root.testEncode; + + // then + text.should.equal(``); } - // given - enum invalidInAttr = `<&"`; - enum invalidInText = `<&]]>`; - const value = Value(invalidInAttr, invalidInText); + @(prefix ~ ": SumType") + unittest + { + with (SumTypeFixture) + { + alias Either = SumType!(A, B); + + @(Xml.Element("root")) + static struct Struct + { + Either field; - // when - auto text = .encode(value); + mixin(GenerateThis); + } - // then - const expected = `<&]]>`; + // given/when/then + Struct(Either(A(5))).testEncode.should.equal(``); - text.should.equal(expected); -} + Struct(Either(B(3))).testEncode.should.equal(``); + } + } -@("regression: encodes optional elements with arrays") -unittest -{ - struct Nested + @(prefix ~ ": SumType with arrays") + unittest { - @(Xml.Element("item")) - string[] items; + with (SumTypeFixture) + { + alias Either = SumType!(A[], B[]); + + @(Xml.Element("root")) + static struct Struct + { + Either field; + + mixin(GenerateThis); + } + + // given/when/then + Struct(Either([A(5), A(6)])).testEncode.should.equal(``); + } } - @(Xml.Element("root")) - struct Root + @(prefix ~ ": attribute/element without specified name") + unittest { - @(Xml.Element("foo")) - Nullable!Nested nested; + struct Value + { + @(Xml.Attribute) + private int value_; + + mixin(GenerateThis); + } + + @(Xml.Element) + struct Container + { + @(Xml.Element) + immutable(Value)[] values; + + mixin(GenerateThis); + } + + // when + const text = Container([Value(1), Value(2), Value(3)]).testEncode; + + // then + text.should.equal(``); } - // given - const root = Root(Nullable!Nested(Nested(["foo", "bar"]))); + @(prefix ~ ": SysTime as text") + unittest + { + @(Xml.Element) + struct Value + { + @(Xml.Text) + SysTime time; + + mixin(GenerateThis); + } - // when - const text = root.encode; + // when + const text = Value(SysTime.fromISOExtString("2003-02-01T12:00:00")).testEncode; - // then - text.should.equal(`foobar`); + // then + text.should.equal(`2003-02-01T12:00:00`); + } } @(Xml.Element("root")) @@ -326,116 +514,6 @@ package struct EncodeAttributeTestType { } -@("struct with optional date attribute") -unittest -{ - @(Xml.Element("root")) - static struct NullableAttributes - { - @(Xml.Attribute("date")) - @(This.Default) - Nullable!Date date; - - mixin(GenerateThis); - } - - // given - const root = NullableAttributes(); - - // when - const text = root.encode; - - // then - text.should.equal(``); -} - -@("struct with optional date element") -unittest -{ - @(Xml.Element("root")) - static struct NullableAttributes - { - @(This.Default) - @(Xml.Element("date")) - Nullable!Date date; - - mixin(GenerateThis); - } - - // given - const root = NullableAttributes(); - - // when - const text = root.encode; - - // then - text.should.equal(``); -} - -@("struct with empty date element") -unittest -{ - @(Xml.Element("root")) - static struct NullableAttributes - { - @(Xml.Element("date")) - Nullable!Date date; - - mixin(GenerateThis); - } - - // given - const root = NullableAttributes(); - - // when - const text = root.encode; - - // then - text.should.equal(``); -} - -@("SumType") -unittest -{ - with (SumTypeFixture) - { - alias Either = SumType!(A, B); - - @(Xml.Element("root")) - static struct Struct - { - Either field; - - mixin(GenerateThis); - } - - // given/when/then - Struct(Either(A(5))).encode.should.equal(``); - - Struct(Either(B(3))).encode.should.equal(``); - } -} - -@("SumType with arrays") -unittest -{ - with (SumTypeFixture) - { - alias Either = SumType!(A[], B[]); - - @(Xml.Element("root")) - static struct Struct - { - Either field; - - mixin(GenerateThis); - } - - // given/when/then - Struct(Either([A(5), A(6)])).encode.should.equal(``); - } -} - private struct SumTypeFixture { @(Xml.Element("A")) @@ -452,49 +530,3 @@ private struct SumTypeFixture int b; } } - -@("attribute/element without specified name") -unittest -{ - struct Value - { - @(Xml.Attribute) - private int value_; - - mixin(GenerateThis); - } - - @(Xml.Element) - struct Container - { - @(Xml.Element) - immutable(Value)[] values; - - mixin(GenerateThis); - } - - // when - const text = Container([Value(1), Value(2), Value(3)]).encode; - - // then - text.should.equal(``); -} - -@("SysTime as text") -unittest -{ - @(Xml.Element) - struct Value - { - @(Xml.Text) - SysTime time; - - mixin(GenerateThis); - } - - // when - const text = Value(SysTime.fromISOExtString("2003-02-01T12:00:00")).encode; - - // then - text.should.equal(`2003-02-01T12:00:00`); -}