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 }