1 module text.xml.Validation; 2 3 version(unittest) import dshould; 4 import dxml.parser; 5 static import dxml.util; 6 import std.algorithm; 7 import std.array; 8 import std.exception; 9 import std.range; 10 import std..string; 11 import text.xml.Convert; 12 import text.xml.Tree; 13 import text.xml.XmlException; 14 15 alias nodes = filter!(node => node.type == XmlNode.Type.element); 16 17 /** 18 * Throws: XmlException on validity violation. 19 */ 20 void enforceName(XmlNode node, string name) pure @safe 21 in (node.type == XmlNode.Type.element) 22 { 23 enforce!XmlException(node.tag == name, 24 format!`element "%s": unexpected element (expected is "%s")`(node.tag, name)); 25 } 26 27 /** 28 * Throws: XmlException on validity violation. 29 */ 30 void enforceFixed(XmlNode node, string name, string expected) pure @safe 31 { 32 const actual = node.require!string(name); 33 34 enforce!XmlException(actual == expected, 35 format!`element "%s": unexpected %s "%s" (expected is "%s")`(node.tag, name, actual, expected)); 36 } 37 38 /** 39 * Throws: XmlException on validity violation. 40 */ 41 XmlNode requireChild(XmlNode node, string name) 42 in (node.type == XmlNode.Type.element) 43 out (resultNode; resultNode.type == XmlNode.Type.element) 44 { 45 auto nodes = node.requireChildren(name); 46 47 enforce!XmlException(nodes.dropOne.empty, 48 format!`element "%s": unexpected extra child element "%s"`(node.tag, name)); 49 50 return nodes.front; 51 } 52 53 /** 54 * Throws: XmlException on validity violation. 55 */ 56 XmlNode requireChild(XmlNode node) 57 in (node.type == XmlNode.Type.element) 58 out (resultNode; resultNode.type == XmlNode.Type.element) 59 { 60 auto nodes = node.requireChildren; 61 62 enforce!XmlException(nodes.dropOne.empty, 63 format!`element "%s": unexpected extra child element "%s"`(node.tag, nodes.front.tag)); 64 65 return nodes.front; 66 } 67 68 /** 69 * Throws: XmlException on validity violation. 70 */ 71 auto requireChildren(XmlNode node, string name) 72 in (node.type == XmlNode.Type.element) 73 out (nodes; nodes.save.all!(node => node.type == XmlNode.Type.element)) 74 out (nodes; !nodes.empty) 75 { 76 auto nodes = node.findChildren(name); 77 78 enforce!XmlException(!nodes.empty, 79 format!`element "%s": required child element "%s" is missing`(node.tag, name)); 80 81 return nodes; 82 } 83 84 /** 85 * Throws: XmlException on validity violation. 86 */ 87 XmlNode[] requireChildren(XmlNode node) 88 in (node.type == XmlNode.Type.element) 89 out (nodes; nodes.all!(node => node.type == XmlNode.Type.element)) 90 out (nodes; !nodes.empty) 91 { 92 XmlNode[] nodes = node.children.nodes.array; 93 94 enforce!XmlException(!nodes.empty, 95 format!`element "%s": required child element is missing`(node.tag)); 96 97 return nodes; 98 } 99 100 /** 101 * Throws: XmlException on validity violation. 102 */ 103 XmlNode requireDescendant(XmlNode node, string name) pure @safe 104 in (node.type == XmlNode.Type.element) 105 out (resultNode; resultNode.type == XmlNode.Type.element) 106 { 107 auto nodes = node.requireDescendants(name); 108 XmlNode front = nodes.front; 109 110 nodes.popFront; 111 enforce!XmlException(nodes.empty, 112 format!`element "%s": unexpected extra descendant element "%s"`(node.tag, name)); 113 114 return front; 115 } 116 117 /** 118 * Throws: XmlException on validity violation. 119 */ 120 auto requireDescendants(XmlNode node, string name) pure @safe 121 in (node.type == XmlNode.Type.element) 122 { 123 auto nodes = node.findDescendants(name); 124 125 enforce!XmlException(!nodes.empty, 126 format!`element "%s": required descendant element "%s" is missing`(node.tag, name)); 127 128 return nodes; 129 } 130 131 auto findDescendants(XmlNode node, string name) nothrow pure @safe 132 out (nodes; nodes.all!(node => node.type == XmlNode.Type.element)) 133 { 134 void traverse(XmlNode node, ref Appender!(XmlNode[]) result) 135 { 136 foreach (child; node.children.nodes) 137 { 138 if (child.tag == name) 139 { 140 result ~= child; 141 } 142 traverse(child, result); 143 } 144 } 145 146 auto result = appender!(XmlNode[]); 147 148 traverse(node, result); 149 return result.data; 150 } 151 152 alias require = requireImpl!"to"; 153 alias requirePositive = requireImpl!"toPositive"; 154 alias requireTime = requireImpl!"toTime"; 155 156 template requireImpl(string conversion) 157 { 158 /** 159 * Throws: XmlException on validity violation. 160 */ 161 T requireImpl(T)(XmlNode node) 162 in (node.type == XmlNode.Type.element) 163 { 164 string text = dxml.util.decodeXML(node.text); 165 166 static if (is(T == string)) 167 { 168 if (text.sameHead(node.text)) 169 { 170 text = text.idup; 171 } 172 } 173 174 try 175 { 176 return mixin("Convert." ~ conversion ~ "!T(text)"); 177 } 178 catch (Exception exception) 179 { 180 throw new XmlException(format!`element "%s": %s`(node.tag, exception.msg)); 181 } 182 } 183 184 /** 185 * Throws: XmlException on validity violation. 186 */ 187 T requireImpl(T)(XmlNode node, string name) 188 in (node.type == XmlNode.Type.element) 189 { 190 enforce!XmlException(name in node.attributes, 191 format!`element "%s": required attribute "%s" is missing`(node.tag, name)); 192 193 string value = dxml.util.decodeXML(node.attributes[name]); 194 195 static if (is(T == string)) 196 { 197 if (value.sameHead(node.attributes[name])) 198 { 199 value = value.idup; 200 } 201 } 202 203 try 204 { 205 return mixin("Convert." ~ conversion ~ "!T(value)"); 206 } 207 catch (Exception exception) 208 { 209 throw new XmlException(format!`element "%s", attribute "%s": %s`(node.tag, name, exception.msg)); 210 } 211 } 212 213 /** 214 * Throws: XmlException on validity violation. 215 */ 216 T requireImpl(T)(XmlNode node, string name, lazy T fallback) 217 in (node.type == XmlNode.Type.element) 218 { 219 if (name !in node.attributes) 220 { 221 return fallback; 222 } 223 224 string value = dxml.util.decodeXML(node.attributes[name]); 225 226 static if (is(T == string)) 227 { 228 if (value.sameHead(node.attributes[name])) 229 { 230 value = value.idup; 231 } 232 } 233 234 try 235 { 236 return mixin("Convert." ~ conversion ~ "!T(value)"); 237 } 238 catch (XmlException exception) 239 { 240 throw new XmlException(format!`element "%s", attribute "%s": %s`(node.tag, name, exception.msg)); 241 } 242 } 243 } 244 245 public string normalize(string value) pure @safe 246 { 247 return value.split.join(" "); 248 } 249 250 @("normalize strings with newlines and tabs") 251 @safe unittest 252 { 253 normalize("\tfoo\r\nbar baz ").should.equal("foo bar baz"); 254 } 255 256 @("normalize allocates memory even in the trivial case") 257 unittest 258 { 259 const foo = "foo"; 260 261 normalize(foo).ptr.should.not.be(foo.ptr); 262 }