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 enum elementName = Xml.elementName!attributes(typeName!T).get; 47 48 writer.openStartTag(elementName, Newline.no); 49 50 // encode all the attribute members 51 static foreach (member; FilterMembers!(T, value, true)) 52 {{ 53 auto memberValue = __traits(getMember, value, member); 54 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 55 alias PlainMemberT = typeof(cast() memberValue); 56 enum name = Xml.attributeName!memberAttrs(optionallyRemoveTrailingUnderline!member).get; 57 58 static if (is(PlainMemberT : Nullable!Arg, Arg)) 59 { 60 if (!memberValue.isNull) 61 { 62 writer.writeAttr(name, encodeLeafImpl!(Arg, memberAttrs)(memberValue.get).encodeAttr, Newline.no); 63 } 64 } 65 else 66 { 67 writer.writeAttr(name, encodeLeafImpl!(PlainMemberT, memberAttrs)(memberValue).encodeAttr, Newline.no); 68 } 69 }} 70 71 bool tagIsEmpty = true; 72 73 static foreach (member; FilterMembers!(T, value, false)) 74 {{ 75 auto memberValue = __traits(getMember, value, member); 76 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 77 alias PlainMemberT = typeof(cast() memberValue); 78 enum hasXmlTag = !Xml.elementName!memberAttrs(typeName!PlainMemberT).isNull 79 || udaIndex!(Xml.Text, memberAttrs) != -1; 80 enum isSumType = is(PlainMemberT : SumType!U, U...); 81 82 static if (hasXmlTag || isSumType) 83 { 84 static if (is(PlainMemberT : Nullable!Arg, Arg)) 85 { 86 if (!memberValue.isNull) 87 { 88 tagIsEmpty = false; 89 } 90 } 91 else 92 { 93 tagIsEmpty = false; 94 } 95 } 96 }} 97 98 writer.closeStartTag(tagIsEmpty ? EmptyTag.yes : EmptyTag.no); 99 100 if (!tagIsEmpty) 101 { 102 static foreach (member; FilterMembers!(T, value, false)) 103 {{ 104 auto memberValue = __traits(getMember, value, member); 105 alias memberAttrs = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 106 alias PlainMemberT = typeof(cast() memberValue); 107 enum name = Xml.elementName!memberAttrs(typeName!PlainMemberT); 108 109 static if (!name.isNull) 110 { 111 enum string nameGet__ = name.get; // work around for weird compiler bug 112 113 encodeNodeImpl!(nameGet__, PlainMemberT, memberAttrs)(writer, memberValue); 114 } 115 else static if (udaIndex!(Xml.Text, memberAttrs) != -1) 116 { 117 writer.writeText(memberValue.encodeText, Newline.no); 118 } 119 else static if (is(typeof(cast() memberValue) : SumType!U, U...)) 120 { 121 encodeSumType(writer, memberValue); 122 } 123 }} 124 125 writer.writeEndTag(Newline.no); 126 } 127 } 128 129 private void encodeSumType(T)(ref XMLWriter!(Appender!string) writer, const T value) 130 { 131 value.match!(staticMap!((const value) { 132 alias T = typeof(value); 133 134 static if (is(T: U[], U)) 135 { 136 alias BaseType = U; 137 } 138 else 139 { 140 alias BaseType = T; 141 } 142 143 mixin enforceTypeHasElementTag!(BaseType, "every member type of SumType"); 144 145 alias attributes = AliasSeq!(__traits(getAttributes, BaseType)); 146 enum name = Xml.elementName!attributes(typeName!BaseType).get; 147 148 encodeNodeImpl!(name, T, attributes)(writer, value); 149 }, T.Types)); 150 } 151 152 private mixin template enforceTypeHasElementTag(T, string context) 153 { 154 static assert( 155 !Xml.elementName!(__traits(getAttributes, T))(typeName!T).isNull, 156 fullyQualifiedName!T ~ 157 ": " ~ context ~ " must have an Xml.Element attribute indicating its element name."); 158 } 159 160 private enum typeName(T) = typeof(cast() T.init).stringof; 161 162 private template FilterMembers(T, alias value, bool keepXmlAttributes) 163 { 164 alias pred = ApplyLeft!(attrFilter, value, keepXmlAttributes); 165 alias FilterMembers = Filter!(pred, __traits(derivedMembers, T)); 166 } 167 168 private template attrFilter(alias value, bool keepXmlAttributes, string member) 169 { 170 // double-check that the member has a type to work around https://issues.dlang.org/show_bug.cgi?id=22214 171 static if (is(typeof(__traits(getMember, value, member))) 172 && __traits(compiles, { auto value = __traits(getMember, value, member); })) 173 { 174 alias attributes = AliasSeq!(__traits(getAttributes, __traits(getMember, value, member))); 175 static if (keepXmlAttributes) 176 { 177 enum bool attrFilter = !Xml.attributeName!(attributes)("").isNull; 178 } 179 else 180 { 181 enum bool attrFilter = Xml.attributeName!(attributes)("").isNull; 182 } 183 } 184 else 185 { 186 enum bool attrFilter = false; 187 } 188 } 189 190 // test for https://issues.dlang.org/show_bug.cgi?id=22214 191 unittest 192 { 193 static struct S 194 { 195 struct T { } 196 } 197 S s; 198 static assert(attrFilter!(s, false, "T") == false); 199 } 200 201 private void encodeNodeImpl(string name, T, attributes...)(ref XMLWriter!(Appender!string) writer, const T value) 202 { 203 alias PlainT = typeof(cast() value); 204 205 static if (__traits(compiles, __traits(getAttributes, T))) 206 { 207 alias typeAttributes = AliasSeq!(__traits(getAttributes, T)); 208 } 209 else 210 { 211 alias typeAttributes = AliasSeq!(); 212 } 213 214 static if (is(PlainT : Nullable!Arg, Arg)) 215 { 216 if (!value.isNull) 217 { 218 encodeNodeImpl!(name, Arg, attributes)(writer, value.get); 219 } 220 } 221 else static if (udaIndex!(Xml.Encode, attributes) != -1) 222 { 223 alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction; 224 225 writer.openStartTag(name, Newline.no); 226 writer.closeStartTag; 227 228 customEncoder(writer, value); 229 writer.writeEndTag(name, Newline.no); 230 } 231 else static if (udaIndex!(Xml.Encode, typeAttributes) != -1) 232 { 233 alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction; 234 235 writer.openStartTag(name, Newline.no); 236 writer.closeStartTag; 237 238 customEncoder(writer, value); 239 writer.writeEndTag(name, Newline.no); 240 } 241 else static if (isLeafType!(PlainT, attributes)) 242 { 243 writer.openStartTag(name, Newline.no); 244 writer.closeStartTag; 245 246 writer.writeText(encodeLeafImpl(value).encodeText, Newline.no); 247 writer.writeEndTag(name, Newline.no); 248 } 249 else static if (isIterable!PlainT) 250 { 251 alias IterationType(T) = typeof({ foreach (value; T.init) return value; assert(0); }()); 252 253 foreach (IterationType!PlainT a; value) 254 { 255 encodeNodeImpl!(name, typeof(a), attributes)(writer, a); 256 } 257 } 258 else 259 { 260 encodeNode!(PlainT, attributes)(writer, value); 261 } 262 } 263 264 // must match encodeLeafImpl 265 private enum bool isLeafType(T, attributes...) = 266 udaIndex!(Xml.Encode, attributes) != -1 267 || udaIndex!(Xml.Encode, attributesOrNothing!T) != -1 268 || is(T == string) 269 || __traits(compiles, { Convert.toString(T.init); }); 270 271 private string encodeLeafImpl(T, attributes...)(T value) 272 { 273 alias typeAttributes = attributesOrNothing!T; 274 275 static if (udaIndex!(Xml.Encode, attributes) != -1) 276 { 277 alias customEncoder = attributes[udaIndex!(Xml.Encode, attributes)].EncodeFunction; 278 279 return customEncoder(value); 280 } 281 else static if (udaIndex!(Xml.Encode, typeAttributes) != -1) 282 { 283 alias customEncoder = typeAttributes[udaIndex!(Xml.Encode, typeAttributes)].EncodeFunction; 284 285 return customEncoder(value); 286 } 287 else static if (is(T == string)) 288 { 289 return value; 290 } 291 else static if (__traits(compiles, Convert.toString(value))) 292 { 293 return Convert.toString(value); 294 } 295 else 296 { 297 static assert(false, "Unknown value type: " ~ T.stringof); 298 } 299 }