1 module tagion.utils.JSONCommon; 2 3 import std.meta : AliasSeq; 4 import std.traits : hasMember; 5 import tagion.basic.tagionexceptions; 6 7 /++ 8 +/ 9 @safe 10 class OptionException : TagionException { 11 this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow { 12 super(msg, file, line); 13 } 14 } 15 16 enum isJSONCommon(T) = is(T == struct) && hasMember!(T, "toJSON"); 17 18 /++ 19 mixin for implememts a JSON interface for a struct 20 +/ 21 mixin template JSONCommon() { 22 import tagion.basic.tagionexceptions : Check; 23 import tagion.utils.JSONCommon : OptionException; 24 25 alias check = Check!OptionException; 26 import std.conv : to; 27 import std.format; 28 import JSON = std.json; 29 import std.meta : AliasSeq; 30 import std.range : ElementType; 31 import std.traits; 32 import tagion.basic.basic : assumeTrusted, basename, isOneOf; 33 34 // import std.traits : isArray; 35 alias ArrayElementTypes = AliasSeq!(bool, string, double, int); 36 37 enum isSupportedArray(T) = isArray!T && isSupported!(ElementType!T); 38 enum isSupportedAssociativeArray(T) = isAssociativeArray!T && is(KeyType!T == string) && isSupported!(ForeachType!T); 39 enum isSupported(T) = isOneOf!(T, ArrayElementTypes) || isNumeric!T || 40 isSupportedArray!T || isJSONCommon!T || 41 isSupportedAssociativeArray!T; 42 43 /++ 44 Returns: 45 JSON of the struct 46 +/ 47 JSON.JSONValue toJSON() const @safe { 48 JSON.JSONValue result; 49 auto get(type, T)(T val) { 50 51 static if (is(type == enum)) { 52 return val.to!string; 53 } 54 else static if (is(type : immutable(ubyte[]))) { 55 return val.toHexString; 56 } 57 else static if (is(type == struct)) { 58 return val.toJSON; 59 } 60 else { 61 return val; 62 } 63 } 64 65 foreach (i, m; this.tupleof) { 66 enum name = basename!(this.tupleof[i]); 67 alias type = typeof(m); 68 static if (is(type == struct)) { 69 result[name] = m.toJSON; 70 } 71 else static if (isArray!type && isSupported!(ForeachType!type)) { 72 alias ElemType = ForeachType!type; 73 JSON.JSONValue[] array; 74 foreach (ref m_element; m) { 75 JSON.JSONValue val = get!ElemType(m_element); 76 array ~= val; 77 } 78 result[name] = array; // ~= m_element.toJSON; 79 } 80 else static if (isSupportedAssociativeArray!type) { 81 JSON.JSONValue obj; 82 alias ElemType = ForeachType!type; 83 foreach (key, m_element; m) { 84 obj[key] = get!ElemType(m_element); 85 } 86 result[name] = obj; 87 } 88 else { 89 result[name] = get!(type)(m); 90 version (none) 91 static if (is(type == enum)) { 92 result[name] = m.to!string; 93 } 94 else static if (is(type : immutable(ubyte[]))) { 95 result[name] = m.toHexString; 96 } 97 else { 98 result[name] = m; 99 } 100 } 101 } 102 return result; 103 } 104 105 /++ 106 Stringify the struct 107 Params: 108 pretty = if true the return string is prettified else the string returned is compact 109 +/ 110 string stringify(bool pretty = true)() const @safe { 111 static if (pretty) { 112 return toJSON.toPrettyString; 113 } 114 else { 115 return toJSON.toString; 116 } 117 } 118 119 /++ 120 Intialize a struct from a JSON 121 Params: 122 json_value = JSON used 123 +/ 124 void parse(ref JSON.JSONValue json_value) @safe { 125 static void set_array(T)(ref T m, ref JSON.JSONValue[] json_array, string name) @safe if (isSupportedArray!T) { 126 foreach (_json_value; json_array) { 127 ElementType!T m_element; 128 set(m_element, _json_value, name); 129 m ~= m_element; 130 } 131 } 132 133 static void set_hashmap(T)(ref T m, ref JSON.JSONValue[string] json_array, string name) @safe 134 if (isSupportedAssociativeArray!T) { 135 alias ElemType = ForeachType!T; 136 foreach (key, json_value; json_array) { 137 ElemType val; 138 set(val, json_value, name); 139 m[key] = val; 140 } 141 142 } 143 144 static bool set(T)(ref T m, ref JSON.JSONValue _json_value, string _name) @safe { 145 146 static if (is(T == struct)) { 147 m.parse(_json_value); 148 } 149 else static if (is(T == enum)) { 150 if (_json_value.type is JSON.JSONType..string) { 151 switch (_json_value.str) { 152 foreach (E; EnumMembers!T) { 153 case E.to!string: 154 m = E; 155 return false; 156 // continue ParseLoop; 157 } 158 default: 159 check(0, format("Illegal value of %s only %s supported not %s", 160 _name, [EnumMembers!T], 161 _json_value.str)); 162 } 163 } 164 else static if (isIntegral!(BuiltinTypeOf!T)) { 165 if (_json_value.type is JSON.JSONType.integer) { 166 const value = _json_value.integer; 167 switch (value) { 168 foreach (E; EnumMembers!T) { 169 case E: 170 m = E; 171 return false; 172 //continue ParseLoop; 173 } 174 default: 175 // Fail; 176 177 } 178 } 179 check(0, format("Illegal value of %s only %s supported not %s", 180 _name, 181 [EnumMembers!T], 182 _json_value.uinteger)); 183 } 184 check(0, format("Illegal value of %s", _name)); 185 } 186 else static if (is(T == string)) { 187 m = _json_value.str; 188 } 189 else static if (isIntegral!T || isFloatingPoint!T) { 190 static if (isIntegral!T) { 191 auto value = _json_value.integer; 192 check((value >= T.min) && (value <= T.max), format("Value %d out of range for type %s of %s", value, T 193 .stringof, m.stringof)); 194 } 195 else { 196 auto value = _json_value.floating; 197 } 198 m = cast(T) value; 199 200 } 201 else static if (is(T == bool)) { 202 check((_json_value.type == JSON.JSONType.true_) || ( 203 _json_value.type == JSON.JSONType.false_), 204 format("Type %s expected for %s but the json type is %s", T.stringof, m.stringof, _json_value 205 .type)); 206 m = _json_value.type == JSON.JSONType.true_; 207 } 208 else static if (isSupportedArray!T) { 209 check(_json_value.type is JSON.JSONType.array, 210 format("Type of member '%s' must be an %s", _name, JSON.JSONType.array)); 211 (() @trusted => set_array(m, _json_value.array, _name))(); 212 213 } 214 else static if (isSupportedAssociativeArray!T) { 215 check(_json_value.type is JSON.JSONType.object, 216 format("Type of member '%s' must be an %s", _name, JSON.JSONType.object)); 217 (() @trusted => set_hashmap(m, _json_value.object, _name))(); 218 219 } 220 else { 221 check(0, format("Unsupported type %s for '%s' member", T.stringof, _name)); 222 } 223 return true; 224 } 225 226 ParseLoop: foreach (i, ref member; this.tupleof) { 227 enum name = basename!(this.tupleof[i]); 228 alias type = typeof(member); 229 // static if (!is(type == struct) || !is(type == class)) { 230 static assert(is(type == struct) || is(type == enum) || isSupported!type, 231 format("Unsupported type %s for '%s' member", type.stringof, name)); 232 233 if (!set(member, json_value[name], name)) { 234 continue ParseLoop; 235 } 236 237 } 238 } 239 240 } 241 242 static T load(T)(string config_file) if (__traits(hasMember, T, "load")) { 243 T result; 244 result.load(config_file); 245 return result; 246 } 247 248 import std.conv; 249 import std.json : JSONType, JSONValue; 250 251 JSONValue toJSONType(string str, JSONType type) @safe pure { 252 with (JSONType) final switch (type) { 253 case float_: 254 return JSONValue(str.to!float); 255 case integer: 256 return JSONValue(str.to!int); 257 case uinteger: 258 return JSONValue(str.to!uint); 259 case null_: 260 return JSONValue(null); 261 case object, array, string: //best guess 262 return JSONValue(str); 263 case false_, true_: 264 return JSONValue(str.to!bool); 265 } 266 } 267 268 mixin template JSONConfig() { 269 import std.file; 270 import JSON = std.json; 271 272 void parseJSON(string json_text) @safe { 273 auto json = JSON.parseJSON(json_text); 274 parse(json); 275 } 276 277 void load(string config_file) @safe { 278 if (config_file.exists) { 279 auto json_text = readText(config_file); 280 parseJSON(json_text); 281 } 282 else { 283 save(config_file); 284 } 285 } 286 287 void save(const string config_file) @safe const { 288 config_file.write(stringify); 289 } 290 } 291 292 version (unittest) { 293 import std.exception : assertThrown; 294 import std.json : JSONException; 295 import tagion.basic.Types : FileExtension; 296 import basic = tagion.basic.basic; 297 298 const(basic.FileNames) fileId(T)(string prefix = null) @safe { 299 return basic.fileId!T(FileExtension.json, prefix); 300 } 301 302 private enum Color { 303 red, 304 green, 305 blue, 306 } 307 308 } 309 310 @safe 311 unittest { 312 static struct OptS { 313 bool _bool; 314 string _string; 315 // double _double; 316 int _int; 317 uint _uint; 318 Color color; 319 mixin JSONCommon; 320 mixin JSONConfig; 321 } 322 323 OptS opt; 324 { // Simple JSONCommon check 325 opt._bool = true; 326 opt._string = "text"; 327 // opt._double=4.2; 328 opt._int = -42; 329 opt._uint = 42; 330 opt.color = Color.blue; 331 332 immutable filename = fileId!OptS.fullpath; 333 opt.save(filename); 334 OptS opt_loaded; 335 opt_loaded.load(filename); 336 assert(opt == opt_loaded); 337 } 338 static struct OptMain { 339 OptS sub_opt; 340 int main_x; 341 mixin JSONCommon; 342 mixin JSONConfig; 343 } 344 345 immutable main_filename = fileId!OptMain.fullpath; 346 347 { // Common check with sub a sub-structure 348 OptMain opt_main; 349 opt_main.sub_opt = opt; 350 opt_main.main_x = 117; 351 opt_main.save(main_filename); 352 //FileExtension 353 OptMain opt_loaded; 354 opt_loaded.load(main_filename); 355 assert(opt_main == opt_loaded); 356 } 357 358 { // Check for bad JSONCommon file 359 OptS opt_s; 360 //immutable bad_filename = fileId!OptMain("bad").fullpath; 361 assertThrown!JSONException(opt_s.load(main_filename)); 362 } 363 364 } 365 366 @safe 367 unittest { // JSONCommon with array types 368 // import std.stdio; 369 static struct OptArray(T) { 370 T[] list; 371 mixin JSONCommon; 372 mixin JSONConfig; 373 } 374 375 //alias StdType=AliasSeq!(bool, int, string, Color); 376 377 { // Check JSONCommon with array of booleans 378 alias OptA = OptArray!bool; 379 OptA opt; 380 opt.list = [true, false, false, true, false]; 381 immutable filename = fileId!OptA.fullpath; 382 383 opt.save(filename); 384 385 OptA opt_loaded; 386 opt_loaded.load(filename); 387 388 assert(opt_loaded == opt); 389 } 390 391 { // Check JSONCommon with array of string 392 alias OptA = OptArray!string; 393 OptA opt; 394 opt.list = ["Hugo", "Borge", "Brian", "Johnny", "Sven Bendt"]; 395 immutable filename = fileId!OptA.fullpath; 396 397 opt.save(filename); 398 399 OptA opt_loaded; 400 opt_loaded.load(filename); 401 402 assert(opt_loaded == opt); 403 } 404 405 { // Check JSONCommon with array of integers 406 alias OptA = OptArray!int; 407 OptA opt; 408 opt.list = [42, -16, 117]; 409 immutable filename = fileId!OptA.fullpath; 410 411 opt.save(filename); 412 413 OptA opt_loaded; 414 opt_loaded.load(filename); 415 416 assert(opt_loaded == opt); 417 } 418 419 { 420 static struct OptSub { 421 string text; 422 mixin JSONCommon; 423 } 424 425 alias OptA = OptArray!OptSub; 426 OptA opt; 427 opt.list = [OptSub("Hugo"), OptSub("Borge"), OptSub("Brian")]; 428 immutable filename = fileId!OptA.fullpath; 429 430 opt.save(filename); 431 432 OptA opt_loaded; 433 opt_loaded.load(filename); 434 435 assert(opt_loaded == opt); 436 } 437 } 438 439 @safe 440 unittest { // Check of support for associative array 441 static struct S { 442 string[string] names; 443 mixin JSONCommon; 444 } 445 446 S s; 447 s.names["Hugo"] = "big"; 448 s.names["Borge"] = "small"; 449 450 auto json = s.toJSON; 451 452 S s_result; 453 s_result.parse(json); 454 assert("Hugo" in s_result.names); 455 assert("Borge" in s_result.names); 456 assert(s_result.names["Hugo"] == "big"); 457 assert(s_result.names["Borge"] == "small"); 458 459 }