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