1 module text.xml.Encode; 2 3 import boilerplate.util; 4 import dxml.util; 5 import dxml.writer; 6 import meta.attributesOrNothing; 7 import std.array; 8 import std.meta; 9 import std.traits; 10 import std.typecons; 11 import sumtype : match, SumType; 12 import text.xml.Convert; 13 import text.xml.Xml; 14 15 /** 16 * The `text.xml.encode` function encodes an arbitrary type as XML. 17 * Each tagged field in the type is encoded. 18 * Tags are @(Xml.Attribute("attributeName")) and @(Xml.Element("tagName")). 19 * Types passed directly to `encode` must be annotated with an @(Xml.Element("...")) attribute. 20 * Child types must be annotated at their fields in the containing type. 21 * For array fields, their values are encoded sequentially. 22 * Nullable fields are omitted if they are null. 23 */ 24 public string encode(T)(const T value) 25 in 26 { 27 static if (is(T == class)) 28 { 29 assert(value !is null); 30 } 31 } 32 do 33 { 34 mixin enforceTypeHasElementTag!(T, "type passed to text.xml.encode"); 35 36 alias attributes = AliasSeq!(__traits(getAttributes, T)); 37 auto writer = xmlWriter(appender!string); 38 39 encodeNode!(T, attributes)(writer, value); 40 41 return writer.output.data; 42 } 43 44 private void encodeNode(T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value) 45 { 46 writer.openStartTag(attributes[udaIndex!(Xml.Element, attributes)].name, Newline.no); 47 48 // encode all the attribute members 49 static foreach (member; FilterMembers!(T, value, true)) 50 {{ 51 auto memberValue = __traits(getMember, value, member); 52 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 53 alias PlainMemberT = typeof(cast() memberValue); 54 enum name = memberAttrs[udaIndex!(Xml.Attribute, memberAttrs)].name; 55 56 static if (is(PlainMemberT : Nullable!Arg, Arg)) 57 { 58 if (!memberValue.isNull) 59 { 60 writer.writeAttr(name, encodeLeafImpl!(Arg, memberAttrs)(memberValue.get).encodeAttr, Newline.no); 61 } 62 } 63 else 64 { 65 writer.writeAttr(name, encodeLeafImpl!(PlainMemberT, memberAttrs)(memberValue).encodeAttr, Newline.no); 66 } 67 }} 68 69 bool tagIsEmpty = true; 70 71 static foreach (member; FilterMembers!(T, value, false)) 72 {{ 73 auto memberValue = __traits(getMember, value, member); 74 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 75 alias PlainMemberT = typeof(cast() memberValue); 76 enum hasXmlTag = udaIndex!(Xml.Element, memberAttrs) != -1 || udaIndex!(Xml.Text, memberAttrs) != -1; 77 enum isSumType = is(PlainMemberT : SumType!U, U...); 78 79 static if (hasXmlTag || isSumType) 80 { 81 static if (is(PlainMemberT : Nullable!Arg, Arg)) 82 { 83 if (!memberValue.isNull) 84 { 85 tagIsEmpty = false; 86 } 87 } 88 else 89 { 90 tagIsEmpty = false; 91 } 92 } 93 }} 94 95 writer.closeStartTag(tagIsEmpty ? EmptyTag.yes : EmptyTag.no); 96 97 if (!tagIsEmpty) 98 { 99 static foreach (member; FilterMembers!(T, value, false)) 100 {{ 101 auto memberValue = __traits(getMember, value, member); 102 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 103 104 static if (udaIndex!(Xml.Element, memberAttrs) != -1) 105 { 106 alias PlainMemberT = typeof(cast() memberValue); 107 enum name = memberAttrs[udaIndex!(Xml.Element, memberAttrs)].name; 108 109 encodeNodeImpl!(name, PlainMemberT, memberAttrs)(writer, memberValue); 110 } 111 else static if (udaIndex!(Xml.Text, memberAttrs) != -1) 112 { 113 writer.writeText(memberValue.encodeText, Newline.no); 114 } 115 else static if (is(typeof(cast() memberValue) : SumType!U, U...)) 116 { 117 encodeSumType(writer, memberValue); 118 } 119 }} 120 121 writer.writeEndTag(Newline.no); 122 } 123 } 124 125 private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T value) 126 { 127 value.match!(staticMap!((const value) { 128 alias T = typeof(value); 129 130 static if (is(T: U[], U)) 131 { 132 alias BaseType = U; 133 } 134 else 135 { 136 alias BaseType = T; 137 } 138 139 mixin enforceTypeHasElementTag!(BaseType, "every member type of SumType"); 140 141 alias attributes = AliasSeq!(__traits(getAttributes, BaseType)); 142 enum name = attributes[udaIndex!(Xml.Element, attributes)].name; 143 144 encodeNodeImpl!(name, T, attributes)(writer, value); 145 }, T.Types)); 146 } 147 148 private mixin template enforceTypeHasElementTag(T, string context) 149 { 150 static assert( 151 udaIndex!(Xml.Element, __traits(getAttributes, T)) != -1, 152 fullyQualifiedName!T ~ 153 ": " ~ context ~ " must have an Xml.Element attribute indicating its element name."); 154 } 155 156 private template FilterMembers(T, alias value, bool keepXmlAttributes) 157 { 158 alias pred = ApplyLeft!(attrFilter, value, keepXmlAttributes); 159 alias FilterMembers = Filter!(pred, __traits(derivedMembers, T)); 160 } 161 162 private template attrFilter(alias value, bool keepXmlAttributes, string member) 163 { 164 static if (__traits(compiles, { return __traits(getMember, value, member); })) 165 { 166 alias attributes = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 167 static if (keepXmlAttributes) 168 { 169 enum bool attrFilter = udaIndex!(Xml.Attribute, attributes) != -1; 170 } 171 else 172 { 173 enum bool attrFilter = udaIndex!(Xml.Attribute, attributes) == -1; 174 } 175 } 176 else 177 { 178 enum bool attrFilter = false; 179 } 180 } 181 182 private void encodeNodeImpl(string name, T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value) 183 { 184 alias PlainT = typeof(cast() value); 185 186 static if (__traits(compiles, __traits(getAttributes, T))) 187 { 188 alias typeAttributes = AliasSeq!(__traits(getAttributes, T)); 189 } 190 else 191 { 192 alias typeAttributes = AliasSeq!(); 193 } 194 195 static if (is(PlainT : Nullable!Arg, Arg)) 196 { 197 if (!value.isNull) 198 { 199 encodeNodeImpl!(name, Arg, attributes)(writer, value.get); 200 } 201 } 202 else static if (udaIndex!(Xml.Encode, attributes) != -1) 203 { 204 alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction; 205 206 writer.openStartTag(name, Newline.no); 207 writer.closeStartTag; 208 209 customEncoder(writer, value); 210 writer.writeEndTag(name, Newline.no); 211 } 212 else static if (udaIndex!(Xml.Encode, typeAttributes) != -1) 213 { 214 alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction; 215 216 writer.openStartTag(name, Newline.no); 217 writer.closeStartTag; 218 219 customEncoder(writer, value); 220 writer.writeEndTag(name, Newline.no); 221 } 222 else static if (isLeafType!(PlainT, attributes)) 223 { 224 writer.openStartTag(name, Newline.no); 225 writer.closeStartTag; 226 227 writer.writeText(encodeLeafImpl(value).encodeText, Newline.no); 228 writer.writeEndTag(name, Newline.no); 229 } 230 else static if (isIterable!PlainT) 231 { 232 alias IterationType(T) = typeof({ foreach (value; T.init) return value; assert(0); }()); 233 234 foreach (IterationType!PlainT a; value) 235 { 236 encodeNodeImpl!(name, typeof(a), attributes)(writer, a); 237 } 238 } 239 else 240 { 241 encodeNode!(PlainT, attributes)(writer, value); 242 } 243 } 244 245 // must match encodeLeafImpl 246 private enum bool isLeafType(T, attributes...) = 247 udaIndex!(Xml.Encode, attributes) != -1 248 || udaIndex!(Xml.Encode, attributesOrNothing!T) != -1 249 || is(T == string) 250 || __traits(compiles, { Convert.toString(T.init); }); 251 252 private string encodeLeafImpl(T, attributes...)(T value) 253 { 254 alias typeAttributes = attributesOrNothing!T; 255 256 static if (udaIndex!(Xml.Encode, attributes) != -1) 257 { 258 alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction; 259 260 return customEncoder(value); 261 } 262 else static if (udaIndex!(Xml.Encode, typeAttributes) != -1) 263 { 264 alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction; 265 266 return customEncoder(value); 267 } 268 else static if (is(T == string)) 269 { 270 return value; 271 } 272 else static if (__traits(compiles, Convert.toString(value))) 273 { 274 return Convert.toString(value); 275 } 276 else 277 { 278 static assert(false, "Unknown value type: " ~ T.stringof); 279 } 280 }