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 == "['Hello world!', ['Hello', '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 @safe unittest 249 { 250 auto stream = new Appender!string(); 251 auto node = Node([Node("Te, st2")]); 252 auto dumper = dumper(); 253 dumper.explicitStart = true; 254 dumper.explicitEnd = false; 255 dumper.YAMLVersion = null; 256 dumper.dump(stream, node); 257 assert(stream.data == "--- ['Te, st2']\n"); 258 } 259 // No explicit document start/end markers 260 @safe unittest 261 { 262 auto stream = new Appender!string(); 263 auto node = Node([1, 2, 3, 4, 5]); 264 auto dumper = dumper(); 265 dumper.explicitEnd = false; 266 dumper.explicitStart = false; 267 dumper.YAMLVersion = null; 268 dumper.dump(stream, node); 269 //Skip version string 270 assert(stream.data[0..3] != "---"); 271 //account for newline at end 272 assert(stream.data[$-4..$-1] != "..."); 273 } 274 // Windows, macOS line breaks 275 @safe unittest 276 { 277 auto node = Node(0); 278 { 279 auto stream = new Appender!string(); 280 auto dumper = dumper(); 281 dumper.explicitEnd = true; 282 dumper.explicitStart = true; 283 dumper.YAMLVersion = null; 284 dumper.lineBreak = LineBreak.windows; 285 dumper.dump(stream, node); 286 assert(stream.data == "--- 0\r\n...\r\n"); 287 } 288 { 289 auto stream = new Appender!string(); 290 auto dumper = dumper(); 291 dumper.explicitEnd = true; 292 dumper.explicitStart = true; 293 dumper.YAMLVersion = null; 294 dumper.lineBreak = LineBreak.macintosh; 295 dumper.dump(stream, node); 296 assert(stream.data == "--- 0\r...\r"); 297 } 298 }