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