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 }