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 }