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 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 (!Xml.attributeName!attributes(builderField).isNull) 99 { 100 enum name = Xml.attributeName!attributes(builderField).get; 101 102 static if (isNullable || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault) 103 { 104 if (name in node.attributes) 105 { 106 __traits(getMember, builder, builderField) 107 = decodeAttributeLeaf!(DecodeType, name, attributes)(node); 108 } 109 } 110 else 111 { 112 __traits(getMember, builder, builderField) 113 = decodeAttributeLeaf!(DecodeType, name, attributes)(node); 114 } 115 } 116 else static if (!Xml.elementName!attributes(typeName!Type).isNull) 117 { 118 119 enum canDecodeNode = isNodeLeafType!(DecodeType, attributes) 120 || __traits(compiles, .decodeUnchecked!(DecodeType, attributes)(XmlNode.init)); 121 122 static if (canDecodeNode) 123 { 124 enum name = Xml.elementName!attributes(typeName!Type).get; 125 126 static if (isNullable 127 || __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault) 128 { 129 static assert( 130 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault, 131 format!"%s." ~ constructorField ~ " is Nullable, but missing @(This.Default)!" 132 (fullyQualifiedName!T)); 133 134 auto child = node.findChild(name); 135 136 if (!child.isNull) 137 { 138 __traits(getMember, builder, builderField) 139 = decodeUnchecked!(DecodeType, attributes)(child.get); 140 } 141 } 142 else 143 { 144 auto child = node.requireChild(name); 145 146 __traits(getMember, builder, builderField) 147 = .decodeUnchecked!(DecodeType, attributes)(child); 148 } 149 } 150 else static if (is(DecodeType: U[], U)) 151 { 152 enum name = Xml.elementName!attributes(typeName!U).get; 153 154 alias decodeChild = delegate U(XmlNode child) 155 { 156 return .decodeUnchecked!(U, attributes)(child); 157 }; 158 159 auto children = node.findChildren(name).map!decodeChild.array; 160 161 __traits(getMember, builder, builderField) = children; 162 } 163 else 164 { 165 pragma(msg, "While decoding field '" ~ constructorField ~ "' of type " ~ DecodeType.stringof ~ ":"); 166 167 // reproduce the error we swallowed earlier 168 auto _ = .decodeUnchecked!(DecodeType, attributes)(XmlNode.init); 169 } 170 } 171 else static if (udaIndex!(Xml.Text, attributes) != -1) 172 { 173 __traits(getMember, builder, builderField) = dxml.util.decodeXML(node.text); 174 } 175 else 176 { 177 enum sameField(string lhs, string rhs) 178 = optionallyRemoveTrailingUnderline!lhs == optionallyRemoveTrailingUnderline!rhs; 179 enum memberIsAliasedToThis = anySatisfy!( 180 ApplyLeft!(sameField, constructorField), 181 __traits(getAliasThis, T)); 182 183 static if (memberIsAliasedToThis) 184 { 185 // decode inline 186 __traits(getMember, builder, builderField) = .decodeUnchecked!(DecodeType, attributes)(node); 187 } 188 else 189 { 190 static assert( 191 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault, 192 "Field " ~ fullyQualifiedName!T ~ "." ~ constructorField ~ " is required but has no Xml tag"); 193 } 194 } 195 }} 196 197 return builder.builderValue(); 198 } 199 } 200 201 /** 202 * Throws: XmlException if the XML element doesn't have a child matching exactly one of the subtypes, 203 * or if the child doesn't match the subtype. 204 */ 205 private SumType!Types decodeSumType(Types...)(XmlNode node) 206 { 207 import std.algorithm : find, map, moveEmplace, sum; 208 import std.array : array, front; 209 import std.exception : enforce; 210 import std.meta : AliasSeq, staticMap; 211 import std.traits : fullyQualifiedName; 212 import std.typecons : apply, Nullable, nullable; 213 import text.xml.XmlException : XmlException; 214 215 Nullable!(SumType!Types)[Types.length] decodedValues; 216 217 static foreach (i, Type; Types) 218 {{ 219 static if (is(Type: U[], U)) 220 { 221 enum isArray = true; 222 alias BaseType = U; 223 } 224 else 225 { 226 enum isArray = false; 227 alias BaseType = Type; 228 } 229 230 alias attributes = AliasSeq!(__traits(getAttributes, BaseType)); 231 232 static assert( 233 !Xml.elementName!attributes(typeName!BaseType).isNull, 234 fullyQualifiedName!Type ~ 235 ": SumType component type must have an Xml.Element attribute indicating its element name."); 236 237 enum name = Xml.elementName!attributes(typeName!BaseType).get; 238 239 static if (isArray) 240 { 241 auto children = node.findChildren(name); 242 243 if (!children.empty) 244 { 245 decodedValues[i] = SumType!Types(children.map!(a => a.decodeUnchecked!U).array); 246 } 247 } 248 else 249 { 250 auto child = node.findChild(name); 251 252 decodedValues[i] = child.apply!(a => SumType!Types(a.decodeUnchecked!Type)); 253 } 254 }} 255 256 const matchedValues = decodedValues[].map!(a => a.isNull ? 0 : 1).sum; 257 258 enforce!XmlException(matchedValues != 0, 259 format!`Element "%s": no child element of %(%s, %)`(node.tag, [staticMap!(typeName, Types)])); 260 enforce!XmlException(matchedValues == 1, 261 format!`Element "%s": contained more than one of %(%s, %)`(node.tag, [staticMap!(typeName, Types)])); 262 return decodedValues[].find!(a => !a.isNull).front.get; 263 } 264 265 private enum typeName(T) = typeof(cast() T.init).stringof; 266 267 private auto decodeAttributeLeaf(T, string name, attributes...)(XmlNode node) 268 { 269 alias typeAttributes = attributesOrNothing!T; 270 271 static if (udaIndex!(Xml.Decode, attributes) != -1) 272 { 273 alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction; 274 275 return decodeFunction(dxml.util.decodeXML(node.attributes[name])); 276 } 277 else static if (udaIndex!(Xml.Decode, typeAttributes) != -1) 278 { 279 alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction; 280 281 return decodeFunction(dxml.util.decodeXML(node.attributes[name])); 282 } 283 else 284 { 285 return node.require!T(name); 286 } 287 } 288 289 // must match decodeNodeLeaf 290 enum isNodeLeafType(T, attributes...) = 291 udaIndex!(Xml.Decode, attributes) != -1 292 || udaIndex!(Xml.Decode, attributesOrNothing!T) != -1 293 || __traits(compiles, XmlNode.init.require!(SafeUnqual!T)()); 294 295 private auto decodeNodeLeaf(T, attributes...)(XmlNode node) 296 { 297 alias typeAttributes = attributesOrNothing!T; 298 299 static if (udaIndex!(Xml.Decode, attributes) != -1 || udaIndex!(Xml.Decode, typeAttributes) != -1) 300 { 301 static if (udaIndex!(Xml.Decode, attributes) != -1) 302 { 303 alias decodeFunction = attributes[udaIndex!(Xml.Decode, attributes)].DecodeFunction; 304 } 305 else 306 { 307 alias decodeFunction = typeAttributes[udaIndex!(Xml.Decode, typeAttributes)].DecodeFunction; 308 } 309 310 static if (__traits(isTemplate, decodeFunction)) 311 { 312 return decodeFunction!T(node); 313 } 314 else 315 { 316 return decodeFunction(node); 317 } 318 } 319 else static if (is(T == string)) 320 { 321 return dxml.util.decodeXML(node.text).normalize; 322 } 323 else 324 { 325 return node.require!(SafeUnqual!T)(); 326 } 327 }