1 /**
2  * YAML configuration module
3  *
4  * Copyright: (c) 2015-2020, Milofon Project.
5  * License: Subject to the terms of the BSD 3-Clause License, as written in the included LICENSE.md file.
6  * Author: <m.galanin@milofon.pro> Maksim Galanin
7  * Date: 2018-09-03
8  */
9 
10 module uniconf.yaml;
11 
12 private
13 {
14     import std.array : appender, replace;
15     import std.datetime.systime : SysTime;
16     import std.datetime : DateTimeException;
17 
18     import dyaml.loader : Loader;
19     import dyaml.node : Node, NodeType;
20     import dyaml.exception : YAMLException;
21     import dyaml.dumper : dumper;
22     import dyaml.style : ScalarStyle;
23 
24     import uniconf.core : UniConfException;
25     import uninode.node : isUniNode;
26 }
27 
28 
29 /**
30  * Conert Yaml to UniConf
31  */
32 UniConf toUniConf(UniConf)(auto ref const Node root) @safe
33     if (isUniNode!UniConf)
34 {
35     UniConf convert(ref const Node node) @safe
36     {
37         if (!node.isValid)
38             return UniConf();
39 
40         switch(node.type)
41         {
42             case NodeType.null_:
43             case NodeType.merge:
44             case NodeType.invalid:
45                 return UniConf();
46             case NodeType.boolean:
47                 return UniConf(node.get!bool);
48             case NodeType.integer:
49                 return UniConf(node.get!long);
50             case NodeType.decimal:
51                 return UniConf(node.get!double);
52             case NodeType.binary:
53                 return UniConf(node.get!(ubyte[]));
54             case NodeType.timestamp:
55                 return UniConf(node.get!SysTime.toISOExtString);
56             case NodeType..string:
57             {
58                 auto style = __traits(getMember,  node, "scalarStyle");
59                 if (style == ScalarStyle.folded)
60                     return UniConf(node.get!string.replace("\n", " "));
61                 return UniConf(node.get!string);
62             }
63             case NodeType.mapping:
64             {
65                 UniConf[string] map;
66                 foreach(string key, ref const Node value; node)
67                     map[key] = convert(value);
68                 return UniConf(map);
69             }
70             case NodeType.sequence:
71             {
72                 UniConf[] arr = new UniConf[node.length];
73                 size_t i;
74                 foreach(ref const Node value; node)
75                     arr[i++] = convert(value);
76                 return UniConf(arr);
77             }
78             default:
79                 return UniConf();
80         }
81     }
82 
83     return convert(root);
84 }
85 
86 
87 /**
88  * Convert UniConf to Yaml
89  */
90 Node toYaml(UniConf)(auto ref const UniConf root) @safe
91     if (isUniNode!UniConf)
92 {
93     Node convert(ref const UniConf node) @trusted
94     {
95         switch(node.tag)
96         {
97             case UniConf.Tag.nil:
98                 return Node();
99             case UniConf.Tag.boolean:
100                 return Node(node.get!bool);
101             case UniConf.Tag.integer:
102                 return Node(node.get!long);
103             case UniConf.Tag.uinteger:
104                 return Node(node.get!ulong);
105             case UniConf.Tag.floating:
106                 return Node(node.get!double);
107             case UniConf.Tag.text:
108             {
109                 string val = node.get!string;
110                 try
111                     return Node(SysTime.fromISOExtString(val));
112                 catch (DateTimeException e)
113                     return Node(node.get!string);
114             }
115             case UniConf.Tag.sequence:
116             {
117                 size_t len = node.length;
118                 Node[] arr = new Node[len];
119                 foreach(size_t i, ref const UniConf ch; node)
120                     arr[i] = convert(ch);
121                 return Node(arr);
122             }
123             case UniConf.Tag.mapping:
124             {
125                 Node[string] map;
126                 foreach (string key, ref const UniConf ch; node)
127                     map[key] = convert(ch);
128                 return Node(map);
129             }
130             default:
131                 return Node();
132         }
133     }
134 
135     return convert(root);
136 }
137 
138 
139 @("Should work convert yaml to config and back")
140 @safe unittest
141 {
142     import uninode.node : UniNode;
143     const val = Node([Node(1), Node("hello"), Node(["one": Node(1)])]);
144     const conf = val.toUniConf!UniNode();
145     const yml = conf.toYaml();
146     assert (yml == val);
147 }
148 
149 
150 /**
151  * Parse UniConf from string
152  */
153 UniConf parseYaml(UniConf)(string data) @safe
154     if (isUniNode!UniConf)
155 {
156     Node root;
157     try
158         root = Loader.fromString(data.idup).load();
159     catch(YAMLException e)
160         throw new UniConfException("Error loading yaml from a string:",
161                 e.file, e.line, e);
162     return toUniConf!UniConf(root);
163 }
164 
165 
166 /**
167  * Convert UniConf to json string
168  */
169 string saveYaml(UniConf)(auto ref const UniConf node) @safe
170     if (isUniNode!UniConf)
171 {
172     const yml = toYaml!UniConf(node);
173     auto buffer = appender!string;
174     dumper().dump(buffer, yml);
175     return buffer.data;
176 }
177 
178 @("Should work parseYaml and saveYaml method")
179 @safe unittest
180 {
181     import uninode.tree : UniTree;
182 
183     enum yamlSrc = `
184 client:
185     host: "localhost"
186     port: 404
187     can: 2001-12-15T02:59:43.1Z
188     collection:
189     - "one"
190     - "two"
191 `;
192 
193     UniTree conf = parseYaml!UniTree(yamlSrc);
194     assert (conf.get!int("client.port") == 404);
195     assert (conf.get!string("client.host") == "localhost");
196     const yml = saveYaml(conf);
197     conf = parseYaml!UniTree(yml);
198     assert (conf.get!int("client.port") == 404);
199     assert (conf.get!string("client.host") == "localhost");
200 }
201