1 module dash.utility.data.yaml;
2 import dash.utility.resources, dash.utility.output;
3 
4 public import yaml;
5 import vibe.data.serialization;
6 import std.traits, std.range, std.typecons, std.variant;
7 
8 /// Convience alias
9 alias Yaml = Node;
10 
11 /**
12  * Serializes the given value to YAML.
13  *
14  * The following types of values are supported:
15  *
16  * All entries of an array or an associative array, as well as all R/W properties and
17  * all public fields of a struct/class are recursively serialized using the same rules.
18  *
19  * Fields ending with an underscore will have the last underscore stripped in the
20  * serialized output. This makes it possible to use fields with D keywords as their name
21  * by simply appending an underscore.
22  *
23  * The following methods can be used to customize the serialization of structs/classes:
24  *
25  * ---
26  * Node toYaml() const;
27  * static T fromYaml( Node src );
28  *
29  * string toString() const;
30  * static T fromString( string src );
31  * ---
32  *
33  * The methods will have to be defined in pairs. The first pair that is implemented by
34  * the type will be used for serialization (i.e. toYaml overrides toString).
35 */
36 Node serializeToYaml( T )( T value )
37 {
38     return serialize!( YamlSerializer )( value );
39 }
40 static assert(is(typeof( serializeToYaml( "" ) )));
41 
42 /**
43  * Deserializes a YAML value into the destination variable.
44  *
45  * The same types as for serializeToYaml() are supported and handled inversely.
46  */
47 T deserializeYaml( T )( Node yaml )
48 {
49     return deserialize!( YamlSerializer, T )( yaml );
50 }
51 /// ditto
52 T deserializeYaml( T, R )( R input ) if ( isInputRange!R && !is( R == Node ) )
53 {
54     return deserialize!( YamlStringSerializer!R, T )( input );
55 }
56 static assert(is(typeof( deserializeYaml!string( Node( "" ) ) )));
57 //static assert(is(typeof( deserializeYaml!string( "" ) )));
58 
59 /// Does the type support custom serialization.
60 enum isYamlSerializable( T ) = is( typeof( T.init.toYaml() ) == Node ) && is( typeof( T.fromYaml( Node() ) ) == T );
61 
62 /**
63 * Get the element, cast to the given type, at the given path, in the given node.
64 *
65 * Params:
66 *  node =          The node to search.
67 *  path =          The path to find the item at.
68 */
69 final T find( T = Node )( Node node, string path )
70 {
71     T temp;
72     if( node.tryFind( path, temp ) )
73         return temp;
74     else
75         throw new YAMLException( "Path " ~ path ~ " not found in the given node." );
76 }
77 ///
78 unittest
79 {
80     import std.stdio;
81     import std.exception;
82 
83     writeln( "Dash Config find unittest" );
84 
85     auto n1 = Node( [ "test1": 10 ] );
86 
87     assert( n1.find!int( "test1" ) == 10, "Config.find error." );
88 
89     assertThrown!YAMLException(n1.find!int( "dontexist" ));
90 
91     // nested test
92     auto n2 = Node( ["test2": n1] );
93     auto n3 = Node( ["test3": n2] );
94 
95     assert( n3.find!int( "test3.test2.test1" ) == 10, "Config.find nested test failed");
96 
97     auto n4 = Loader.fromString( cast(char[])
98                                 "test3:\n" ~
99                                 "   test2:\n" ~
100                                 "       test1: 10" ).load;
101     assert( n4.find!int( "test3.test2.test1" ) == 10, "Config.find nested test failed");
102 }
103 
104 /**
105 * Try to get the value at path, assign to result, and return success.
106 *
107 * Params:
108 *  node =          The node to search.
109 *  path =          The path to look for in the node.
110 *  result =        [ref] The value to assign the result to.
111 *
112 * Returns: Whether or not the path was found.
113 */
114 final bool tryFind( T )( Node node, string path, ref T result ) nothrow @safe
115 {
116     // If anything goes wrong, it means the node wasn't found.
117     scope( failure ) return false;
118 
119     Node res;
120     bool found = node.tryFind( path, res );
121 
122     if( found )
123     {
124         static if( !isSomeString!T && is( T U : U[] ) )
125         {
126             assert( res.isSequence, "Trying to access non-sequence node " ~ path ~ " as an array." );
127 
128             foreach( Node element; res )
129                 result ~= element.get!U;
130         }
131         else static if( __traits( compiles, res.getObject!T ) )
132         {
133             result = res.getObject!T;
134         }
135         else
136         {
137             result = res.get!T;
138         }
139     }
140 
141     return found;
142 }
143 
144 /// ditto
145 final bool tryFind( T: Node )( Node node, string path, ref T result ) nothrow @safe
146 {
147     import std.algorithm: countUntil;
148 
149     // If anything goes wrong, it means the node wasn't found.
150     scope( failure ) return false;
151 
152     Node current;
153     string left;
154     string right = path;
155 
156     for( current = node; right.length; )
157     {
158         auto split = right.countUntil( '.' );
159 
160         if( split == -1 )
161         {
162             left = right;
163             right.length = 0;
164         }
165         else
166         {
167             left = right[ 0..split ];
168             right = right[ split + 1..$ ];
169         }
170 
171         if( !current.isMapping || !current.containsKey( left ) )
172             return false;
173 
174         current = current[ left ];
175     }
176 
177     result = current;
178 
179     return true;
180 }
181 
182 /// ditto
183 final bool tryFind( T = Node )( Node node, string path, ref Variant result ) nothrow @safe
184 {
185     // Get the value
186     T temp;
187     bool found = node.tryFind( path, temp );
188 
189     // Assign and return results
190     if( found )
191         result = temp;
192 
193     return found;
194 }
195 
196 /**
197 * You may not get a variant from a node. You may assign to one,
198 * but you must specify a type to search for.
199 */
200 @disable bool tryFind( T: Variant )( Node node, string path, ref Variant result );
201 
202 unittest
203 {
204     import std.stdio;
205     writeln( "Dash Config tryFind unittest" );
206 
207     auto n1 = Node( [ "test1": 10 ] );
208 
209     int val;
210     assert( n1.tryFind( "test1", val ), "Config.tryFind failed." );
211     assert( !n1.tryFind( "dontexist", val ), "Config.tryFind returned true." );
212 }
213 
214 /// Serializer for vibe.d framework.
215 struct YamlSerializer
216 {
217 private:
218     Node m_current;
219     Node[] m_compositeStack;
220 
221 public:
222     enum isYamlBasicType( T ) = isNumeric!T || isBoolean!T || is( T == string ) || is( T == typeof(null) ) || isYamlSerializable!T;
223     enum isSupportedValueType( T ) = isYamlBasicType!T || is( T == Node );
224 
225     this( Node data ) { m_current = data; }
226     @disable this(this);
227 
228     //
229     // serialization
230     //
231     Node getSerializedResult() { return m_current; }
232     void beginWriteDictionary( T )() { m_compositeStack ~= Node( cast(string[])[], cast(string[])[] ); }
233     void endWriteDictionary( T )() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; }
234     void beginWriteDictionaryEntry( T )(string name) {}
235     void endWriteDictionaryEntry( T )(string name) { m_compositeStack[$-1][name] = m_current; }
236 
237     void beginWriteArray( T )( size_t ) { m_compositeStack ~= Node( cast(string[])[] ); }
238     void endWriteArray( T )() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; }
239     void beginWriteArrayEntry( T )( size_t ) {}
240     void endWriteArrayEntry( T )( size_t ) { m_compositeStack[$-1].add( m_current ); }
241 
242     void writeValue( T )( T value )
243     {
244         static if( is( T == Node ) )
245             m_current = value;
246         else static if( isYamlSerializable!T )
247             m_current = value.toYaml();
248         else static if( is( T == typeof(null) ) )
249             m_current = Node( YAMLNull() );
250         else
251             m_current = Node( value );
252     }
253 
254     //
255     // deserialization
256     //
257     void readDictionary( T )( scope void delegate( string ) field_handler )
258     {
259         enforceYaml( m_current.isMapping, "Yaml map expected, got a " ~ ( m_current.isScalar ? "scalar" : "sequence" ) ~ " instead." );
260 
261         auto old = m_current;
262         foreach( string key, Node value; m_current )
263         {
264             m_current = value;
265             field_handler( key );
266         }
267         m_current = old;
268     }
269 
270     void readArray( T )( scope void delegate( size_t ) size_callback, scope void delegate() entry_callback )
271     {
272         enforceYaml( m_current.isSequence || m_current.isScalar, "Yaml scalar or sequence expected, got a map instead." );
273 
274         if( m_current.isSequence )
275         {
276             auto old = m_current;
277             size_callback( m_current.length );
278             foreach( Node ent; old )
279             {
280                 m_current = ent;
281                 entry_callback();
282             }
283             m_current = old;
284         }
285         else
286         {
287             entry_callback();
288         }
289     }
290 
291     T readValue( T )()
292     {
293         static if( is( T == Node ) )
294             return m_current;
295         else static if( isYamlSerializable!T )
296             return T.fromYaml( m_current );
297         else
298             return m_current.get!T();
299     }
300 
301     bool tryReadNull() { return m_current.isNull; }
302 }
303 
304 unittest
305 {
306     import std.stdio;
307     writeln( "Dash Config YamlSerializer unittest" );
308 
309     Node str = serializeToYaml( "MyString" );
310     assert( str.isScalar );
311     assert( str.get!string == "MyString" );
312 
313     struct LetsSeeWhatHappens
314     {
315         string key;
316         string value;
317     }
318 
319     Node obj = serializeToYaml( LetsSeeWhatHappens( "Key", "Value" ) );
320     assert( obj.isMapping );
321     assert( obj[ "key" ] == "Key" );
322     assert( obj[ "value" ] == "Value" );
323 
324     auto lswh = deserializeYaml!LetsSeeWhatHappens( obj );
325     assert( lswh.key == "Key" );
326     assert( lswh.value == "Value" );
327 }
328 
329 private:
330 void enforceYaml( string file = __FILE__, size_t line = __LINE__ )( bool cond, lazy string message = "YAML exception" )
331 {
332     import std.exception;
333     enforceEx!Exception(cond, message, file, line);
334 }