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.style;
27 import dyaml.tagdirective;
28 
29 
30 /**
31  * Dumps YAML documents to files or streams.
32  *
33  * User specified Representer and/or Resolver can be used to support new
34  * tags / data types.
35  *
36  * Setters are provided to affect output details (style, etc.).
37  */
38 auto dumper()
39 {
40     auto dumper = Dumper();
41     dumper.resolver = Resolver.withDefaultResolvers;
42     return dumper;
43 }
44 
45 struct Dumper
46 {
47     private:
48         //Indentation width.
49         int indent_ = 2;
50         //Tag directives to use.
51         TagDirective[] tags_;
52     public:
53         //Resolver to resolve tags.
54         Resolver resolver;
55         //Write scalars in canonical form?
56         bool canonical;
57         //Preferred text width.
58         uint textWidth = 80;
59         //Line break to use. Unix by default.
60         LineBreak lineBreak = LineBreak.unix;
61         //YAML version string. Default is 1.1.
62         string YAMLVersion = "1.1";
63         //Always explicitly write document start? Default is no explicit start.
64         bool explicitStart = false;
65         //Always explicitly write document end? Default is no explicit end.
66         bool explicitEnd = false;
67 
68         //Name of the output file or stream, used in error messages.
69         string name = "<unknown>";
70 
71         // Default style for scalar nodes. If style is $(D ScalarStyle.invalid), the _style is chosen automatically.
72         ScalarStyle defaultScalarStyle = ScalarStyle.invalid;
73         // Default style for collection nodes. If style is $(D CollectionStyle.invalid), the _style is chosen automatically.
74         CollectionStyle defaultCollectionStyle = CollectionStyle.invalid;
75 
76         @disable bool opEquals(ref Dumper);
77         @disable int opCmp(ref Dumper);
78 
79         ///Set indentation width. 2 by default. Must not be zero.
80         @property void indent(uint indent) pure @safe nothrow
81         in
82         {
83             assert(indent != 0, "Can't use zero YAML indent width");
84         }
85         do
86         {
87             indent_ = indent;
88         }
89 
90         /**
91          * Specify tag directives.
92          *
93          * A tag directive specifies a shorthand notation for specifying _tags.
94          * Each tag directive associates a handle with a prefix. This allows for
95          * compact tag notation.
96          *
97          * Each handle specified MUST start and end with a '!' character
98          * (a single character "!" handle is allowed as well).
99          *
100          * Only alphanumeric characters, '-', and '__' may be used in handles.
101          *
102          * Each prefix MUST not be empty.
103          *
104          * The "!!" handle is used for default YAML _tags with prefix
105          * "tag:yaml.org,2002:". This can be overridden.
106          *
107          * Params:  tags = Tag directives (keys are handles, values are prefixes).
108          */
109         @property void tagDirectives(string[string] tags) pure @safe
110         {
111             TagDirective[] t;
112             foreach(handle, prefix; tags)
113             {
114                 assert(handle.length >= 1 && handle[0] == '!' && handle[$ - 1] == '!',
115                        "A tag handle is empty or does not start and end with a " ~
116                        "'!' character : " ~ handle);
117                 assert(prefix.length >= 1, "A tag prefix is empty");
118                 t ~= TagDirective(handle, prefix);
119             }
120             tags_ = t;
121         }
122         ///
123         @safe unittest
124         {
125             auto dumper = dumper();
126             string[string] directives;
127             directives["!short!"] = "tag:long.org,2011:";
128             //This will emit tags starting with "tag:long.org,2011"
129             //with a "!short!" prefix instead.
130             dumper.tagDirectives(directives);
131             dumper.dump(new Appender!string(), Node("foo"));
132         }
133 
134         /**
135          * Dump one or more YAML _documents to the file/stream.
136          *
137          * Note that while you can call dump() multiple times on the same
138          * dumper, you will end up writing multiple YAML "files" to the same
139          * file/stream.
140          *
141          * Params:  documents = Documents to _dump (root nodes of the _documents).
142          *
143          * Throws:  YAMLException on error (e.g. invalid nodes,
144          *          unable to write to file/stream).
145          */
146         void dump(CharacterType = char, Range)(Range range, Node[] documents ...)
147             if (isOutputRange!(Range, CharacterType) &&
148                 isOutputRange!(Range, char) || isOutputRange!(Range, wchar) || isOutputRange!(Range, dchar))
149         {
150             try
151             {
152                 auto emitter = new Emitter!(Range, CharacterType)(range, canonical, indent_, textWidth, lineBreak);
153                 auto serializer = Serializer(resolver, explicitStart ? Yes.explicitStart : No.explicitStart,
154                                              explicitEnd ? Yes.explicitEnd : No.explicitEnd, YAMLVersion, tags_);
155                 serializer.startStream(emitter);
156                 foreach(ref document; documents)
157                 {
158                     auto data = representData(document, defaultScalarStyle, defaultCollectionStyle);
159                     serializer.serialize(emitter, data);
160                 }
161                 serializer.endStream(emitter);
162             }
163             catch(YAMLException e)
164             {
165                 throw new YAMLException("Unable to dump YAML to stream "
166                                         ~ name ~ " : " ~ e.msg, e.file, e.line);
167             }
168         }
169 }
170 ///Write to a file
171 @safe unittest
172 {
173     auto node = Node([1, 2, 3, 4, 5]);
174     dumper().dump(new Appender!string(), node);
175 }
176 ///Write multiple YAML documents to a file
177 @safe unittest
178 {
179     auto node1 = Node([1, 2, 3, 4, 5]);
180     auto node2 = Node("This document contains only one string");
181     dumper().dump(new Appender!string(), node1, node2);
182     //Or with an array:
183     dumper().dump(new Appender!string(), [node1, node2]);
184 }
185 ///Write to memory
186 @safe unittest
187 {
188     auto stream = new Appender!string();
189     auto node = Node([1, 2, 3, 4, 5]);
190     dumper().dump(stream, node);
191 }
192 ///Use a custom resolver to support custom data types and/or implicit tags
193 @safe unittest
194 {
195     import std.regex : regex;
196     auto node = Node([1, 2, 3, 4, 5]);
197     auto dumper = dumper();
198     dumper.resolver.addImplicitResolver("!tag", regex("A.*"), "A");
199     dumper.dump(new Appender!string(), node);
200 }
201 /// Set default scalar style
202 @safe unittest
203 {
204     auto stream = new Appender!string();
205     auto node = Node("Hello world!");
206     auto dumper = dumper();
207     dumper.defaultScalarStyle = ScalarStyle.singleQuoted;
208     dumper.dump(stream, node);
209 }
210 /// Set default collection style
211 @safe unittest
212 {
213     auto stream = new Appender!string();
214     auto node = Node(["Hello", "world!"]);
215     auto dumper = dumper();
216     dumper.defaultCollectionStyle = CollectionStyle.flow;
217     dumper.dump(stream, node);
218 }
219 // Make sure the styles are actually used
220 @safe unittest
221 {
222     auto stream = new Appender!string();
223     auto node = Node([Node("Hello world!"), Node(["Hello", "world!"])]);
224     auto dumper = dumper();
225     dumper.defaultScalarStyle = ScalarStyle.singleQuoted;
226     dumper.defaultCollectionStyle = CollectionStyle.flow;
227     dumper.explicitEnd = false;
228     dumper.explicitStart = false;
229     dumper.YAMLVersion = null;
230     dumper.dump(stream, node);
231     assert(stream.data == "[!!str 'Hello world!', [!!str 'Hello', !!str 'world!']]\n");
232 }
233 // Explicit document start/end markers
234 @safe unittest
235 {
236     auto stream = new Appender!string();
237     auto node = Node([1, 2, 3, 4, 5]);
238     auto dumper = dumper();
239     dumper.explicitEnd = true;
240     dumper.explicitStart = true;
241     dumper.YAMLVersion = null;
242     dumper.dump(stream, node);
243     //Skip version string
244     assert(stream.data[0..3] == "---");
245     //account for newline at end
246     assert(stream.data[$-4..$-1] == "...");
247 }
248 // No explicit document start/end markers
249 @safe unittest
250 {
251     auto stream = new Appender!string();
252     auto node = Node([1, 2, 3, 4, 5]);
253     auto dumper = dumper();
254     dumper.explicitEnd = false;
255     dumper.explicitStart = false;
256     dumper.YAMLVersion = null;
257     dumper.dump(stream, node);
258     //Skip version string
259     assert(stream.data[0..3] != "---");
260     //account for newline at end
261     assert(stream.data[$-4..$-1] != "...");
262 }
263 // Windows, macOS line breaks
264 @safe unittest
265 {
266     auto node = Node(0);
267     {
268         auto stream = new Appender!string();
269         auto dumper = dumper();
270         dumper.explicitEnd = true;
271         dumper.explicitStart = true;
272         dumper.YAMLVersion = null;
273         dumper.lineBreak = LineBreak.windows;
274         dumper.dump(stream, node);
275         assert(stream.data == "--- 0\r\n...\r\n");
276     }
277     {
278         auto stream = new Appender!string();
279         auto dumper = dumper();
280         dumper.explicitEnd = true;
281         dumper.explicitStart = true;
282         dumper.YAMLVersion = null;
283         dumper.lineBreak = LineBreak.macintosh;
284         dumper.dump(stream, node);
285         assert(stream.data == "--- 0\r...\r");
286     }
287 }