1 
2 //          Copyright Ferdinand Majerech 2011.
3 // Distributed under the Boost Software License, Version 1.0.
4 //    (See accompanying file LICENSE_1_0.txt or copy at
5 //          http://www.boost.org/LICENSE_1_0.txt)
6 
7 /**
8  * YAML dumper.
9  *
10  * Code based on $(LINK2 http://www.pyyaml.org, PyYAML).
11  */
12 module dyaml.dumper;
13 
14 import std.array;
15 import std.range.primitives;
16 import std.typecons;
17 
18 import dyaml.emitter;
19 import dyaml.event;
20 import dyaml.exception;
21 import dyaml.linebreak;
22 import dyaml.node;
23 import dyaml.representer;
24 import dyaml.resolver;
25 import dyaml.serializer;
26 import dyaml.tagdirective;
27 
28 
29 /**
30  * Dumps YAML documents to files or streams.
31  *
32  * User specified Representer and/or Resolver can be used to support new
33  * tags / data types.
34  *
35  * Setters are provided to affect output details (style, etc.).
36  */
37 auto dumper(Range)(auto ref Range output)
38     if (isOutputRange!(Range, char) || isOutputRange!(Range, wchar) || isOutputRange!(Range, dchar))
39 {
40     return Dumper!Range(output);
41 }
42 
43 struct Dumper(Range)
44 {
45     private:
46         //Resolver to resolve tags.
47         Resolver resolver_;
48         //Representer to represent data types.
49         Representer representer_;
50 
51         //Stream to write to.
52         Range stream_;
53 
54         //Write scalars in canonical form?
55         bool canonical_;
56         //Indentation width.
57         int indent_ = 2;
58         //Preferred text width.
59         uint textWidth_ = 80;
60         //Line break to use.
61         LineBreak lineBreak_ = LineBreak.Unix;
62         //YAML version string.
63         string YAMLVersion_ = "1.1";
64         //Tag directives to use.
65         TagDirective[] tags_;
66         //Always write document start?
67         Flag!"explicitStart" explicitStart_ = No.explicitStart;
68         //Always write document end?
69         Flag!"explicitEnd" explicitEnd_ = No.explicitEnd;
70 
71         //Name of the output file or stream, used in error messages.
72         string name_ = "<unknown>";
73 
74     public:
75         @disable this();
76         @disable bool opEquals(ref Dumper!Range);
77         @disable int opCmp(ref Dumper!Range);
78 
79         /**
80          * Construct a Dumper writing to a file.
81          *
82          * Params: filename = File name to write to.
83          */
84         this(Range stream) @safe
85         {
86             resolver_    = new Resolver();
87             representer_ = new Representer();
88             stream_ = stream;
89         }
90 
91         ///Set stream _name. Used in debugging messages.
92         @property void name(string name) pure @safe nothrow
93         {
94             name_ = name;
95         }
96 
97         ///Specify custom Resolver to use.
98         @property void resolver(Resolver resolver) @safe
99         {
100             resolver_ = resolver;
101         }
102 
103         ///Specify custom Representer to use.
104         @property void representer(Representer representer) @safe
105         {
106             representer_ = representer;
107         }
108 
109         ///Write scalars in _canonical form?
110         @property void canonical(bool canonical) pure @safe nothrow
111         {
112             canonical_ = canonical;
113         }
114 
115         ///Set indentation width. 2 by default. Must not be zero.
116         @property void indent(uint indent) pure @safe nothrow
117         in
118         {
119             assert(indent != 0, "Can't use zero YAML indent width");
120         }
121         do
122         {
123             indent_ = indent;
124         }
125 
126         ///Set preferred text _width.
127         @property void textWidth(uint width) pure @safe nothrow
128         {
129             textWidth_ = width;
130         }
131 
132         ///Set line break to use. Unix by default.
133         @property void lineBreak(LineBreak lineBreak) pure @safe nothrow
134         {
135             lineBreak_ = lineBreak;
136         }
137 
138         ///Always explicitly write document start?
139         @property void explicitStart(bool explicit) pure @safe nothrow
140         {
141             explicitStart_ = explicit ? Yes.explicitStart : No.explicitStart;
142         }
143 
144         ///Always explicitly write document end?
145         @property void explicitEnd(bool explicit) pure @safe nothrow
146         {
147             explicitEnd_ = explicit ? Yes.explicitEnd : No.explicitEnd;
148         }
149 
150         ///Specify YAML version string. "1.1" by default.
151         @property void YAMLVersion(string YAMLVersion) pure @safe nothrow
152         {
153             YAMLVersion_ = YAMLVersion;
154         }
155 
156         /**
157          * Specify tag directives.
158          *
159          * A tag directive specifies a shorthand notation for specifying _tags.
160          * Each tag directive associates a handle with a prefix. This allows for
161          * compact tag notation.
162          *
163          * Each handle specified MUST start and end with a '!' character
164          * (a single character "!" handle is allowed as well).
165          *
166          * Only alphanumeric characters, '-', and '__' may be used in handles.
167          *
168          * Each prefix MUST not be empty.
169          *
170          * The "!!" handle is used for default YAML _tags with prefix
171          * "tag:yaml.org,2002:". This can be overridden.
172          *
173          * Params:  tags = Tag directives (keys are handles, values are prefixes).
174          */
175         @property void tagDirectives(string[string] tags) pure @safe
176         {
177             TagDirective[] t;
178             foreach(handle, prefix; tags)
179             {
180                 assert(handle.length >= 1 && handle[0] == '!' && handle[$ - 1] == '!',
181                        "A tag handle is empty or does not start and end with a " ~
182                        "'!' character : " ~ handle);
183                 assert(prefix.length >= 1, "A tag prefix is empty");
184                 t ~= TagDirective(handle, prefix);
185             }
186             tags_ = t;
187         }
188         ///
189         @safe unittest
190         {
191             auto dumper = dumper(new Appender!string());
192             string[string] directives;
193             directives["!short!"] = "tag:long.org,2011:";
194             //This will emit tags starting with "tag:long.org,2011"
195             //with a "!short!" prefix instead.
196             dumper.tagDirectives(directives);
197             dumper.dump(Node("foo"));
198         }
199 
200         /**
201          * Dump one or more YAML _documents to the file/stream.
202          *
203          * Note that while you can call dump() multiple times on the same
204          * dumper, you will end up writing multiple YAML "files" to the same
205          * file/stream.
206          *
207          * Params:  documents = Documents to _dump (root nodes of the _documents).
208          *
209          * Throws:  YAMLException on error (e.g. invalid nodes,
210          *          unable to write to file/stream).
211          */
212         void dump(CharacterType = char)(Node[] documents ...) @trusted
213             if (isOutputRange!(Range, CharacterType))
214         {
215             try
216             {
217                 auto emitter = new Emitter!(Range, CharacterType)(stream_, canonical_, indent_, textWidth_, lineBreak_);
218                 auto serializer = Serializer!(Range, CharacterType)(emitter, resolver_, explicitStart_,
219                                              explicitEnd_, YAMLVersion_, tags_);
220                 foreach(ref document; documents)
221                 {
222                     representer_.represent(serializer, document);
223                 }
224             }
225             catch(YAMLException e)
226             {
227                 throw new YAMLException("Unable to dump YAML to stream "
228                                         ~ name_ ~ " : " ~ e.msg, e.file, e.line);
229             }
230         }
231 
232     package:
233         /*
234          * Emit specified events. Used for debugging/testing.
235          *
236          * Params:  events = Events to emit.
237          *
238          * Throws:  YAMLException if unable to emit.
239          */
240         void emit(CharacterType = char, T)(T events) @safe
241             if (isInputRange!T && is(ElementType!T == Event))
242         {
243             try
244             {
245                 auto emitter = Emitter!(Range, CharacterType)(stream_, canonical_, indent_, textWidth_, lineBreak_);
246                 foreach(ref event; events)
247                 {
248                     emitter.emit(event);
249                 }
250             }
251             catch(YAMLException e)
252             {
253                 throw new YAMLException("Unable to emit YAML to stream "
254                                         ~ name_ ~ " : " ~ e.msg, e.file, e.line);
255             }
256         }
257 }
258 ///Write to a file
259 @safe unittest
260 {
261     auto node = Node([1, 2, 3, 4, 5]);
262     dumper(new Appender!string()).dump(node);
263 }
264 ///Write multiple YAML documents to a file
265 @safe unittest
266 {
267     auto node1 = Node([1, 2, 3, 4, 5]);
268     auto node2 = Node("This document contains only one string");
269     dumper(new Appender!string()).dump(node1, node2);
270     //Or with an array:
271     dumper(new Appender!string()).dump([node1, node2]);
272 }
273 ///Write to memory
274 @safe unittest
275 {
276     auto stream = new Appender!string();
277     auto node = Node([1, 2, 3, 4, 5]);
278     dumper(stream).dump(node);
279 }
280 ///Use a custom representer/resolver to support custom data types and/or implicit tags
281 @safe unittest
282 {
283     auto node = Node([1, 2, 3, 4, 5]);
284     auto representer = new Representer();
285     auto resolver = new Resolver();
286     //Add representer functions / resolver expressions here...
287     auto dumper = dumper(new Appender!string());
288     dumper.representer = representer;
289     dumper.resolver = resolver;
290     dumper.dump(node);
291 }
292 // Explicit document start/end markers
293 @safe unittest
294 {
295     auto stream = new Appender!string();
296     auto node = Node([1, 2, 3, 4, 5]);
297     auto dumper = dumper(stream);
298     dumper.explicitEnd = true;
299     dumper.explicitStart = true;
300     dumper.YAMLVersion = null;
301     dumper.dump(node);
302     //Skip version string
303     assert(stream.data[0..3] == "---");
304     //account for newline at end
305     assert(stream.data[$-4..$-1] == "...");
306 }
307 // No explicit document start/end markers
308 @safe unittest
309 {
310     auto stream = new Appender!string();
311     auto node = Node([1, 2, 3, 4, 5]);
312     auto dumper = dumper(stream);
313     dumper.explicitEnd = false;
314     dumper.explicitStart = false;
315     dumper.YAMLVersion = null;
316     dumper.dump(node);
317     //Skip version string
318     assert(stream.data[0..3] != "---");
319     //account for newline at end
320     assert(stream.data[$-4..$-1] != "...");
321 }
322 // Windows, macOS line breaks
323 @safe unittest
324 {
325     auto node = Node(0);
326     {
327         auto stream = new Appender!string();
328         auto dumper = dumper(stream);
329         dumper.explicitEnd = true;
330         dumper.explicitStart = true;
331         dumper.YAMLVersion = null;
332         dumper.lineBreak = LineBreak.Windows;
333         dumper.dump(node);
334         assert(stream.data == "--- 0\r\n...\r\n");
335     }
336     {
337         auto stream = new Appender!string();
338         auto dumper = dumper(stream);
339         dumper.explicitEnd = true;
340         dumper.explicitStart = true;
341         dumper.YAMLVersion = null;
342         dumper.lineBreak = LineBreak.Macintosh;
343         dumper.dump(node);
344         assert(stream.data == "--- 0\r...\r");
345     }
346 }