1 module text.xml.Decode; 2 3 import boilerplate.util : udaIndex; 4 static import dxml.util; 5 import meta.attributesOrNothing; 6 import meta.never; 7 import meta.SafeUnqual; 8 import std.format : format; 9 import std.sumtype; 10 import text.xml.Tree; 11 import text.xml.Validation : enforceName, normalize, require, requireChild; 12 public import text.xml.Xml; 13 14 /** 15 * Throws: XmlException if the message is not well-formed or doesn't match the type 16 */ 17 public T decode(T, alias customDecode = never)(string message) 18 { 19 import text.xml.Parser : parse; 20 21 static assert(__traits(isSame, customDecode, never), "XML does not yet support a decode function"); 22 23 XmlNode rootNode = parse(message); 24 25 return decodeXml!T(rootNode); 26 } 27 28 /** 29 * Throws: XmlException if the XML element doesn't match the type 30 */ 31 public T decodeXml(T)(XmlNode node) 32 { 33 import std.traits : fullyQualifiedName; 34 35 enum name = Xml.elementName!(__traits(getAttributes, T))(typeName!T); 36 37 static assert( 38 !name.isNull, 39 fullyQualifiedName!T ~ 40 ": type passed to text.xml.decode must have an Xml.Element attribute indicating its element name."); 41 42 node.enforceName(name.get); 43 44 return decodeUnchecked!T(node); 45 } 46 47 /** 48 * Throws: XmlException if the XML element doesn't match the type 49 * Returns: T, or the type returned from a decoder function defined on T. 50 */ 51 public auto decodeUnchecked(T, attributes...)(XmlNode node) 52 { 53 import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, udaIndex; 54 import std.algorithm : map; 55 import std.meta : AliasSeq, anySatisfy, ApplyLeft; 56 import std.range : array, ElementType; 57 import std.string : strip; 58 import std.traits : fullyQualifiedName, isIterable, Unqual; 59 import std.typecons : Nullable, Tuple; 60 61 static if (isNodeLeafType!(T, attributes)) 62 { 63 return decodeNodeLeaf!(T, attributes)(node); 64 } 65 else 66 { 67 static assert( 68 __traits(hasMember, T, "ConstructorInfo"), 69 fullyQualifiedName!T ~ " does not have a boilerplate constructor!"); 70 71 auto builder = T.Builder(); 72 73 alias Info = Tuple!(string, "builderField", string, "constructorField"); 74 75 static foreach (string constructorField; T.ConstructorInfo.fields) 76 {{ 77 enum builderField = optionallyRemoveTrailingUnderline!constructorField; 78 79 alias Type = Unqual!(__traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).Type); 80 alias attributes = AliasSeq!( 81 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).attributes); 82 83 static if (is(Type : Nullable!Arg, Arg)) 84 { 85 alias DecodeType = Arg; 86 enum isNullable = true; 87 } 88 else 89 { 90 alias DecodeType = SafeUnqual!Type; 91 enum isNullable = false; 92 } 93 94 static if (is(Type : SumType!T, T...)) 95 { 96 __traits(getMember, builder, builderField) = decodeSumType!T(node); 97 } 98 else static if (is(Type : SumType!T[], T...)) 99 { 100 __traits(getMember, builder, builderField) = decodeSumTypeArray!T(node); 101 } 102 else static if (!Xml.attributeName!attributes(builderField).isNull) 103 { 104 enum name = Xml.attributeName!attributes(builderField).get; 105 106 static if (isNullable || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault) 107 { 108 if (name in node.attributes) 109 { 110 __traits(getMember, builder, builderField) 111 = decodeAttributeLeaf!(DecodeType, name, attributes)(node); 112 } 113 } 114 else 115 { 116 __traits(getMember, builder, builderField) 117 = decodeAttributeLeaf!(DecodeType, name, attributes)(node); 118 } 119 } 120 else static if (!Xml.elementName!attributes(typeName!Type).isNull) 121 { 122 123 enum canDecodeNode = isNodeLeafType!(DecodeType, attributes) 124 || __traits(compiles, .decodeUnchecked!(DecodeType, attributes)(XmlNode.init)); 125 126 static if (canDecodeNode) 127 { 128 enum name = Xml.elementName!attributes(typeName!Type).get; 129 130 static if (isNullable 131 || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault) 132 { 133 static assert( 134 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault, 135 format!"%s." ~ constructorField ~ " is Nullable, but missing @(This.Default)!" 136 (fullyQualifiedName!T)); 137 138 auto child = node.findChild(name); 139 140 if (!child.isNull) 141 { 142 __traits(getMember, builder, builderField) 143 = decodeUnchecked!(DecodeType, attributes)(child.get); 144 } 145 } 146 else 147 { 148 auto child = node.requireChild(name); 149 150 __traits(getMember, builder, builderField) 151 = .decodeUnchecked!(DecodeType, attributes)(child); 152 } 153 } 154 else static if (is(DecodeType: U[], U)) 155 { 156 enum name = Xml.elementName!attributes(typeName!U).get; 157 158 alias decodeChild = delegate U(XmlNode child) 159 { 160 return .decodeUnchecked!(U, attributes)(child); 161 }; 162 163 auto children = node.findChildren(name).map!decodeChild.array; 164 165 __traits(getMember, builder, builderField) = children; 166 } 167 else 168 { 169 pragma(msg, "While decoding field '" ~ constructorField ~ "' of type " ~ DecodeType.stringof ~ ":"); 170 171 // reproduce the error we swallowed earlier 172 auto _ = .decodeUnchecked!(DecodeType, attributes)(XmlNode.init); 173 } 174 } 175 else static if (udaIndex!(Xml.Text, attributes) != -1) 176 { 177 __traits(getMember, builder, builderField) = dxml.util.decodeXML(node.text); 178 } 179 else 180 { 181 enum sameField(string lhs, string rhs) 182 = optionallyRemoveTrailingUnderline!lhs == optionallyRemoveTrailingUnderline!rhs; 183 enum memberIsAliasedToThis = anySatisfy!( 184 ApplyLeft!(sameField, constructorField), 185 __traits(getAliasThis, T)); 186 187 static if (memberIsAliasedToThis) 188 { 189 // decode inline 190 __traits(getMember, builder, builderField) = .decodeUnchecked!(DecodeType, attributes)(node); 191 } 192 else 193 { 194 static assert( 195 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault, 196 "Field " ~ fullyQualifiedName!T ~ "." ~ constructorField ~ " is required but has no Xml tag"); 197 } 198 } 199 }} 200 201 return builder.builderValue(); 202 } 203 } 204 205 /** 206 * Throws: XmlException if the XML element doesn't have a child matching exactly one of the subtypes, 207 * or if the child doesn't match the subtype. 208 */ 209 private SumType!Types decodeSumType(Types...)(XmlNode node) 210 { 211 import std.algorithm : find, map, moveEmplace, sum; 212 import std.array : array, front; 213 import std.exception : enforce; 214 import std.meta : AliasSeq, staticMap; 215 import std.traits : fullyQualifiedName; 216 import std.typecons : apply, Nullable, nullable; 217 import text.xml.XmlException : XmlException; 218 219 Nullable!(SumType!Types)[Types.length] decodedValues; 220 221 static foreach (i, Type; Types) 222 {{ 223 static if (is(Type: U[], U)) 224 { 225 enum isArray = true; 226 alias BaseType = U; 227 } 228 else 229 { 230 enum isArray = false; 231 alias BaseType = Type; 232 } 233 234 alias attributes = AliasSeq!(__traits(getAttributes, BaseType)); 235 236 static assert( 237 !Xml.elementName!attributes(typeName!BaseType).isNull, 238 fullyQualifiedName!Type ~ 239 ": SumType component type must have an Xml.Element attribute indicating its element name."); 240 241 enum name = Xml.elementName!attributes(typeName!BaseType).get; 242 243 static if (isArray) 244 { 245 auto children = node.findChildren(name); 246 247 if (!children.empty) 248 { 249 decodedValues[i] = SumType!Types(children.map!(a => a.decodeUnchecked!U).array); 250 } 251 } 252 else 253 { 254 auto child = node.findChild(name); 255 256 decodedValues[i] = child.apply!(a => SumType!Types(a.decodeUnchecked!Type)); 257 } 258 }} 259 260 const matchedValues = decodedValues[].map!(a => a.isNull ? 0 : 1).sum; 261 262 enforce!XmlException(matchedValues != 0, 263 format!`Element "%s": no child element of %(%s, %)`(node.tag, [staticMap!(typeName, Types)])); 264 enforce!XmlException(matchedValues == 1, 265 format!`Element "%s": contained more than one of %(%s, %)`(node.tag, [staticMap!(typeName, Types)])); 266 return decodedValues[].find!(a => !a.isNull).front.get; 267 } 268 269 private SumType!Types[] decodeSumTypeArray(Types...)(XmlNode node) 270 { 271 import std.meta : AliasSeq; 272 import std.traits : fullyQualifiedName; 273 274 SumType!Types[] result; 275 276 foreach (child; node.children) 277 { 278 static foreach (Type; Types) 279 {{ 280 alias attributes = AliasSeq!(__traits(getAttributes, Type)); 281 282 static assert( 283 !Xml.elementName!attributes(typeName!Type).isNull, 284 fullyQualifiedName!Type ~ 285 ": SumType component type must have an Xml.Element attribute indicating its element name."); 286 287 enum name = Xml.elementName!attributes(typeName!Type).get; 288 289 if (child.tag == name) 290 { 291 result ~= SumType!Types(child.decodeUnchecked!Type); 292 } 293 }} 294 } 295 return result; 296 } 297 298 private enum typeName(T) = typeof(cast() T.init).stringof; 299 300 private auto decodeAttributeLeaf(T, string name, attributes...)(XmlNode node) 301 { 302 alias typeAttributes = attributesOrNothing!T; 303 304 static if (udaIndex!(Xml.Decode, attributes) != -1) 305 { 306 alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction; 307 308 return decodeFunction(dxml.util.decodeXML(node.attributes[name])); 309 } 310 else static if (udaIndex!(Xml.Decode, typeAttributes) != -1) 311 { 312 alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction; 313 314 return decodeFunction(dxml.util.decodeXML(node.attributes[name])); 315 } 316 else 317 { 318 return node.require!T(name); 319 } 320 } 321 322 // must match decodeNodeLeaf 323 enum isNodeLeafType(T, attributes...) = 324 udaIndex!(Xml.Decode, attributes) != -1 325 || udaIndex!(Xml.Decode, attributesOrNothing!T) != -1 326 || __traits(compiles, XmlNode.init.require!(SafeUnqual!T)()); 327 328 private auto decodeNodeLeaf(T, attributes...)(XmlNode node) 329 { 330 alias typeAttributes = attributesOrNothing!T; 331 332 static if (udaIndex!(Xml.Decode, attributes) != -1 || udaIndex!(Xml.Decode, typeAttributes) != -1) 333 { 334 static if (udaIndex!(Xml.Decode, attributes) != -1) 335 { 336 alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction; 337 } 338 else 339 { 340 alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction; 341 } 342 343 static if (__traits(isTemplate, decodeFunction)) 344 { 345 return decodeFunction!T(node); 346 } 347 else 348 { 349 return decodeFunction(node); 350 } 351 } 352 else static if (is(T == string)) 353 { 354 return dxml.util.decodeXML(node.text).normalize; 355 } 356 else 357 { 358 return node.require!(SafeUnqual!T)(); 359 } 360 }