diff --git a/build-files.txt b/build-files.txt index 9a1c99d..c297f27 100644 --- a/build-files.txt +++ b/build-files.txt @@ -33,6 +33,7 @@ source/dub/internal/vibecompat/core/file.d source/dub/internal/vibecompat/core/log.d source/dub/internal/vibecompat/data/json.d +source/dub/internal/vibecompat/data/serialization.d source/dub/internal/vibecompat/data/utils.d source/dub/internal/vibecompat/inet/path.d source/dub/internal/vibecompat/inet/url.d diff --git a/source/dub/internal/vibecompat/data/json.d b/source/dub/internal/vibecompat/data/json.d index 22e12f1..0faca18 100644 --- a/source/dub/internal/vibecompat/data/json.d +++ b/source/dub/internal/vibecompat/data/json.d @@ -1,3 +1,14 @@ +/** + JSON serialization and value handling. + + This module provides the Json struct for reading, writing and manipulating + JSON values. De(serialization) of arbitrary D types is also supported and + is recommended for handling JSON in performance sensitive applications. + + Copyright: © 2012-2015 RejectedSoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ module dub.internal.vibecompat.data.json; version (Have_vibe_d) public import vibe.data.json; @@ -5,6 +16,10 @@ import dub.internal.vibecompat.data.utils; +public import dub.internal.vibecompat.data.serialization; + +public import std.json : JSONException; +import std.algorithm : equal, min; import std.array; import std.conv; import std.datetime; @@ -28,52 +43,61 @@ behave mostly like values in ECMA script in the way that you can transparently perform operations on them. However, strict typechecking is done, so that operations between differently typed JSON values will throw - an exception. Additionally, an explicit cast or using get!() or to!() is + a JSONException. Additionally, an explicit cast or using get!() or to!() is required to convert a JSON value to the corresponding static D type. */ struct Json { private { - union { - bool m_bool; - long m_int; - double m_float; - string m_string; - Json[] m_array; - Json[string] m_object; - }; + // putting all fields in a union results in many false pointers leading to + // memory leaks and, worse, std.algorithm.swap triggering an assertion + // because of internal pointers. This crude workaround seems to fix + // the issues. + void*[2] m_data; + ref inout(T) getDataAs(T)() inout { static assert(T.sizeof <= m_data.sizeof); return *cast(inout(T)*)m_data.ptr; } + @property ref inout(long) m_int() inout { return getDataAs!long(); } + @property ref inout(double) m_float() inout { return getDataAs!double(); } + @property ref inout(bool) m_bool() inout { return getDataAs!bool(); } + @property ref inout(string) m_string() inout { return getDataAs!string(); } + @property ref inout(Json[string]) m_object() inout { return getDataAs!(Json[string])(); } + @property ref inout(Json[]) m_array() inout { return getDataAs!(Json[])(); } + Type m_type = Type.undefined; - uint m_magic = 0x1337f00d; // workaround for Appender bug - string m_name; + + version (VibeJsonFieldNames) { + uint m_magic = 0x1337f00d; // works around Appender bug (DMD BUG 10690/10859/11357) + string m_name; + } } /** Represents the run time type of a JSON value. */ enum Type { - /// A non-existent value in a JSON object - undefined, - /// Null value - null_, - /// Boolean value - bool_, - /// 64-bit integer value - int_, - /// 64-bit floating point value - float_, - /// UTF-8 string - string, - /// Array of JSON values - array, - /// JSON object aka. dictionary from string to Json - object + undefined, /// A non-existent value in a JSON object + null_, /// Null value + bool_, /// Boolean value + int_, /// 64-bit integer value + float_, /// 64-bit floating point value + string, /// UTF-8 string + array, /// Array of JSON values + object, /// JSON object aka. dictionary from string to Json + + Undefined = undefined, /// Compatibility alias - will be deprecated soon + Null = null_, /// Compatibility alias - will be deprecated soon + Bool = bool_, /// Compatibility alias - will be deprecated soon + Int = int_, /// Compatibility alias - will be deprecated soon + Float = float_, /// Compatibility alias - will be deprecated soon + String = string, /// Compatibility alias - will be deprecated soon + Array = array, /// Compatibility alias - will be deprecated soon + Object = object /// Compatibility alias - will be deprecated soon } - /// New JSON value of Type.undefined + /// New JSON value of Type.Undefined static @property Json undefined() { return Json(); } - /// New JSON value of Type.object + /// New JSON value of Type.Object static @property Json emptyObject() { return Json(cast(Json[string])null); } - /// New JSON value of Type.array + /// New JSON value of Type.Array static @property Json emptyArray() { return Json(cast(Json[])null); } version(JsonLineNumbers) int line; @@ -85,7 +109,17 @@ /// ditto this(bool v) { m_type = Type.bool_; m_bool = v; } /// ditto - this(int v) { m_type = Type.int_; m_int = v; } + this(byte v) { this(cast(long)v); } + /// ditto + this(ubyte v) { this(cast(long)v); } + /// ditto + this(short v) { this(cast(long)v); } + /// ditto + this(ushort v) { this(cast(long)v); } + /// ditto + this(int v) { this(cast(long)v); } + /// ditto + this(uint v) { this(cast(long)v); } /// ditto this(long v) { m_type = Type.int_; m_int = v; } /// ditto @@ -100,7 +134,8 @@ /** Allows assignment of D values to a JSON value. */ - ref Json opAssign(Json v){ + ref Json opAssign(Json v) + { m_type = v.m_type; final switch(m_type){ case Type.undefined: m_string = null; break; @@ -109,14 +144,8 @@ case Type.int_: m_int = v.m_int; break; case Type.float_: m_float = v.m_float; break; case Type.string: m_string = v.m_string; break; - case Type.array: - m_array = v.m_array; - if (m_magic == 0x1337f00d) { foreach (ref av; m_array) av.m_name = m_name; } else m_name = null; - break; - case Type.object: - m_object = v.m_object; - if (m_magic == 0x1337f00d) { foreach (k, ref av; m_object) av.m_name = m_name ~ "." ~ k; } else m_name = null; - break; + case Type.array: opAssign(v.m_array); break; + case Type.object: opAssign(v.m_object); break; } return this; } @@ -137,7 +166,7 @@ { m_type = Type.array; m_array = v; - if (m_magic == 0x1337f00d) foreach (ref av; m_array) av.m_name = m_name; + version (VibeJsonFieldNames) { if (m_magic == 0x1337f00d) { foreach (idx, ref av; m_array) av.m_name = format("%s[%s]", m_name, idx); } else m_name = null; } return v; } /// ditto @@ -145,44 +174,113 @@ { m_type = Type.object; m_object = v; - if (m_magic == 0x1337f00d) foreach (k, ref av; m_object) av.m_name = m_name ~ "." ~ k; + version (VibeJsonFieldNames) { if (m_magic == 0x1337f00d) { foreach (key, ref av; m_object) av.m_name = format("%s.%s", m_name, key); } else m_name = null; } return v; } /** + Allows removal of values from Type.Object Json objects. + */ + void remove(string item) { checkType!(Json[string])(); m_object.remove(item); } + + /** The current type id of this JSON object. */ @property Type type() const { return m_type; } /** + Clones a JSON value recursively. + */ + Json clone() + const { + final switch (m_type) { + case Type.undefined: return Json.undefined; + case Type.null_: return Json(null); + case Type.bool_: return Json(m_bool); + case Type.int_: return Json(m_int); + case Type.float_: return Json(m_float); + case Type.string: return Json(m_string); + case Type.array: + auto ret = Json.emptyArray; + foreach (v; this) ret ~= v.clone(); + return ret; + case Type.object: + auto ret = Json.emptyObject; + foreach (string name, v; this) ret[name] = v.clone(); + return ret; + } + } + + /** + Check whether the JSON object contains the given key and if yes, + return a pointer to the corresponding object, otherwise return `null`. + */ + inout(Json*) opBinaryRight(string op : "in")(string key) inout { + checkType!(Json[string])(); + return key in m_object; + } + + /** Allows direct indexing of array typed JSON values. */ ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; } + /// + unittest { + Json value = Json.emptyArray; + value ~= 1; + value ~= true; + value ~= "foo"; + assert(value[0] == 1); + assert(value[1] == true); + assert(value[2] == "foo"); + } + + /** Allows direct indexing of object typed JSON values using a string as the key. */ - const(Json) opIndex(string key) const { + const(Json) opIndex(string key) + const { checkType!(Json[string])(); if( auto pv = key in m_object ) return *pv; Json ret = Json.undefined; ret.m_string = key; + version (VibeJsonFieldNames) ret.m_name = format("%s.%s", m_name, key); return ret; } /// ditto - ref Json opIndex(string key){ + ref Json opIndex(string key) + { checkType!(Json[string])(); if( auto pv = key in m_object ) return *pv; - m_object[key] = Json(); + if (m_object is null) { + m_object = ["": Json.init]; + m_object.remove(""); + } + m_object[key] = Json.init; + assert(m_object !is null); + assert(key in m_object, "Failed to insert key '"~key~"' into AA!?"); m_object[key].m_type = Type.undefined; // DMDBUG: AAs are teh $H1T!!!11 assert(m_object[key].type == Type.undefined); - m_object[key].m_name = m_name ~ "." ~ key; m_object[key].m_string = key; + version (VibeJsonFieldNames) m_object[key].m_name = format("%s.%s", m_name, key); return m_object[key]; } + /// + unittest { + Json value = Json.emptyObject; + value["a"] = 1; + value["b"] = true; + value["c"] = "foo"; + assert(value["a"] == 1); + assert(value["b"] == true); + assert(value["c"] == "foo"); + } + /** Returns a slice of a JSON array. */ @@ -191,16 +289,11 @@ inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; } /** - Removes an entry from an object. - */ - void remove(string item) { checkType!(Json[string])(); m_object.remove(item); } - - /** Returns the number of entries of string, array or object typed JSON values. */ @property size_t length() const { - checkType!(string, Json[], Json[string]); + checkType!(string, Json[], Json[string])("property length"); switch(m_type){ case Type.string: return m_string.length; case Type.array: return m_array.length; @@ -214,7 +307,7 @@ */ int opApply(int delegate(ref Json obj) del) { - checkType!(Json[], Json[string]); + checkType!(Json[], Json[string])("opApply"); if( m_type == Type.array ){ foreach( ref v; m_array ) if( auto ret = del(v) ) @@ -231,7 +324,7 @@ /// ditto int opApply(int delegate(ref const Json obj) del) const { - checkType!(Json[], Json[string]); + checkType!(Json[], Json[string])("opApply"); if( m_type == Type.array ){ foreach( ref v; m_array ) if( auto ret = del(v) ) @@ -248,7 +341,7 @@ /// ditto int opApply(int delegate(ref size_t idx, ref Json obj) del) { - checkType!(Json[]); + checkType!(Json[])("opApply"); foreach( idx, ref v; m_array ) if( auto ret = del(idx, v) ) return ret; @@ -257,7 +350,7 @@ /// ditto int opApply(int delegate(ref size_t idx, ref const Json obj) del) const { - checkType!(Json[]); + checkType!(Json[])("opApply"); foreach( idx, ref v; m_array ) if( auto ret = del(idx, v) ) return ret; @@ -266,7 +359,7 @@ /// ditto int opApply(int delegate(ref string idx, ref Json obj) del) { - checkType!(Json[string]); + checkType!(Json[string])("opApply"); foreach( idx, ref v; m_object ) if( v.type != Type.undefined ) if( auto ret = del(idx, v) ) @@ -276,7 +369,7 @@ /// ditto int opApply(int delegate(ref string idx, ref const Json obj) del) const { - checkType!(Json[string]); + checkType!(Json[string])("opApply"); foreach( idx, ref v; m_object ) if( v.type != Type.undefined ) if( auto ret = del(idx, v) ) @@ -286,23 +379,46 @@ /** Converts the JSON value to the corresponding D type - types must match exactly. + + Available_Types: + $(UL + $(LI `bool` (`Type.bool_`)) + $(LI `double` (`Type.float_`)) + $(LI `float` (Converted from `double`)) + $(LI `long` (`Type.int_`)) + $(LI `ulong`, `int`, `uint`, `short`, `ushort`, `byte`, `ubyte` (Converted from `long`)) + $(LI `string` (`Type.string`)) + $(LI `Json[]` (`Type.array`)) + $(LI `Json[string]` (`Type.object`)) + ) + + See_Also: `opt`, `to`, `deserializeJson` */ inout(T) opCast(T)() inout { return get!T; } /// ditto @property inout(T) get(T)() inout { checkType!T(); - static if( is(T == bool) ) return m_bool; - else static if( is(T == double) ) return m_float; - else static if( is(T == float) ) return cast(T)m_float; - else static if( is(T == long) ) return m_int; - else static if( is(T : long) ){ enforce(m_int <= T.max && m_int >= T.min); return cast(T)m_int; } - else static if( is(T == string) ) return m_string; - else static if( is(T == Json[]) ) return m_array; - else static if( is(T == Json[string]) ) return m_object; - else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + static if (is(T == bool)) return m_bool; + else static if (is(T == double)) return m_float; + else static if (is(T == float)) return cast(T)m_float; + else static if (is(T == long)) return m_int; + else static if (is(T == ulong)) return cast(ulong)m_int; + else static if (is(T : long)){ enforceJson(m_int <= T.max && m_int >= T.min, "Integer conversion out of bounds error"); return cast(T)m_int; } + else static if (is(T == string)) return m_string; + else static if (is(T == Json[])) return m_array; + else static if (is(T == Json[string])) return m_object; + else static assert("JSON can only be cast to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); } - /// ditto + + /** + Returns the native type for this JSON if it matches the current runtime type. + + If the runtime type does not match the given native type, the 'def' parameter is returned + instead. + + See_Also: `get` + */ @property const(T) opt(T)(const(T) def = T.init) const { if( typeId!T != m_type ) return def; @@ -316,7 +432,13 @@ } /** - Converts the JSON value to the corresponding D type - types are converted as neccessary. + Converts the JSON value to the corresponding D type - types are converted as necessary. + + Automatically performs conversions between strings and numbers. See + `get` for the list of available types. For converting/deserializing + JSON to complex data types see `deserializeJson`. + + See_Also: `get`, `deserializeJson` */ @property inout(T) to(T)() inout { @@ -391,7 +513,7 @@ default: return Json(["value": this]); case Type.object: return m_object; } - } else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); + } else static assert("JSON can only be cast to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~"."); } /** @@ -415,7 +537,7 @@ checkType!bool(); return Json(~m_bool); } else static if( op == "+" || op == "-" || op == "++" || op == "--" ){ - checkType!(long, double); + checkType!(long, double)("unary "~op); if( m_type == Type.int_ ) mixin("return Json("~op~"m_int);"); else if( m_type == Type.float_ ) mixin("return Json("~op~"m_float);"); else assert(false); @@ -425,7 +547,7 @@ /** Performs binary operations between JSON values. - The two JSON values must be of the same run time type or an exception + The two JSON values must be of the same run time type or a JSONException will be thrown. Only the operations listed are allowed for each of the types. @@ -436,46 +558,47 @@ $(DT Float) $(DD +, -, *, /, %) $(DT String) $(DD ~) $(DT Array) $(DD ~) - $(DT Object) $(DD none) + $(DT Object) $(DD in) ) */ Json opBinary(string op)(ref const(Json) other) const { - enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + enforceJson(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); static if( op == "&&" ){ - checkType!(bool)("'&&'"); other.checkType!(bool)("'&&'"); + checkType!(bool)(op); return Json(m_bool && other.m_bool); } else static if( op == "||" ){ - checkType!(bool)("'||'"); other.checkType!(bool)("'||'"); + checkType!(bool)(op); return Json(m_bool || other.m_bool); } else static if( op == "+" ){ - checkType!(double, long)("'+'"); other.checkType!(double, long)("'+'"); - if( m_type == Type.int_ ) return Json(m_int + other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int + other.m_int); else if( m_type == Type.float_ ) return Json(m_float + other.m_float); else assert(false); } else static if( op == "-" ){ - checkType!(double, long)("'-'"); other.checkType!(double, long)("'-'"); - if( m_type == Type.int_ ) return Json(m_int - other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int - other.m_int); else if( m_type == Type.float_ ) return Json(m_float - other.m_float); else assert(false); } else static if( op == "*" ){ - checkType!(double, long)("'*'"); other.checkType!(double, long)("'*'"); - if( m_type == Type.int_ ) return Json(m_int * other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int * other.m_int); else if( m_type == Type.float_ ) return Json(m_float * other.m_float); else assert(false); } else static if( op == "/" ){ - checkType!(double, long)("'/'"); other.checkType!(double, long)("'/'"); - if( m_type == Type.int_ ) return Json(m_int / other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int / other.m_int); else if( m_type == Type.float_ ) return Json(m_float / other.m_float); else assert(false); } else static if( op == "%" ){ - checkType!(double, long)("'%'"); other.checkType!(double, long)("'%'"); - if( m_type == Type.int_ ) return Json(m_int % other.m_int); + checkType!(long, double)(op); + if( m_type == Type.Int ) return Json(m_int % other.m_int); else if( m_type == Type.float_ ) return Json(m_float % other.m_float); else assert(false); } else static if( op == "~" ){ - checkType!(string)("'~'"); other.checkType!(string)("'~'"); + checkType!(string, Json[])(op); if( m_type == Type.string ) return Json(m_string ~ other.m_string); + else if (m_type == Type.array) return Json(m_array ~ other.m_array); else assert(false); } else static assert("Unsupported operator '"~op~"' for type JSON."); } @@ -484,7 +607,7 @@ if( op == "~" ) { static if( op == "~" ){ - checkType!(string, Json[])("'~'"); other.checkType!(string, Json[])("'~'"); + checkType!(string, Json[])(op); if( m_type == Type.string ) return Json(m_string ~ other.m_string); else if( m_type == Type.array ) return Json(m_array ~ other.m_array); else assert(false); @@ -492,35 +615,43 @@ } /// ditto void opOpAssign(string op)(Json other) - if( op == "+" || op == "-" || op == "*" ||op == "/" || op == "%" ) + if (op == "+" || op == "-" || op == "*" || op == "/" || op == "%" || op =="~") { - enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); + enforceJson(m_type == other.m_type || op == "~" && m_type == Type.array, + "Binary operation '"~op~"=' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects."); static if( op == "+" ){ if( m_type == Type.int_ ) m_int += other.m_int; else if( m_type == Type.float_ ) m_float += other.m_float; - else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'+=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "-" ){ if( m_type == Type.int_ ) m_int -= other.m_int; else if( m_type == Type.float_ ) m_float -= other.m_float; - else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'-=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "*" ){ if( m_type == Type.int_ ) m_int *= other.m_int; else if( m_type == Type.float_ ) m_float *= other.m_float; - else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'*=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "/" ){ if( m_type == Type.int_ ) m_int /= other.m_int; else if( m_type == Type.float_ ) m_float /= other.m_float; - else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~"."); + else enforceJson(false, "'/=' only allowed for scalar types, not "~.to!string(m_type)~"."); } else static if( op == "%" ){ if( m_type == Type.int_ ) m_int %= other.m_int; else if( m_type == Type.float_ ) m_float %= other.m_float; - else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); - } /*else static if( op == "~" ){ - if( m_type == Type.string ) m_string ~= other.m_string; - else if( m_type == Type.array ) m_array ~= other.m_array; - else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~"."); - }*/ else static assert("Unsupported operator '"~op~"' for type JSON."); - assert(false); + else enforceJson(false, "'%=' only allowed for scalar types, not "~.to!string(m_type)~"."); + } else static if( op == "~" ){ + if (m_type == Type.string) m_string ~= other.m_string; + else if (m_type == Type.array) { + if (other.m_type == Type.array) m_array ~= other.m_array; + else appendArrayElement(other); + } else enforceJson(false, "'~=' only allowed for string and array types, not "~.to!string(m_type)~"."); + } else static assert("Unsupported operator '"~op~"=' for type JSON."); + } + /// ditto + void opOpAssign(string op, T)(T other) + if (!is(T == Json) && is(typeof(Json(other)))) + { + opOpAssign!op(Json(other)); } /// ditto Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); } @@ -531,11 +662,7 @@ /// ditto Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); } /// ditto - Json opBinary(string op)(Json other) - if (op == "~") { - if (m_type == Type.array) return Json(m_array ~ other); - else return Json(this ~ other); - } + Json opBinary(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(m_array "~op~" other);"); } /// ditto Json opBinaryRight(string op)(bool other) const { checkType!bool(); mixin("return Json(other "~op~" m_bool);"); } /// ditto @@ -552,8 +679,20 @@ if( pv.type == Type.undefined ) return null; return pv; } + /// ditto + Json opBinaryRight(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(other "~op~" m_array);"); } /** + * The append operator will append arrays. This method always appends it's argument as an array element, so nested arrays can be created. + */ + void appendArrayElement(Json element) + { + enforceJson(m_type == Type.array, "'appendArrayElement' only allowed for array types, not "~.to!string(m_type)~"."); + m_array ~= element; + } + + /** Scheduled for deprecation, please use `opIndex` instead. + Allows to access existing fields of a JSON object using dot syntax. */ @property const(Json) opDispatch(string prop)() const { return opIndex(prop); } @@ -588,6 +727,8 @@ /// ditto bool opEquals(bool v) const { return m_type == Type.bool_ && m_bool == v; } /// ditto + bool opEquals(int v) const { return m_type == Type.int_ && m_int == v; } + /// ditto bool opEquals(long v) const { return m_type == Type.int_ && m_int == v; } /// ditto bool opEquals(double v) const { return m_type == Type.float_ && m_float == v; } @@ -616,12 +757,12 @@ case Type.string: return m_string < other.m_string ? -1 : m_string == other.m_string ? 0 : 1; case Type.array: return m_array < other.m_array ? -1 : m_array == other.m_array ? 0 : 1; case Type.object: - enforce(false, "JSON objects cannot be compared."); + enforceJson(false, "JSON objects cannot be compared."); assert(false); } } - + alias opDollar = length; /** Returns the type id corresponding to the given D type. @@ -667,7 +808,7 @@ --- Params: - level = Specifies the base amount of indentation for the output. Indentation is always + level = Specifies the base amount of indentation for the output. Indentation is always done using tab characters. See_Also: writePrettyJsonString, toString @@ -679,32 +820,34 @@ return ret.data; } - private void checkType(T...)(string op = null) + private void checkType(TYPES...)(string op = null) const { - bool found = false; - foreach (t; T) if (m_type == typeId!t) found = true; - if (found) return; - if (T.length == 1) { - throw new Exception(format("Got %s - expected %s.", this.displayName, typeId!(T[0]).to!string)); - } else { - string types; - foreach (t; T) { - if (types.length) types ~= ", "; - types ~= typeId!t.to!string; - } - throw new Exception(format("Got %s - expected one of %s.", this.displayName, types)); - } - } + bool matched = false; + foreach (T; TYPES) if (m_type == typeId!T) matched = true; + if (matched) return; - private @property string displayName() - const { - if (m_name.length) return m_name ~ " of type " ~ m_type.to!string(); - else return "JSON of type " ~ m_type.to!string(); + string name; + version (VibeJsonFieldNames) { + if (m_name.length) name = m_name ~ " of type " ~ m_type.to!string; + else name = "JSON of type " ~ m_type.to!string; + } else name = "JSON of type " ~ m_type.to!string; + + string expected; + static if (TYPES.length == 1) expected = typeId!(TYPES[0]).to!string; + else { + foreach (T; TYPES) { + if (expected.length > 0) expected ~= ", "; + expected ~= typeId!T.to!string; + } + } + + if (!op.length) throw new JSONException(format("Got %s, expected %s.", name, expected)); + else throw new JSONException(format("Got %s, expected %s for %s.", name, expected, op)); } /*invariant() { - assert(m_type >= Type.undefined && m_type <= Type.object); + assert(m_type >= Type.Undefined && m_type <= Type.Object); }*/ } @@ -719,60 +862,59 @@ The range is shrunk during parsing, leaving any remaining text that is not part of the JSON contents. - Throws an Exception if any parsing error occured. + Throws a JSONException if any parsing error occured. */ -Json parseJson(R)(ref R range, string filename, int* line) +Json parseJson(R)(ref R range, int* line = null, string filename = null) if( is(R == string) ) { - import std.algorithm : min; - - assert(line !is null); Json ret; enforceJson(!range.empty, "JSON string is empty.", filename, 0); skipWhitespace(range, line); - version(JsonLineNumbers){ - import dub.internal.vibecompat.core.log; + version(JsonLineNumbers) { + import vibe.core.log; int curline = line ? *line : 0; } switch( range.front ){ case 'f': - enforceJson(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. min($, 5)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. min(5, $)]~"'.", filename, line); range.popFrontN(5); ret = false; break; case 'n': - enforceJson(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. min($, 4)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. min(4, $)]~"'.", filename, line); range.popFrontN(4); ret = null; break; case 't': - enforceJson(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. min($, 4)]~"'.", filename, *line); + enforceJson(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. min(4, $)]~"'.", filename, line); range.popFrontN(4); ret = true; break; - case '0': .. case '9'+1: + case '0': .. case '9': case '-': bool is_float; - auto num = skipNumber(range, is_float, filename, *line); + auto num = skipNumber(range, is_float); if( is_float ) ret = to!double(num); else ret = to!long(num); break; case '\"': - ret = skipJsonString(range, filename, line); + ret = skipJsonString(range); break; case '[': Json[] arr; range.popFront(); - while(true) { + while (true) { skipWhitespace(range, line); - enforceJson(!range.empty, "Missing ']' before EOF.", filename, *line); + enforceJson(!range.empty, "Missing ']' before EOF.", filename, line); if(range.front == ']') break; - arr ~= parseJson(range, filename, line); + arr ~= parseJson(range, line, filename); skipWhitespace(range, line); - enforceJson(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','.", filename, *line); + enforceJson(!range.empty, "Missing ']' before EOF.", filename, line); + enforceJson(range.front == ',' || range.front == ']', + format("Expected ']' or ',' - got '%s'.", range.front), filename, line); if( range.front == ']' ) break; else range.popFront(); } @@ -782,27 +924,30 @@ case '{': Json[string] obj; range.popFront(); - while(true) { + while (true) { skipWhitespace(range, line); - enforceJson(!range.empty, "Missing '}' before EOF.", filename, *line); + enforceJson(!range.empty, "Missing '}' before EOF.", filename, line); if(range.front == '}') break; - string key = skipJsonString(range, filename, line); + string key = skipJsonString(range); skipWhitespace(range, line); - enforceJson(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'", filename, *line); + enforceJson(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'", filename, line); range.popFront(); skipWhitespace(range, line); - Json itm = parseJson(range, filename, line); + Json itm = parseJson(range, line, filename); obj[key] = itm; skipWhitespace(range, line); - enforceJson(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'.", filename, *line); - if( range.front == '}' ) break; + enforceJson(!range.empty, "Missing '}' before EOF.", filename, line); + enforceJson(range.front == ',' || range.front == '}', + format("Expected '}' or ',' - got '%s'.", range.front), filename, line); + if (range.front == '}') break; else range.popFront(); } range.popFront(); ret = obj; break; default: - enforceJson(false, "Expected valid json token, got '"~range[0 .. min($, 12)]~"'.", filename, *line); + enforceJson(false, format("Expected valid JSON token, got '%s'.", range[0 .. min(12, $)]), filename, line); + assert(false); } assert(ret.type != Json.Type.undefined); @@ -813,13 +958,14 @@ /** Parses the given JSON string and returns the corresponding Json object. - Throws an Exception if any parsing error occurs. + Throws a JSONException if any parsing error occurs. */ Json parseJsonString(string str, string filename = null) { + auto strcopy = str; int line = 0; - auto ret = parseJson(str, filename, &line); - enforceJson(str.strip().length == 0, "Expected end of string after JSON value, not '"~str.strip()~"'.", filename, line); + auto ret = parseJson(strcopy, &line, filename); + enforceJson(strcopy.strip().length == 0, "Expected end of string after JSON value.", filename, line); return ret; } @@ -833,8 +979,20 @@ assert(parseJsonString("[1, 2, 3]") == Json([Json(1), Json(2), Json(3)])); assert(parseJsonString("{\"a\": 1}") == Json(["a": Json(1)])); assert(parseJsonString(`"\\\/\b\f\n\r\t\u1234"`).get!string == "\\/\b\f\n\r\t\u1234"); + auto json = parseJsonString(`{"hey": "This is @à test éhééhhéhéé !%/??*&?\ud83d\udcec"}`); + assert(json.toPrettyString() == parseJsonString(json.toPrettyString()).toPrettyString()); } +unittest { + try parseJsonString(`{"a": 1`); + catch (Exception e) assert(e.msg.endsWith("Missing '}' before EOF.")); + try parseJsonString(`{"a": 1 x`); + catch (Exception e) assert(e.msg.endsWith("Expected '}' or ',' - got 'x'.")); + try parseJsonString(`[1`); + catch (Exception e) assert(e.msg.endsWith("Missing ']' before EOF.")); + try parseJsonString(`[1 x`); + catch (Exception e) assert(e.msg.endsWith("Expected ']' or ',' - got 'x'.")); +} /** Serializes the given value to JSON. @@ -842,16 +1000,16 @@ The following types of values are supported: $(DL - $(DT Json) $(DD Used as-is) - $(DT null) $(DD Converted to Json.Type.null_) - $(DT bool) $(DD Converted to Json.Type.bool_) - $(DT float, double) $(DD Converted to Json.Type.Double) - $(DT short, ushort, int, uint, long, ulong) $(DD Converted to Json.Type.int_) - $(DT string) $(DD Converted to Json.Type.string) - $(DT T[]) $(DD Converted to Json.Type.array) - $(DT T[string]) $(DD Converted to Json.Type.object) - $(DT struct) $(DD Converted to Json.Type.object) - $(DT class) $(DD Converted to Json.Type.object or Json.Type.null_) + $(DT `Json`) $(DD Used as-is) + $(DT `null`) $(DD Converted to `Json.Type.null_`) + $(DT `bool`) $(DD Converted to `Json.Type.bool_`) + $(DT `float`, `double`) $(DD Converted to `Json.Type.float_`) + $(DT `short`, `ushort`, `int`, `uint`, `long`, `ulong`) $(DD Converted to `Json.Type.int_`) + $(DT `string`) $(DD Converted to `Json.Type.string`) + $(DT `T[]`) $(DD Converted to `Json.Type.array`) + $(DT `T[string]`) $(DD Converted to `Json.Type.object`) + $(DT `struct`) $(DD Converted to `Json.Type.object`) + $(DT `class`) $(DD Converted to `Json.Type.object` or `Json.Type.null_`) ) All entries of an array or an associative array, as well as all R/W properties and @@ -872,55 +1030,150 @@ --- The methods will have to be defined in pairs. The first pair that is implemented by - the type will be used for serialization (i.e. toJson overrides toString). + the type will be used for serialization (i.e. `toJson` overrides `toString`). + + See_Also: `deserializeJson`, `vibe.data.serialization` */ Json serializeToJson(T)(T value) { + version (VibeOldSerialization) { + return serializeToJsonOld(value); + } else { + return serialize!JsonSerializer(value); + } +} +/// ditto +void serializeToJson(R, T)(R destination, T value) + if (isOutputRange!(R, char) || isOutputRange!(R, ubyte)) +{ + serialize!(JsonStringSerializer!R)(value, destination); +} +/// ditto +string serializeToJsonString(T)(T value) +{ + auto ret = appender!string; + serializeToJson(ret, value); + return ret.data; +} + +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f; + f.number = 12; + f.str = "hello"; + + string json = serializeToJsonString(f); + assert(json == `{"number":12,"str":"hello"}`); + + Json jsonval = serializeToJson(f); + assert(jsonval.type == Json.Type.object); + assert(jsonval["number"] == Json(12)); + assert(jsonval["str"] == Json("hello")); +} + + +/** + Serializes the given value to a pretty printed JSON string. + + See_also: `serializeToJson`, `vibe.data.serialization` +*/ +void serializeToPrettyJson(R, T)(R destination, T value) + if (isOutputRange!(R, char) || isOutputRange!(R, ubyte)) +{ + serialize!(JsonStringSerializer!(R, true))(value, destination); +} +/// ditto +string serializeToPrettyJson(T)(T value) +{ + auto ret = appender!string; + serializeToPrettyJson(ret, value); + return ret.data; +} + +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f; + f.number = 12; + f.str = "hello"; + + string json = serializeToPrettyJson(f); + assert(json == +`{ + "number": 12, + "str": "hello" +}`); +} + + +/// private +Json serializeToJsonOld(T)(T value) +{ + import vibe.internal.meta.traits; + alias TU = Unqual!T; - static if( is(TU == Json) ) return value; - else static if( is(TU == typeof(null)) ) return Json(null); - else static if( is(TU == bool) ) return Json(value); - else static if( is(TU == float) ) return Json(cast(double)value); - else static if( is(TU == double) ) return Json(value); - else static if( is(TU == DateTime) ) return Json(value.toISOExtString()); - else static if( is(TU == SysTime) ) return Json(value.toISOExtString()); - else static if( is(TU : long) ) return Json(cast(long)value); - else static if( is(TU == string) ) return Json(value); - else static if( isArray!T ){ + static if (is(TU == Json)) return value; + else static if (is(TU == typeof(null))) return Json(null); + else static if (is(TU == bool)) return Json(value); + else static if (is(TU == float)) return Json(cast(double)value); + else static if (is(TU == double)) return Json(value); + else static if (is(TU == DateTime)) return Json(value.toISOExtString()); + else static if (is(TU == SysTime)) return Json(value.toISOExtString()); + else static if (is(TU == Date)) return Json(value.toISOExtString()); + else static if (is(TU : long)) return Json(cast(long)value); + else static if (is(TU : string)) return Json(value); + else static if (isArray!T) { auto ret = new Json[value.length]; - foreach( i; 0 .. value.length ) + foreach (i; 0 .. value.length) ret[i] = serializeToJson(value[i]); return Json(ret); - } else static if( isAssociativeArray!TU ){ + } else static if (isAssociativeArray!TU) { Json[string] ret; - foreach( string key, value; value ) - ret[key] = serializeToJson(value); + alias TK = KeyType!T; + foreach (key, value; value) { + static if(is(TK == string)) { + ret[key] = serializeToJson(value); + } else static if (is(TK == enum)) { + ret[to!string(key)] = serializeToJson(value); + } else static if (isStringSerializable!(TK)) { + ret[key.toString()] = serializeToJson(value); + } else static assert("AA key type %s not supported for JSON serialization."); + } return Json(ret); - } else static if( isJsonSerializable!TU ){ + } else static if (isJsonSerializable!TU) { return value.toJson(); - } else static if( isStringSerializable!TU ){ + } else static if (isStringSerializable!TU) { return Json(value.toString()); - } else static if( is(TU == struct) ){ + } else static if (is(TU == struct)) { Json[string] ret; - foreach( m; __traits(allMembers, T) ){ - static if( isRWField!(TU, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWField!(TU, m)) { auto mv = __traits(getMember, value, m); ret[underscoreStrip(m)] = serializeToJson(mv); } } return Json(ret); - } else static if( is(TU == class) ){ - if( value is null ) return Json(null); + } else static if(is(TU == class)) { + if (value is null) return Json(null); Json[string] ret; - foreach( m; __traits(allMembers, T) ){ - static if( isRWField!(TU, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWField!(TU, m)) { auto mv = __traits(getMember, value, m); ret[underscoreStrip(m)] = serializeToJson(mv); } } return Json(ret); - } else static if( isPointer!TU ){ - if( value is null ) return Json(null); + } else static if (isPointer!TU) { + if (value is null) return Json(null); return serializeToJson(*value); } else { static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization."); @@ -931,7 +1184,9 @@ /** Deserializes a JSON value into the destination variable. - The same types as for serializeToJson() are supported and handled inversely. + The same types as for `serializeToJson()` are supported and handled inversely. + + See_Also: `serializeToJson`, `serializeToJsonString`, `vibe.data.serialization` */ void deserializeJson(T)(ref T dst, Json src) { @@ -940,52 +1195,82 @@ /// ditto T deserializeJson(T)(Json src) { - static if( is(T == Json) ) return src; - else static if( is(T == typeof(null)) ){ return null; } - else static if( is(T == bool) ) return src.get!bool; - else static if( is(T == float) ) return src.to!float; // since doubles are frequently serialized without - else static if( is(T == double) ) return src.to!double; // a decimal point, we allow conversions here - else static if( is(T == DateTime) ) return DateTime.fromISOExtString(src.get!string); - else static if( is(T == SysTime) ) return SysTime.fromISOExtString(src.get!string); - else static if( is(T : long) ) return cast(T)src.get!long; - else static if( is(T == string) ) return src.get!string; - else static if( isArray!T ){ + version (VibeOldSerialization) { + return deserializeJsonOld!T(src); + } else { + return deserialize!(JsonSerializer, T)(src); + } +} +/// ditto +T deserializeJson(T, R)(R input) + if (isInputRange!R && !is(R == Json)) +{ + return deserialize!(JsonStringSerializer!R, T)(input); +} + +/// private +T deserializeJsonOld(T)(Json src) +{ + import vibe.internal.meta.traits; + + static if( is(T == struct) || isSomeString!T || isIntegral!T || isFloatingPoint!T ) + if( src.type == Json.Type.null_ ) return T.init; + static if (is(T == Json)) return src; + else static if (is(T == typeof(null))) { return null; } + else static if (is(T == bool)) return src.get!bool; + else static if (is(T == float)) return src.to!float; // since doubles are frequently serialized without + else static if (is(T == double)) return src.to!double; // a decimal point, we allow conversions here + else static if (is(T == DateTime)) return DateTime.fromISOExtString(src.get!string); + else static if (is(T == SysTime)) return SysTime.fromISOExtString(src.get!string); + else static if (is(T == Date)) return Date.fromISOExtString(src.get!string); + else static if (is(T : long)) return cast(T)src.get!long; + else static if (is(T : string)) return cast(T)src.get!string; + else static if (isArray!T) { alias TV = typeof(T.init[0]) ; auto dst = new Unqual!TV[src.length]; - foreach( size_t i, v; src ) + foreach (size_t i, v; src) dst[i] = deserializeJson!(Unqual!TV)(v); - return dst; - } else static if( isAssociativeArray!T ){ + return cast(T)dst; + } else static if( isAssociativeArray!T ) { alias TV = typeof(T.init.values[0]) ; - Unqual!TV[string] dst; - foreach( string key, value; src ) - dst[key] = deserializeJson!(Unqual!TV)(value); + alias TK = KeyType!T; + Unqual!TV[TK] dst; + foreach (string key, value; src) { + static if (is(TK == string)) { + dst[key] = deserializeJson!(Unqual!TV)(value); + } else static if (is(TK == enum)) { + dst[to!(TK)(key)] = deserializeJson!(Unqual!TV)(value); + } else static if (isStringSerializable!TK) { + auto dsk = TK.fromString(key); + dst[dsk] = deserializeJson!(Unqual!TV)(value); + } else static assert("AA key type %s not supported for JSON serialization."); + } return dst; - } else static if( isJsonSerializable!T ){ + } else static if (isJsonSerializable!T) { return T.fromJson(src); - } else static if( isStringSerializable!T ){ + } else static if (isStringSerializable!T) { return T.fromString(src.get!string); - } else static if( is(T == struct) ){ + } else static if (is(T == struct)) { T dst; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWPlainField!(T, m) || isRWField!(T, m)) { alias TM = typeof(__traits(getMember, dst, m)) ; __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); } } return dst; - } else static if( is(T == class) ){ - if( src.type == Json.Type.null_ ) return null; + } else static if (is(T == class)) { + if (src.type == Json.Type.null_) return null; auto dst = new T; - foreach( m; __traits(allMembers, T) ){ - static if( isRWPlainField!(T, m) || isRWField!(T, m) ){ + foreach (m; __traits(allMembers, T)) { + static if (isRWPlainField!(T, m) || isRWField!(T, m)) { alias TM = typeof(__traits(getMember, dst, m)) ; __traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]); } } return dst; - } else static if( isPointer!T ){ - if( src.type == Json.Type.null_ ) return null; + } else static if (isPointer!T) { + if (src.type == Json.Type.null_) return null; alias TD = typeof(*T.init) ; dst = new TD; *dst = deserializeJson!TD(src); @@ -995,10 +1280,24 @@ } } +/// +unittest { + struct Foo { + int number; + string str; + } + + Foo f = deserializeJson!Foo(`{"number": 12, "str": "hello"}`); + assert(f.number == 12); + assert(f.str == "hello"); +} + unittest { import std.stdio; - static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; } - immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3]}; + enum Foo : string { k = "test" } + enum Boo : int { l = 5 } + static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; Foo k; Boo l; } + immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3], Foo.k, Boo.l}; S u; deserializeJson(u, serializeToJson(t)); assert(t.a == u.a); @@ -1011,6 +1310,49 @@ assert(t.h == u.h); assert(t.i == u.i); assert(t.j == u.j); + assert(t.k == u.k); + assert(t.l == u.l); +} + +unittest +{ + assert(uint.max == serializeToJson(uint.max).deserializeJson!uint); + assert(ulong.max == serializeToJson(ulong.max).deserializeJson!ulong); +} + +unittest { + static struct A { int value; static A fromJson(Json val) { return A(val.get!int); } Json toJson() const { return Json(value); } } + static struct C { int value; static C fromString(string val) { return C(val.to!int); } string toString() const { return value.to!string; } } + static struct D { int value; } + + assert(serializeToJson(const A(123)) == Json(123)); + assert(serializeToJson(A(123)) == Json(123)); + assert(serializeToJson(const C(123)) == Json("123")); + assert(serializeToJson(C(123)) == Json("123")); + assert(serializeToJson(const D(123)) == serializeToJson(["value": 123])); + assert(serializeToJson(D(123)) == serializeToJson(["value": 123])); +} + +unittest { + auto d = Date(2001,1,1); + deserializeJson(d, serializeToJson(Date.init)); + assert(d == Date.init); + deserializeJson(d, serializeToJson(Date(2001,1,1))); + assert(d == Date(2001,1,1)); + struct S { immutable(int)[] x; } + S s; + deserializeJson(s, serializeToJson(S([1,2,3]))); + assert(s == S([1,2,3])); + struct T { + @optional S s; + @optional int i; + @optional float f_; // underscore strip feature + @optional double d; + @optional string str; + } + auto t = T(S([1,2,3])); + deserializeJson(t, parseJsonString(`{ "s" : null, "i" : null, "f" : null, "d" : null, "str" : null }`)); + assert(text(t) == text(T())); } unittest { @@ -1034,13 +1376,357 @@ assert(c.b == d.b); } +unittest { + static struct C { int value; static C fromString(string val) { return C(val.to!int); } string toString() const { return value.to!string; } } + enum Color { Red, Green, Blue } + { + static class T { + string[Color] enumIndexedMap; + string[C] stringableIndexedMap; + this() { + enumIndexedMap = [ Color.Red : "magenta", Color.Blue : "deep blue" ]; + stringableIndexedMap = [ C(42) : "forty-two" ]; + } + } + + T original = new T; + original.enumIndexedMap[Color.Green] = "olive"; + T other; + deserializeJson(other, serializeToJson(original)); + assert(serializeToJson(other) == serializeToJson(original)); + } + { + static struct S { + string[Color] enumIndexedMap; + string[C] stringableIndexedMap; + } + + S *original = new S; + original.enumIndexedMap = [ Color.Red : "magenta", Color.Blue : "deep blue" ]; + original.enumIndexedMap[Color.Green] = "olive"; + original.stringableIndexedMap = [ C(42) : "forty-two" ]; + S other; + deserializeJson(other, serializeToJson(original)); + assert(serializeToJson(other) == serializeToJson(original)); + } +} + +unittest { + import std.typecons : Nullable; + + struct S { Nullable!int a, b; } + S s; + s.a = 2; + + auto j = serializeToJson(s); + assert(j.a.type == Json.Type.int_); + assert(j.b.type == Json.Type.null_); + + auto t = deserializeJson!S(j); + assert(!t.a.isNull() && t.a == 2); + assert(t.b.isNull()); +} + +unittest { // #840 + int[2][2] nestedArray = 1; + assert(nestedArray.serializeToJson.deserializeJson!(typeof(nestedArray)) == nestedArray); +} + + +/** + Serializer for a plain Json representation. + + See_Also: vibe.data.serialization.serialize, vibe.data.serialization.deserialize, serializeToJson, deserializeJson +*/ +struct JsonSerializer { + template isJsonBasicType(T) { enum isJsonBasicType = isNumeric!T || isBoolean!T || is(T == string) || is(T == typeof(null)) || isJsonSerializable!T; } + + template isSupportedValueType(T) { enum isSupportedValueType = isJsonBasicType!T || is(T == Json); } + + private { + Json m_current; + Json[] m_compositeStack; + } + + this(Json data) { m_current = data; } + + @disable this(this); + + // + // serialization + // + Json getSerializedResult() { return m_current; } + void beginWriteDictionary(T)() { m_compositeStack ~= Json.emptyObject; } + void endWriteDictionary(T)() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; } + void beginWriteDictionaryEntry(T)(string name) {} + void endWriteDictionaryEntry(T)(string name) { m_compositeStack[$-1][name] = m_current; } + + void beginWriteArray(T)(size_t) { m_compositeStack ~= Json.emptyArray; } + void endWriteArray(T)() { m_current = m_compositeStack[$-1]; m_compositeStack.length--; } + void beginWriteArrayEntry(T)(size_t) {} + void endWriteArrayEntry(T)(size_t) { m_compositeStack[$-1].appendArrayElement(m_current); } + + void writeValue(T)(T value) + { + static if (is(T == Json)) m_current = value; + else static if (isJsonSerializable!T) m_current = value.toJson(); + else m_current = Json(value); + } + + void writeValue(T)(in Json value) if (is(T == Json)) + { + m_current = value.clone; + } + + // + // deserialization + // + void readDictionary(T)(scope void delegate(string) field_handler) + { + enforceJson(m_current.type == Json.Type.object, "Expected JSON object, got "~m_current.type.to!string); + auto old = m_current; + foreach (string key, value; m_current) { + m_current = value; + field_handler(key); + } + m_current = old; + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + enforceJson(m_current.type == Json.Type.array, "Expected JSON array, got "~m_current.type.to!string); + auto old = m_current; + size_callback(m_current.length); + foreach (ent; old) { + m_current = ent; + entry_callback(); + } + m_current = old; + } + + T readValue(T)() + { + static if (is(T == Json)) return m_current; + else static if (isJsonSerializable!T) return T.fromJson(m_current); + else static if (is(T == float) || is(T == double)) { + if (m_current.type == Json.Type.undefined) return T.nan; + return m_current.type == Json.Type.float_ ? cast(T)m_current.get!double : cast(T)m_current.get!long; + } + else { + return m_current.get!T(); + } + } + + bool tryReadNull() { return m_current.type == Json.Type.null_; } +} + + +/** + Serializer for a range based plain JSON string representation. + + See_Also: vibe.data.serialization.serialize, vibe.data.serialization.deserialize, serializeToJson, deserializeJson +*/ +struct JsonStringSerializer(R, bool pretty = false) + if (isInputRange!R || isOutputRange!(R, char)) +{ + private { + R m_range; + size_t m_level = 0; + } + + template isJsonBasicType(T) { enum isJsonBasicType = isNumeric!T || isBoolean!T || is(T == string) || is(T == typeof(null)) || isJsonSerializable!T; } + + template isSupportedValueType(T) { enum isSupportedValueType = isJsonBasicType!T || is(T == Json); } + + this(R range) + { + m_range = range; + } + + @disable this(this); + + // + // serialization + // + static if (isOutputRange!(R, char)) { + private { + bool m_firstInComposite; + } + + void getSerializedResult() {} + + void beginWriteDictionary(T)() { startComposite(); m_range.put('{'); } + void endWriteDictionary(T)() { endComposite(); m_range.put("}"); } + void beginWriteDictionaryEntry(T)(string name) + { + startCompositeEntry(); + m_range.put('"'); + m_range.jsonEscape(name); + static if (pretty) m_range.put(`": `); + else m_range.put(`":`); + } + void endWriteDictionaryEntry(T)(string name) {} + + void beginWriteArray(T)(size_t) { startComposite(); m_range.put('['); } + void endWriteArray(T)() { endComposite(); m_range.put(']'); } + void beginWriteArrayEntry(T)(size_t) { startCompositeEntry(); } + void endWriteArrayEntry(T)(size_t) {} + + void writeValue(T)(in T value) + { + static if (is(T == typeof(null))) m_range.put("null"); + else static if (is(T == bool)) m_range.put(value ? "true" : "false"); + else static if (is(T : long)) m_range.formattedWrite("%s", value); + else static if (is(T : real)) m_range.formattedWrite("%.16g", value); + else static if (is(T == string)) { + m_range.put('"'); + m_range.jsonEscape(value); + m_range.put('"'); + } + else static if (is(T == Json)) m_range.writeJsonString(value); + else static if (isJsonSerializable!T) m_range.writeJsonString!(R, pretty)(value.toJson(), m_level); + else static assert(false, "Unsupported type: " ~ T.stringof); + } + + private void startComposite() + { + static if (pretty) m_level++; + m_firstInComposite = true; + } + + private void startCompositeEntry() + { + if (!m_firstInComposite) { + m_range.put(','); + } else { + m_firstInComposite = false; + } + static if (pretty) indent(); + } + + private void endComposite() + { + static if (pretty) { + m_level--; + if (!m_firstInComposite) indent(); + } + m_firstInComposite = false; + } + + private void indent() + { + m_range.put('\n'); + foreach (i; 0 .. m_level) m_range.put('\t'); + } + } + + // + // deserialization + // + static if (isInputRange!(R)) { + private { + int m_line = 0; + } + + void readDictionary(T)(scope void delegate(string) entry_callback) + { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == '{', "Expecting object."); + m_range.popFront(); + bool first = true; + while(true) { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty, "Missing '}'."); + if (m_range.front == '}') { + m_range.popFront(); + break; + } else if (!first) { + enforceJson(m_range.front == ',', "Expecting ',' or '}', not '"~m_range.front.to!string~"'."); + m_range.popFront(); + m_range.skipWhitespace(&m_line); + } else first = false; + + auto name = m_range.skipJsonString(&m_line); + + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == ':', "Expecting ':', not '"~m_range.front.to!string~"'."); + m_range.popFront(); + + entry_callback(name); + } + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty && m_range.front == '[', "Expecting array."); + m_range.popFront(); + bool first = true; + while(true) { + m_range.skipWhitespace(&m_line); + enforceJson(!m_range.empty, "Missing ']'."); + if (m_range.front == ']') { + m_range.popFront(); + break; + } else if (!first) { + enforceJson(m_range.front == ',', "Expecting ',' or ']'."); + m_range.popFront(); + } else first = false; + + entry_callback(); + } + } + + T readValue(T)() + { + m_range.skipWhitespace(&m_line); + static if (is(T == typeof(null))) { enforceJson(m_range.take(4).equal("null"), "Expecting 'null'."); return null; } + else static if (is(T == bool)) { + bool ret = m_range.front == 't'; + string expected = ret ? "true" : "false"; + foreach (ch; expected) { + enforceJson(m_range.front == ch, "Expecting 'true' or 'false'."); + m_range.popFront(); + } + return ret; + } else static if (is(T : long)) { + bool is_float; + auto num = m_range.skipNumber(is_float); + enforceJson(!is_float, "Expecting integer number."); + return to!T(num); + } else static if (is(T : real)) { + bool is_float; + auto num = m_range.skipNumber(is_float); + return to!T(num); + } + else static if (is(T == string)) return m_range.skipJsonString(&m_line); + else static if (is(T == Json)) return m_range.parseJson(&m_line); + else static if (isJsonSerializable!T) return T.fromJson(m_range.parseJson(&m_line)); + else static assert(false, "Unsupported type: " ~ T.stringof); + } + + bool tryReadNull() + { + m_range.skipWhitespace(&m_line); + if (m_range.front != 'n') return false; + foreach (ch; "null") { + enforceJson(m_range.front == ch, "Expecting 'null'."); + m_range.popFront(); + } + assert(m_range.empty || m_range.front != 'l'); + return true; + } + } +} + + /** Writes the given JSON object as a JSON string into the destination range. This function will convert the given JSON value to a string without adding any white space between tokens (no newlines, no indentation and no padding). - The output size is thus minizized, at the cost of bad human readability. + The output size is thus minimized, at the cost of bad human readability. Params: dst = References the string output range to which the result is written. @@ -1048,7 +1734,7 @@ See_Also: Json.toString, writePrettyJsonString */ -void writeJsonString(R)(ref R dst, in Json json) +void writeJsonString(R, bool pretty = false)(ref R dst, in Json json, size_t level = 0) // if( isOutputRange!R && is(ElementEncodingType!R == char) ) { final switch( json.type ){ @@ -1056,40 +1742,125 @@ case Json.Type.null_: dst.put("null"); break; case Json.Type.bool_: dst.put(cast(bool)json ? "true" : "false"); break; case Json.Type.int_: formattedWrite(dst, "%d", json.get!long); break; - case Json.Type.float_: formattedWrite(dst, "%.16g", json.get!double); break; + case Json.Type.float_: + auto d = json.get!double; + if (d != d) + dst.put("undefined"); // JSON has no NaN value so set null + else + formattedWrite(dst, "%.16g", json.get!double); + break; case Json.Type.string: - dst.put("\""); + dst.put('\"'); jsonEscape(dst, cast(string)json); - dst.put("\""); + dst.put('\"'); break; case Json.Type.array: - dst.put("["); + dst.put('['); bool first = true; - foreach( ref const Json e; json ){ - if( e.type == Json.Type.undefined ) continue; + foreach (ref const Json e; json) { if( !first ) dst.put(","); first = false; - writeJsonString(dst, e); + static if (pretty) { + dst.put('\n'); + foreach (tab; 0 .. level+1) dst.put('\t'); + } + if (e.type == Json.Type.undefined) dst.put("null"); + else writeJsonString!(R, pretty)(dst, e, level+1); } - dst.put("]"); + static if (pretty) { + if (json.length > 0) { + dst.put('\n'); + foreach (tab; 0 .. level) dst.put('\t'); + } + } + dst.put(']'); break; case Json.Type.object: - dst.put("{"); + dst.put('{'); bool first = true; foreach( string k, ref const Json e; json ){ if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); + if( !first ) dst.put(','); first = false; - dst.put("\""); + static if (pretty) { + dst.put('\n'); + foreach (tab; 0 .. level+1) dst.put('\t'); + } + dst.put('\"'); jsonEscape(dst, k); - dst.put("\":"); - writeJsonString(dst, e); + dst.put(pretty ? `": ` : `":`); + writeJsonString!(R, pretty)(dst, e, level+1); } - dst.put("}"); + static if (pretty) { + if (json.length > 0) { + dst.put('\n'); + foreach (tab; 0 .. level) dst.put('\t'); + } + } + dst.put('}'); break; } } +unittest { + auto a = Json.emptyObject; + a.a = Json.emptyArray; + a.b = Json.emptyArray; + a.b ~= Json(1); + a.b ~= Json.emptyObject; + + assert(a.toString() == `{"a":[],"b":[1,{}]}`); + assert(a.toPrettyString() == +`{ + "a": [], + "b": [ + 1, + {} + ] +}`); +} + +unittest { // #735 + auto a = Json.emptyArray; + a ~= "a"; + a ~= Json(); + a ~= "b"; + a ~= null; + a ~= "c"; + assert(a.toString() == `["a",null,"b",null,"c"]`); +} + +unittest { + auto a = Json.emptyArray; + a ~= Json(1); + a ~= Json(2); + a ~= Json(3); + a ~= Json(4); + a ~= Json(5); + + auto b = Json(a[0..a.length]); + assert(a == b); + + auto c = Json(a[0..$]); + assert(a == c); + assert(b == c); + + auto d = [Json(1),Json(2),Json(3)]; + assert(d == a[0..a.length-2]); + assert(d == a[0..$-2]); +} + +unittest { + auto j = Json(double.init); + + assert(j.toString == "undefined"); // A double nan should serialize to undefined + j = 17.04f; + assert(j.toString == "17.04"); // A proper double should serialize correctly + + double d; + deserializeJson(d, Json.undefined); // Json.undefined should deserialize to nan + assert(d != d); +} /** Writes the given JSON object as a prettified JSON string into the destination range. @@ -1098,71 +1869,74 @@ Params: dst = References the string output range to which the result is written. json = Specifies the JSON value that is to be stringified. - level = Specifies the base amount of indentation for the output. Indentation is always - done using tab characters. + level = Specifies the base amount of indentation for the output. Indentation is always + done using tab characters. See_Also: Json.toPrettyString, writeJsonString */ void writePrettyJsonString(R)(ref R dst, in Json json, int level = 0) // if( isOutputRange!R && is(ElementEncodingType!R == char) ) { - final switch( json.type ){ - case Json.Type.undefined: dst.put("undefined"); break; - case Json.Type.null_: dst.put("null"); break; - case Json.Type.bool_: dst.put(cast(bool)json ? "true" : "false"); break; - case Json.Type.int_: formattedWrite(dst, "%d", json.get!long); break; - case Json.Type.float_: formattedWrite(dst, "%.16g", json.get!double); break; - case Json.Type.string: - dst.put("\""); - jsonEscape(dst, cast(string)json); - dst.put("\""); - break; - case Json.Type.array: - dst.put("["); - bool first = true; - foreach( e; json ){ - if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); - first = false; - dst.put("\n"); - foreach( tab; 0 .. level+1 ) dst.put('\t'); - writePrettyJsonString(dst, e, level+1); - } - if( json.length > 0 ) { - dst.put('\n'); - foreach( tab; 0 .. level ) dst.put('\t'); - } - dst.put("]"); - break; - case Json.Type.object: - dst.put("{"); - bool first = true; - foreach( string k, e; json ){ - if( e.type == Json.Type.undefined ) continue; - if( !first ) dst.put(","); - dst.put("\n"); - first = false; - foreach( tab; 0 .. level+1 ) dst.put('\t'); - dst.put("\""); - jsonEscape(dst, k); - dst.put("\": "); - writePrettyJsonString(dst, e, level+1); - } - if( json.length > 0 ) { - dst.put('\n'); - foreach( tab; 0 .. level ) dst.put('\t'); - } - dst.put("}"); - break; - } + writeJsonString!(R, true)(dst, json, level); } -/// private -private void jsonEscape(R)(ref R dst, string s) + +/** + Helper function that escapes all Unicode characters in a JSON string. +*/ +string convertJsonToASCII(string json) { - foreach( ch; s ){ - switch(ch){ - default: dst.put(ch); break; + auto ret = appender!string; + jsonEscape!true(ret, json); + return ret.data; +} + + +/// private +private void jsonEscape(bool escape_unicode = false, R)(ref R dst, string s) +{ + for (size_t pos = 0; pos < s.length; pos++) { + immutable(char) ch = s[pos]; + + switch (ch) { + default: + static if (escape_unicode) { + if (ch > 0x20 && ch < 0x80) dst.put(ch); + else { + import std.utf : decode; + char[13] buf; + int len; + dchar codepoint = decode(s, pos); + import std.c.stdio : sprintf; + /* codepoint is in BMP */ + if(codepoint < 0x10000) + { + sprintf(&buf[0], "\\u%04X", codepoint); + len = 6; + } + /* not in BMP -> construct a UTF-16 surrogate pair */ + else + { + int first, last; + + codepoint -= 0x10000; + first = 0xD800 | ((codepoint & 0xffc00) >> 10); + last = 0xDC00 | (codepoint & 0x003ff); + + sprintf(&buf[0], "\\u%04X\\u%04X", first, last); + len = 12; + } + + pos -= 1; + foreach (i; 0 .. len) + dst.put(buf[i]); + + } + } else { + if (ch < 0x20) dst.formattedWrite("\\u%04X", ch); + else dst.put(ch); + } + break; case '\\': dst.put("\\\\"); break; case '\r': dst.put("\\r"); break; case '\n': dst.put("\\n"); break; @@ -1182,9 +1956,9 @@ case '"': return ret.data; case '\\': range.popFront(); - enforce(!range.empty, "Unterminated string escape sequence."); + enforceJson(!range.empty, "Unterminated string escape sequence."); switch(range.front){ - default: enforce("Invalid string escape sequence."); break; + default: enforceJson(false, "Invalid string escape sequence."); break; case '"': ret.put('\"'); range.popFront(); break; case '\\': ret.put('\\'); range.popFront(); break; case '/': ret.put('/'); range.popFront(); break; @@ -1194,17 +1968,39 @@ case 'r': ret.put('\r'); range.popFront(); break; case 't': ret.put('\t'); range.popFront(); break; case 'u': - range.popFront(); - dchar uch = 0; - foreach( i; 0 .. 4 ){ - uch *= 16; - enforce(!range.empty, "Unicode sequence must be '\\uXXXX'."); - auto dc = range.front; + + dchar decode_unicode_escape() { + enforceJson(range.front == 'u'); range.popFront(); - if( dc >= '0' && dc <= '9' ) uch += dc - '0'; - else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10; - else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10; - else enforce(false, "Unicode sequence must be '\\uXXXX'."); + dchar uch = 0; + foreach( i; 0 .. 4 ){ + uch *= 16; + enforceJson(!range.empty, "Unicode sequence must be '\\uXXXX'."); + auto dc = range.front; + range.popFront(); + + if( dc >= '0' && dc <= '9' ) uch += dc - '0'; + else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10; + else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10; + else enforceJson(false, "Unicode sequence must be '\\uXXXX'."); + } + return uch; + } + + auto uch = decode_unicode_escape(); + + if(0xD800 <= uch && uch <= 0xDBFF) { + /* surrogate pair */ + range.popFront(); // backslash '\' + auto uch2 = decode_unicode_escape(); + enforceJson(0xDC00 <= uch2 && uch2 <= 0xDFFF, "invalid Unicode"); + { + /* valid second surrogate */ + uch = + ((uch - 0xD800) << 10) + + (uch2 - 0xDC00) + + 0x10000; + } } ret.put(uch); break; @@ -1219,14 +2015,16 @@ return ret.data; } -private string skipNumber(ref string s, out bool is_float, string filename, int line) +/// private +private string skipNumber(R)(ref R s, out bool is_float) { + // TODO: make this work with input ranges size_t idx = 0; is_float = false; - if( s[idx] == '-' ) idx++; - if( s[idx] == '0' ) idx++; + if (s[idx] == '-') idx++; + if (s[idx] == '0') idx++; else { - enforceJson(isDigit(s[idx++]), "Digit expected at beginning of number.", filename, line); + enforceJson(isDigit(s[idx++]), "Digit expected at beginning of number."); while( idx < s.length && isDigit(s[idx]) ) idx++; } @@ -1240,7 +2038,7 @@ idx++; is_float = true; if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++; - enforceJson(idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx], filename, line); + enforceJson( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]); idx++; while( idx < s.length && isDigit(s[idx]) ) idx++; } @@ -1250,39 +2048,40 @@ return ret; } -private string skipJsonString(ref string s, string filename, int* line = null) +/// private +private string skipJsonString(R)(ref R s, int* line = null) { - enforceJson(s.length >= 2, "Too small for a string: '" ~ s ~ "'", filename, *line); - enforceJson(s[0] == '\"', "Expected string, not '" ~ s ~ "'", filename, *line); - s = s[1 .. $]; + // TODO: count or disallow any newlines inside of the string + enforceJson(!s.empty && s.front == '"', "Expected '\"' to start string."); + s.popFront(); string ret = jsonUnescape(s); - enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal.", filename, *line); - s = s[1 .. $]; + enforceJson(!s.empty && s.front == '"', "Expected '\"' to terminate string."); + s.popFront(); return ret; } -private void skipWhitespace(ref string s, int* line = null) +/// private +private void skipWhitespace(R)(ref R s, int* line = null) { - while( s.length > 0 ){ - switch( s[0] ){ + while (!s.empty) { + switch (s.front) { default: return; - case ' ', '\t': s = s[1 .. $]; break; + case ' ', '\t': s.popFront(); break; case '\n': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $]; - if( line ) (*line)++; + s.popFront(); + if (!s.empty && s.front == '\r') s.popFront(); + if (line) (*line)++; break; case '\r': - s = s[1 .. $]; - if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $]; - if( line ) (*line)++; + s.popFront(); + if (!s.empty && s.front == '\n') s.popFront(); + if (line) (*line)++; break; } } } -/// private -private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; } +private bool isDigit(dchar ch) { return ch >= '0' && ch <= '9'; } private string underscoreStrip(string field_name) { @@ -1290,13 +2089,23 @@ else return field_name[0 .. $-1]; } -private template isJsonSerializable(T) { enum isJsonSerializable = is(typeof(T.init.toJson()) == Json) && is(typeof(T.fromJson(Json())) == T); } -package template isStringSerializable(T) { enum isStringSerializable = is(typeof(T.init.toString()) == string) && is(typeof(T.fromString("")) == T); } +/// private +package template isJsonSerializable(T) { enum isJsonSerializable = is(typeof(T.init.toJson()) == Json) && is(typeof(T.fromJson(Json())) == T); } -private void enforceJson(string filename = __FILE__, int line = __LINE__)(bool cond, lazy string message, string err_file, int err_line) +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message = "JSON exception") { - if (!cond) { - auto err_msg = format("%s(%s): Error: %s", err_file, err_line, message); - throw new Exception(err_msg, filename, line); - } + static if (__VERSION__ >= 2065) enforceEx!JSONException(cond, message, file, line); + else if (!cond) throw new JSONException(message); +} + +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message, string err_file, int err_line) +{ + auto errmsg = format("%s(%s): Error: %s", err_file, err_line+1, message); + static if (__VERSION__ >= 2065) enforceEx!JSONException(cond, errmsg, file, line); + else if (!cond) throw new JSONException(errmsg); +} + +private void enforceJson(string file = __FILE__, size_t line = __LINE__)(bool cond, lazy string message, string err_file, int* err_line) +{ + enforceJson!(file, line)(cond, message, err_file, err_line ? *err_line : -1); } diff --git a/source/dub/internal/vibecompat/data/serialization.d b/source/dub/internal/vibecompat/data/serialization.d new file mode 100644 index 0000000..a0ada84 --- /dev/null +++ b/source/dub/internal/vibecompat/data/serialization.d @@ -0,0 +1,1304 @@ +/** + Generic serialization framework. + + This module provides general means for implementing (de-)serialization with + a standardized behavior. + + Supported_types: + The following rules are applied in order when serializing or + deserializing a certain type: + + $(OL + $(LI An `enum` type is serialized as its raw value, except if + `@byName` is used, in which case the name of the enum value + is serialized.) + $(LI Any type that is specifically supported by the serializer + is directly serialized. For example, the BSON serializer + supports `BsonObjectID` directly.) + $(LI Arrays and tuples (`std.typecons.Tuple`) are serialized + using the array serialization functions where each element is + serialized again according to these rules.) + $(LI Associative arrays are serialized similar to arrays. The key + type of the AA must satisfy the `isStringSerializable` trait + and will always be serialized as a string.) + $(LI Any `Nullable!T` will be serialized as either `null`, or + as the contained value (subject to these rules again).) + $(LI Any `BitFlags!T` value will be serialized as `T[]`) + $(LI Types satisfying the `isPolicySerializable` trait for the + supplied `Policy` will be serialized as the value returned + by the policy `toRepresentation` function (again subject to + these rules).) + $(LI Types satisfying the `isCustomSerializable` trait will be + serialized as the value returned by their `toRepresentation` + method (again subject to these rules).) + $(LI Types satisfying the `isISOExtStringSerializable` trait will be + serialized as a string, as returned by their `toISOExtString` + method. This causes types such as `SysTime` to be serialized + as strings.) + $(LI Types satisfying the `isStringSerializable` trait will be + serialized as a string, as returned by their `toString` + method.) + $(LI Struct and class types by default will be serialized as + associative arrays, where the key is the name of the + corresponding field (can be overridden using the `@name` + attribute). If the struct/class is annotated with `@asArray`, + it will instead be serialized as a flat array of values in the + order of declaration. Null class references will be serialized + as `null`.) + $(LI Pointer types will be serialized as either `null`, or as + the value they point to.) + $(LI Built-in integers and floating point values, as well as + boolean values will be converted to strings, if the serializer + doesn't support them directly.) + ) + + Note that no aliasing detection is performed, so that pointers, class + references and arrays referencing the same memory will be serialized + as multiple copies. When in turn deserializing the data, they will also + end up as separate copies in memory. + + Serializer_implementation: + Serializers are implemented in terms of a struct with template methods that + get called by the serialization framework: + + --- + struct ExampleSerializer { + enum isSupportedValueType(T) = is(T == string) || is(T == typeof(null)); + + // serialization + auto getSerializedResult(); + void beginWriteDictionary(T)(); + void endWriteDictionary(T)(); + void beginWriteDictionaryEntry(T)(string name); + void endWriteDictionaryEntry(T)(string name); + void beginWriteArray(T)(size_t length); + void endWriteArray(T)(); + void beginWriteArrayEntry(T)(size_t index); + void endWriteArrayEntry(T)(size_t index); + void writeValue(T)(T value); + + // deserialization + void readDictionary(T)(scope void delegate(string) entry_callback); + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback); + T readValue(T)(); + bool tryReadNull(); + } + --- + + Copyright: © 2013-2014 rejectedsoftware e.K. + License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. + Authors: Sönke Ludwig +*/ +module dub.internal.vibecompat.data.serialization; + +version (Have_vibe_d) public import vibe.data.serialization; +else: + +import dub.internal.vibecompat.data.utils; + +import std.array : Appender, appender; +import std.conv : to; +import std.exception : enforce; +import std.traits; +import std.typetuple; + + +/** + Serializes a value with the given serializer. + + The serializer must have a value result for the first form + to work. Otherwise, use the range based form. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +auto serialize(Serializer, T, ARGS...)(T value, ARGS args) +{ + auto serializer = Serializer(args); + serialize(serializer, value); + return serializer.getSerializedResult(); +} +/// ditto +void serialize(Serializer, T)(ref Serializer serializer, T value) +{ + serializeImpl!(Serializer, DefaultPolicy, T)(serializer, value); +} + +/** Note that there is a convenience function `vibe.data.json.serializeToJson` + that can be used instead of manually invoking `serialize`. +*/ +unittest { + import dub.internal.vibecompat.data.json; + + struct Test { + int value; + string text; + } + + Test test; + test.value = 12; + test.text = "Hello"; + + Json serialized = serialize!JsonSerializer(test); + assert(serialized.value.get!int == 12); + assert(serialized.text.get!string == "Hello"); +} + +unittest { + import dub.internal.vibecompat.data.json; + + // Make sure that immutable(char[]) works just like string + // (i.e., immutable(char)[]). + immutable key = "answer"; + auto ints = [key: 42]; + auto serialized = serialize!JsonSerializer(ints); + assert(serialized[key].get!int == 42); +} + +/** + Serializes a value with the given serializer, representing values according to `Policy` when possible. + + The serializer must have a value result for the first form + to work. Otherwise, use the range based form. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +auto serializeWithPolicy(Serializer, alias Policy, T, ARGS...)(T value, ARGS args) +{ + auto serializer = Serializer(args); + serializeWithPolicy!(Serializer, Policy)(serializer, value); + return serializer.getSerializedResult(); +} +/// ditto +void serializeWithPolicy(Serializer, alias Policy, T)(ref Serializer serializer, T value) +{ + serializeImpl!(Serializer, Policy, T)(serializer, value); +} +/// +version (unittest) +{ + template SizePol(T) + { + import std.conv; + import std.array; + + string toRepresentation(T value) { + return to!string(value.x) ~ "x" ~ to!string(value.y); + } + + T fromRepresentation(string value) { + string[] fields = value.split('x'); + alias fieldT = typeof(T.x); + auto x = to!fieldT(fields[0]); + auto y = to!fieldT(fields[1]); + return T(x, y); + } + } +} + +/// +unittest { + import dub.internal.vibecompat.data.json; + + static struct SizeI { + int x; + int y; + } + SizeI sizeI = SizeI(1,2); + Json serializedI = serializeWithPolicy!(JsonSerializer, SizePol)(sizeI); + assert(serializedI.get!string == "1x2"); + + static struct SizeF { + float x; + float y; + } + SizeF sizeF = SizeF(0.1f,0.2f); + Json serializedF = serializeWithPolicy!(JsonSerializer, SizePol)(sizeF); + assert(serializedF.get!string == "0.1x0.2"); +} + + +/** + Deserializes and returns a serialized value. + + serialized_data can be either an input range or a value containing + the serialized data, depending on the type of serializer used. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +T deserialize(Serializer, T, ARGS...)(ARGS args) +{ + auto deserializer = Serializer(args); + return deserializeImpl!(T, DefaultPolicy, Serializer)(deserializer); +} + +/** Note that there is a convenience function `vibe.data.json.deserializeJson` + that can be used instead of manually invoking `deserialize`. +*/ +unittest { + import dub.internal.vibecompat.data.json; + + struct Test { + int value; + string text; + } + + Json serialized = Json.emptyObject; + serialized.value = 12; + serialized.text = "Hello"; + + Test test = deserialize!(JsonSerializer, Test)(serialized); + assert(test.value == 12); + assert(test.text == "Hello"); +} + +/** + Deserializes and returns a serialized value, interpreting values according to `Policy` when possible. + + serialized_data can be either an input range or a value containing + the serialized data, depending on the type of serializer used. + + See_Also: `vibe.data.json.JsonSerializer`, `vibe.data.json.JsonStringSerializer`, `vibe.data.bson.BsonSerializer` +*/ +T deserializeWithPolicy(Serializer, alias Policy, T, ARGS...)(ARGS args) +{ + auto deserializer = Serializer(args); + return deserializeImpl!(T, Policy, Serializer)(deserializer); +} + +/// +unittest { + import dub.internal.vibecompat.data.json; + + static struct SizeI { + int x; + int y; + } + + Json serializedI = "1x2"; + SizeI sizeI = deserializeWithPolicy!(JsonSerializer, SizePol, SizeI)(serializedI); + assert(sizeI.x == 1); + assert(sizeI.y == 2); + + static struct SizeF { + float x; + float y; + } + Json serializedF = "0.1x0.2"; + SizeF sizeF = deserializeWithPolicy!(JsonSerializer, SizePol, SizeF)(serializedF); + assert(sizeF.x == 0.1f); + assert(sizeF.y == 0.2f); +} + +private void serializeImpl(Serializer, alias Policy, T, ATTRIBUTES...)(ref Serializer serializer, T value) +{ + import std.typecons : BitFlags, Nullable, Tuple, tuple; + + static assert(Serializer.isSupportedValueType!string, "All serializers must support string values."); + static assert(Serializer.isSupportedValueType!(typeof(null)), "All serializers must support null values."); + + alias TU = Unqual!T; + + static if (is(TU == enum)) { + static if (hasAttributeL!(ByNameAttribute, ATTRIBUTES)) { + serializeImpl!(Serializer, Policy, string)(serializer, value.to!string()); + } else { + serializeImpl!(Serializer, Policy, OriginalType!TU)(serializer, cast(OriginalType!TU)value); + } + } else static if (Serializer.isSupportedValueType!TU) { + static if (is(TU == typeof(null))) serializer.writeValue!TU(null); + else serializer.writeValue!TU(value); + } else static if (/*isInstanceOf!(Tuple, TU)*/is(T == Tuple!TPS, TPS...)) { + static if (TU.Types.length == 1) { + serializeImpl!(Serializer, Policy, typeof(value[0]), ATTRIBUTES)(serializer, value[0]); + } else { + serializer.beginWriteArray!TU(value.length); + foreach (i, TV; T.Types) { + serializer.beginWriteArrayEntry!TV(i); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, value[i]); + serializer.endWriteArrayEntry!TV(i); + } + serializer.endWriteArray!TU(); + } + } else static if (isArray!TU) { + alias TV = typeof(value[0]); + serializer.beginWriteArray!TU(value.length); + foreach (i, ref el; value) { + serializer.beginWriteArrayEntry!TV(i); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, el); + serializer.endWriteArrayEntry!TV(i); + } + serializer.endWriteArray!TU(); + } else static if (isAssociativeArray!TU) { + alias TK = KeyType!TU; + alias TV = ValueType!TU; + static if (__traits(compiles, serializer.beginWriteDictionary!TU(0))) { + auto nfields = value.length; + serializer.beginWriteDictionary!TU(nfields); + } else { + serializer.beginWriteDictionary!TU(); + } + foreach (key, ref el; value) { + string keyname; + static if (is(TK : string)) keyname = key; + else static if (is(TK : real) || is(TK : long) || is(TK == enum)) keyname = key.to!string; + else static if (isStringSerializable!TK) keyname = key.toString(); + else static assert(false, "Associative array keys must be strings, numbers, enums, or have toString/fromString methods."); + serializer.beginWriteDictionaryEntry!TV(keyname); + serializeImpl!(Serializer, Policy, TV, ATTRIBUTES)(serializer, el); + serializer.endWriteDictionaryEntry!TV(keyname); + } + static if (__traits(compiles, serializer.endWriteDictionary!TU(0))) { + serializer.endWriteDictionary!TU(nfields); + } else { + serializer.endWriteDictionary!TU(); + } + } else static if (/*isInstanceOf!(Nullable, TU)*/is(T == Nullable!TPS, TPS...)) { + if (value.isNull()) serializeImpl!(Serializer, Policy, typeof(null))(serializer, null); + else serializeImpl!(Serializer, Policy, typeof(value.get()), ATTRIBUTES)(serializer, value.get()); + } else static if (is(T == BitFlags!E, E)) { + size_t cnt = 0; + foreach (v; EnumMembers!E) + if (value & v) + cnt++; + + serializer.beginWriteArray!(E[])(cnt); + cnt = 0; + foreach (v; EnumMembers!E) + if (value & v) { + serializer.beginWriteArrayEntry!E(cnt); + serializeImpl!(Serializer, Policy, E, ATTRIBUTES)(serializer, v); + serializer.endWriteArrayEntry!E(cnt); + cnt++; + } + serializer.endWriteArray!(E[])(); + } else static if (isPolicySerializable!(Policy, TU)) { + alias CustomType = typeof(Policy!TU.toRepresentation(TU.init)); + serializeImpl!(Serializer, Policy, CustomType, ATTRIBUTES)(serializer, Policy!TU.toRepresentation(value)); + } else static if (isCustomSerializable!TU) { + alias CustomType = typeof(T.init.toRepresentation()); + serializeImpl!(Serializer, Policy, CustomType, ATTRIBUTES)(serializer, value.toRepresentation()); + } else static if (isISOExtStringSerializable!TU) { + serializer.writeValue(value.toISOExtString()); + } else static if (isStringSerializable!TU) { + serializer.writeValue(value.toString()); + } else static if (is(TU == struct) || is(TU == class)) { + static if (!hasSerializableFields!TU) + pragma(msg, "Serializing composite type "~T.stringof~" which has no serializable fields"); + static if (is(TU == class)) { + if (value is null) { + serializeImpl!(Serializer, Policy, typeof(null))(serializer, null); + return; + } + } + static if (hasAttributeL!(AsArrayAttribute, ATTRIBUTES)) { + enum nfields = getExpandedFieldCount!(TU, SerializableFields!TU); + serializer.beginWriteArray!TU(nfields); + foreach (mname; SerializableFields!TU) { + alias TMS = TypeTuple!(typeof(__traits(getMember, value, mname))); + foreach (j, TM; TMS) { + alias TA = TypeTuple!(__traits(getAttributes, TypeTuple!(__traits(getMember, T, mname))[j])); + serializer.beginWriteArrayEntry!TM(j); + serializeImpl!(Serializer, Policy, TM, TA)(serializer, tuple(__traits(getMember, value, mname))[j]); + serializer.endWriteArrayEntry!TM(j); + } + } + serializer.endWriteArray!TU(); + } else { + static if (__traits(compiles, serializer.beginWriteDictionary!TU(0))) { + enum nfields = getExpandedFieldCount!(TU, SerializableFields!TU); + serializer.beginWriteDictionary!TU(nfields); + } else { + serializer.beginWriteDictionary!TU(); + } + foreach (mname; SerializableFields!TU) { + alias TM = TypeTuple!(typeof(__traits(getMember, value, mname))); + static if (TM.length == 1) { + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, T, mname))); + enum name = getAttribute!(TU, mname, NameAttribute)(NameAttribute(underscoreStrip(mname))).name; + auto vt = __traits(getMember, value, mname); + serializer.beginWriteDictionaryEntry!(typeof(vt))(name); + serializeImpl!(Serializer, Policy, typeof(vt), TA)(serializer, vt); + serializer.endWriteDictionaryEntry!(typeof(vt))(name); + } else { + alias TA = TypeTuple!(); // FIXME: support attributes for tuples somehow + enum name = underscoreStrip(mname); + auto vt = tuple(__traits(getMember, value, mname)); + serializer.beginWriteDictionaryEntry!(typeof(vt))(name); + serializeImpl!(Serializer, Policy, typeof(vt), TA)(serializer, vt); + serializer.endWriteDictionaryEntry!(typeof(vt))(name); + } + } + static if (__traits(compiles, serializer.endWriteDictionary!TU(0))) { + serializer.endWriteDictionary!TU(nfields); + } else { + serializer.endWriteDictionary!TU(); + } + } + } else static if (isPointer!TU) { + if (value is null) { + serializer.writeValue(null); + return; + } + serializeImpl!(Serializer, Policy, PointerTarget!TU)(serializer, *value); + } else static if (is(TU == bool) || is(TU : real) || is(TU : long)) { + serializeImpl!(Serializer, Policy, string)(serializer, to!string(value)); + } else static assert(false, "Unsupported serialization type: " ~ T.stringof); +} + + +private T deserializeImpl(T, alias Policy, Serializer, ATTRIBUTES...)(ref Serializer deserializer) +{ + import std.typecons : BitFlags, Nullable; + + static assert(Serializer.isSupportedValueType!string, "All serializers must support string values."); + static assert(Serializer.isSupportedValueType!(typeof(null)), "All serializers must support null values."); + + static if (is(T == enum)) { + static if (hasAttributeL!(ByNameAttribute, ATTRIBUTES)) { + return deserializeImpl!(string, Policy, Serializer)(deserializer).to!T(); + } else { + return cast(T)deserializeImpl!(OriginalType!T, Policy, Serializer)(deserializer); + } + } else static if (Serializer.isSupportedValueType!T) { + return deserializer.readValue!T(); + } else static if (isStaticArray!T) { + alias TV = typeof(T.init[0]); + T ret; + size_t i = 0; + deserializer.readArray!T((sz) { assert(sz == 0 || sz == T.length); }, { + assert(i < T.length); + ret[i++] = deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isDynamicArray!T) { + alias TV = typeof(T.init[0]); + //auto ret = appender!T(); + T ret; // Cannot use appender because of DMD BUG 10690/10859/11357 + deserializer.readArray!T((sz) { ret.reserve(sz); }, () { + ret ~= deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret;//cast(T)ret.data; + } else static if (isAssociativeArray!T) { + alias TK = KeyType!T; + alias TV = ValueType!T; + T ret; + deserializer.readDictionary!T((name) { + TK key; + static if (is(TK == string)) key = name; + else static if (is(TK : real) || is(TK : long) || is(TK == enum)) key = name.to!TK; + else static if (isStringSerializable!TK) key = TK.fromString(name); + else static assert(false, "Associative array keys must be strings, numbers, enums, or have toString/fromString methods."); + ret[key] = deserializeImpl!(TV, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isInstanceOf!(Nullable, T)) { + if (deserializer.tryReadNull()) return T.init; + return T(deserializeImpl!(typeof(T.init.get()), Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (is(T == BitFlags!E, E)) { + T ret; + deserializer.readArray!(E[])((sz) {}, { + ret |= deserializeImpl!(E, Policy, Serializer, ATTRIBUTES)(deserializer); + }); + return ret; + } else static if (isPolicySerializable!(Policy, T)) { + alias CustomType = typeof(Policy!T.toRepresentation(T.init)); + return Policy!T.fromRepresentation(deserializeImpl!(CustomType, Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (isCustomSerializable!T) { + alias CustomType = typeof(T.init.toRepresentation()); + return T.fromRepresentation(deserializeImpl!(CustomType, Policy, Serializer, ATTRIBUTES)(deserializer)); + } else static if (isISOExtStringSerializable!T) { + return T.fromISOExtString(deserializer.readValue!string()); + } else static if (isStringSerializable!T) { + return T.fromString(deserializer.readValue!string()); + } else static if (is(T == struct) || is(T == class)) { + static if (is(T == class)) { + if (deserializer.tryReadNull()) return null; + } + + bool[__traits(allMembers, T).length] set; + string name; + T ret; + static if (is(T == class)) ret = new T; + + static if (hasAttributeL!(AsArrayAttribute, ATTRIBUTES)) { + size_t idx = 0; + deserializer.readArray!T((sz){}, { + static if (hasSerializableFields!T) { + switch (idx++) { + default: break; + foreach (i, mname; SerializableFields!T) { + alias TM = typeof(__traits(getMember, ret, mname)); + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, ret, mname))); + case i: + static if (hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + if (deserializer.tryReadNull()) return; + set[i] = true; + __traits(getMember, ret, mname) = deserializeImpl!(TM, Serializer, TA)(deserializer); + break; + } + } + } else { + pragma(msg, "Deserializing composite type "~T.stringof~" which has no serializable fields."); + } + }); + } else { + deserializer.readDictionary!T((name) { + static if (hasSerializableFields!T) { + switch (name) { + default: break; + foreach (i, mname; SerializableFields!T) { + alias TM = typeof(__traits(getMember, ret, mname)); + alias TA = TypeTuple!(__traits(getAttributes, __traits(getMember, ret, mname))); + enum fname = getAttribute!(T, mname, NameAttribute)(NameAttribute(underscoreStrip(mname))).name; + case fname: + static if (hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + if (deserializer.tryReadNull()) return; + set[i] = true; + __traits(getMember, ret, mname) = deserializeImpl!(TM, Policy, Serializer, TA)(deserializer); + break; + } + } + } else { + pragma(msg, "Deserializing composite type "~T.stringof~" which has no serializable fields."); + } + }); + } + foreach (i, mname; SerializableFields!T) + static if (!hasAttribute!(OptionalAttribute, __traits(getMember, T, mname))) + enforce(set[i], "Missing non-optional field '"~mname~"' of type '"~T.stringof~"'."); + return ret; + } else static if (isPointer!T) { + if (deserializer.tryReadNull()) return null; + alias PT = PointerTarget!T; + auto ret = new PT; + *ret = deserializeImpl!(PT, Policy, Serializer)(deserializer); + return ret; + } else static if (is(T == bool) || is(T : real) || is(T : long)) { + return to!T(deserializeImpl!(string, Policy, Serializer)(deserializer)); + } else static assert(false, "Unsupported serialization type: " ~ T.stringof); +} + + +/** + Attribute for overriding the field name during (de-)serialization. +*/ +NameAttribute name(string name) +{ + return NameAttribute(name); +} +/// +unittest { + struct Test { + @name("screen-size") int screenSize; + } +} + + +/** + Attribute marking a field as optional during deserialization. +*/ +@property OptionalAttribute optional() +{ + return OptionalAttribute(); +} +/// +unittest { + struct Test { + // does not need to be present during deserialization + @optional int screenSize = 100; + } +} + + +/** + Attribute for marking non-serialized fields. +*/ +@property IgnoreAttribute ignore() +{ + return IgnoreAttribute(); +} +/// +unittest { + struct Test { + // is neither serialized not deserialized + @ignore int screenSize; + } +} + + +/** + Attribute for forcing serialization of enum fields by name instead of by value. +*/ +@property ByNameAttribute byName() +{ + return ByNameAttribute(); +} +/// +unittest { + enum Color { + red, + green, + blue + } + + struct Test { + // serialized as an int (e.g. 1 for Color.green) + Color color; + // serialized as a string (e.g. "green" for Color.green) + @byName Color namedColor; + // serialized as array of ints + Color[] colorArray; + // serialized as array of strings + @byName Color[] namedColorArray; + } +} + + +/** + Attribute for representing a struct/class as an array instead of an object. + + Usually structs and class objects are serialized as dictionaries mapping + from field name to value. Using this attribute, they will be serialized + as a flat array instead. Note that changing the layout will make any + already serialized data mismatch when this attribute is used. +*/ +@property AsArrayAttribute asArray() +{ + return AsArrayAttribute(); +} +/// +unittest { + struct Fields { + int f1; + string f2; + double f3; + } + + struct Test { + // serialized as name:value pairs ["f1": int, "f2": string, "f3": double] + Fields object; + // serialized as a sequential list of values [int, string, double] + @asArray Fields array; + } + + import dub.internal.vibecompat.data.json; + static assert(is(typeof(serializeToJson(Test())))); +} + + +/// +enum FieldExistence +{ + missing, + exists, + defer +} + +/// User defined attribute (not intended for direct use) +struct NameAttribute { string name; } +/// ditto +struct OptionalAttribute {} +/// ditto +struct IgnoreAttribute {} +/// ditto +struct ByNameAttribute {} +/// ditto +struct AsArrayAttribute {} + +/** + Checks if a given type has a custom serialization representation. + + A class or struct type is custom serializable if it defines a pair of + `toRepresentation`/`fromRepresentation` methods. Any class or + struct type that has this trait will be serialized by using the return + value of it's `toRepresentation` method instead of the original value. + + This trait has precedence over `isISOExtStringSerializable` and + `isStringSerializable`. +*/ +template isCustomSerializable(T) +{ + enum bool isCustomSerializable = is(typeof(T.init.toRepresentation())) && is(typeof(T.fromRepresentation(T.init.toRepresentation())) == T); +} +/// +unittest { + // represented as a single uint when serialized + static struct S { + ushort x, y; + + uint toRepresentation() const { return x + (y << 16); } + static S fromRepresentation(uint i) { return S(i & 0xFFFF, i >> 16); } + } + + static assert(isCustomSerializable!S); +} + + +/** + Checks if a given type has an ISO extended string serialization representation. + + A class or struct type is ISO extended string serializable if it defines a + pair of `toISOExtString`/`fromISOExtString` methods. Any class or + struct type that has this trait will be serialized by using the return + value of it's `toISOExtString` method instead of the original value. + + This is mainly useful for supporting serialization of the the date/time + types in `std.datetime`. + + This trait has precedence over `isStringSerializable`. +*/ +template isISOExtStringSerializable(T) +{ + enum bool isISOExtStringSerializable = is(typeof(T.init.toISOExtString()) == string) && is(typeof(T.fromISOExtString("")) == T); +} +/// +unittest { + import std.datetime; + + static assert(isISOExtStringSerializable!DateTime); + static assert(isISOExtStringSerializable!SysTime); + + // represented as an ISO extended string when serialized + static struct S { + // dummy example implementations + string toISOExtString() const { return ""; } + static S fromISOExtString(string s) { return S.init; } + } + + static assert(isISOExtStringSerializable!S); +} + + +/** + Checks if a given type has a string serialization representation. + + A class or struct type is string serializable if it defines a pair of + `toString`/`fromString` methods. Any class or struct type that + has this trait will be serialized by using the return value of it's + `toString` method instead of the original value. +*/ +template isStringSerializable(T) +{ + enum bool isStringSerializable = is(typeof(T.init.toString()) == string) && is(typeof(T.fromString("")) == T); +} +/// +unittest { + import std.conv; + + // represented as the boxed value when serialized + static struct Box(T) { + T value; + } + + template BoxPol(S) + { + auto toRepresentation(S s) { + return s.value; + } + + S fromRepresentation(typeof(S.init.value) v) { + return S(v); + } + } + static assert(isPolicySerializable!(BoxPol, Box!int)); +} + +private template DefaultPolicy(T) +{ +} + +/** + Checks if a given policy supports custom serialization for a given type. + + A class or struct type is custom serializable according to a policy if + the policy defines a pair of `toRepresentation`/`fromRepresentation` + functions. Any class or struct type that has this trait for the policy supplied to + `serializeWithPolicy` will be serialized by using the return value of the + policy `toRepresentation` function instead of the original value. + + This trait has precedence over `isCustomSerializable`, + `isISOExtStringSerializable` and `isStringSerializable`. + + See_Also: `vibe.data.serialization.serializeWithPolicy` +*/ +template isPolicySerializable(alias Policy, T) +{ + enum bool isPolicySerializable = is(typeof(Policy!T.toRepresentation(T.init))) && + is(typeof(Policy!T.fromRepresentation(Policy!T.toRepresentation(T.init))) == T); +} +/// +unittest { + import std.conv; + + // represented as a string when serialized + static struct S { + int value; + + // dummy example implementations + string toString() const { return value.to!string(); } + static S fromString(string s) { return S(s.to!int()); } + } + + static assert(isStringSerializable!S); +} + +/** + Chains serialization policy. + + Constructs a serialization policy that given a type `T` will apply the + first compatible policy `toRepresentation` and `fromRepresentation` + functions. Policies are evaluated left-to-right according to + `isPolicySerializable`. + + See_Also: `vibe.data.serialization.serializeWithPolicy` +*/ +template ChainedPolicy(alias Primary, Fallbacks...) +{ + static if (Fallbacks.length == 0) { + alias ChainedPolicy = Primary; + } else { + alias ChainedPolicy = ChainedPolicy!(ChainedPolicyImpl!(Primary, Fallbacks[0]), Fallbacks[1..$]); + } +} +/// +unittest { + import std.conv; + + // To be represented as the boxed value when serialized + static struct Box(T) { + T value; + } + // Also to berepresented as the boxed value when serialized, but has + // a different way to access the value. + static struct Box2(T) { + private T v; + ref T get() { + return v; + } + } + template BoxPol(S) + { + auto toRepresentation(S s) { + return s.value; + } + + S fromRepresentation(typeof(toRepresentation(S.init)) v) { + return S(v); + } + } + template Box2Pol(S) + { + auto toRepresentation(S s) { + return s.get(); + } + + S fromRepresentation(typeof(toRepresentation(S.init)) v) { + S s; + s.get() = v; + return s; + } + } + alias ChainPol = ChainedPolicy!(BoxPol, Box2Pol); + static assert(!isPolicySerializable!(BoxPol, Box2!int)); + static assert(!isPolicySerializable!(Box2Pol, Box!int)); + static assert(isPolicySerializable!(ChainPol, Box!int)); + static assert(isPolicySerializable!(ChainPol, Box2!int)); +} + +private template ChainedPolicyImpl(alias Primary, alias Fallback) +{ + template Pol(T) + { + static if (isPolicySerializable!(Primary, T)) { + alias toRepresentation = Primary!T.toRepresentation; + alias fromRepresentation = Primary!T.fromRepresentation; + } else { + alias toRepresentation = Fallback!T.toRepresentation; + alias fromRepresentation = Fallback!T.fromRepresentation; + } + } + alias ChainedPolicyImpl = Pol; +} + +private template hasAttribute(T, alias decl) { enum hasAttribute = findFirstUDA!(T, decl).found; } + +unittest { + @asArray int i1; + static assert(hasAttribute!(AsArrayAttribute, i1)); + int i2; + static assert(!hasAttribute!(AsArrayAttribute, i2)); +} + +private template hasAttributeL(T, ATTRIBUTES...) { + static if (ATTRIBUTES.length == 1) { + enum hasAttributeL = is(typeof(ATTRIBUTES[0]) == T); + } else static if (ATTRIBUTES.length > 1) { + enum hasAttributeL = hasAttributeL!(T, ATTRIBUTES[0 .. $/2]) || hasAttributeL!(T, ATTRIBUTES[$/2 .. $]); + } else { + enum hasAttributeL = false; + } +} + +unittest { + static assert(hasAttributeL!(AsArrayAttribute, byName, asArray)); + static assert(!hasAttributeL!(AsArrayAttribute, byName)); +} + +private static T getAttribute(TT, string mname, T)(T default_value) +{ + enum val = findFirstUDA!(T, __traits(getMember, TT, mname)); + static if (val.found) return val.value; + else return default_value; +} + +private string underscoreStrip(string field_name) +{ + if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name; + else return field_name[0 .. $-1]; +} + + +private template hasSerializableFields(T, size_t idx = 0) +{ + enum hasSerializableFields = SerializableFields!(T).length > 0; + /*static if (idx < __traits(allMembers, T).length) { + enum mname = __traits(allMembers, T)[idx]; + static if (!isRWPlainField!(T, mname) && !isRWField!(T, mname)) enum hasSerializableFields = hasSerializableFields!(T, idx+1); + else static if (hasAttribute!(IgnoreAttribute, __traits(getMember, T, mname))) enum hasSerializableFields = hasSerializableFields!(T, idx+1); + else enum hasSerializableFields = true; + } else enum hasSerializableFields = false;*/ +} + +private template SerializableFields(COMPOSITE) +{ + alias SerializableFields = FilterSerializableFields!(COMPOSITE, __traits(allMembers, COMPOSITE)); +} + +private template FilterSerializableFields(COMPOSITE, FIELDS...) +{ + static if (FIELDS.length > 1) { + alias FilterSerializableFields = TypeTuple!( + FilterSerializableFields!(COMPOSITE, FIELDS[0 .. $/2]), + FilterSerializableFields!(COMPOSITE, FIELDS[$/2 .. $])); + } else static if (FIELDS.length == 1) { + alias T = COMPOSITE; + enum mname = FIELDS[0]; + static if (isRWPlainField!(T, mname) || isRWField!(T, mname)) { + alias Tup = TypeTuple!(__traits(getMember, COMPOSITE, FIELDS[0])); + static if (Tup.length != 1) { + alias FilterSerializableFields = TypeTuple!(mname); + } else { + static if (!hasAttribute!(IgnoreAttribute, __traits(getMember, T, mname))) + alias FilterSerializableFields = TypeTuple!(mname); + else alias FilterSerializableFields = TypeTuple!(); + } + } else alias FilterSerializableFields = TypeTuple!(); + } else alias FilterSerializableFields = TypeTuple!(); +} + +private size_t getExpandedFieldCount(T, FIELDS...)() +{ + size_t ret = 0; + foreach (F; FIELDS) ret += TypeTuple!(__traits(getMember, T, F)).length; + return ret; +} + +/******************************************************************************/ +/* General serialization unit testing */ +/******************************************************************************/ + +version (unittest) { + private struct TestSerializer { + import std.array, std.conv, std.string; + + string result; + + enum isSupportedValueType(T) = is(T == string) || is(T == typeof(null)) || is(T == float) || is (T == int); + + string getSerializedResult() { return result; } + void beginWriteDictionary(T)() { result ~= "D("~T.mangleof~"){"; } + void endWriteDictionary(T)() { result ~= "}D("~T.mangleof~")"; } + void beginWriteDictionaryEntry(T)(string name) { result ~= "DE("~T.mangleof~","~name~")("; } + void endWriteDictionaryEntry(T)(string name) { result ~= ")DE("~T.mangleof~","~name~")"; } + void beginWriteArray(T)(size_t length) { result ~= "A("~T.mangleof~")["~length.to!string~"]["; } + void endWriteArray(T)() { result ~= "]A("~T.mangleof~")"; } + void beginWriteArrayEntry(T)(size_t i) { result ~= "AE("~T.mangleof~","~i.to!string~")("; } + void endWriteArrayEntry(T)(size_t i) { result ~= ")AE("~T.mangleof~","~i.to!string~")"; } + void writeValue(T)(T value) { + if (is(T == typeof(null))) result ~= "null"; + else { + assert(isSupportedValueType!T); + result ~= "V("~T.mangleof~")("~value.to!string~")"; + } + } + + // deserialization + void readDictionary(T)(scope void delegate(string) entry_callback) + { + skip("D("~T.mangleof~"){"); + while (result.startsWith("DE(")) { + result = result[3 .. $]; + auto idx = result.indexOf(','); + auto idx2 = result.indexOf(")("); + assert(idx > 0 && idx2 > idx); + auto t = result[0 .. idx]; + auto n = result[idx+1 .. idx2]; + result = result[idx2+2 .. $]; + entry_callback(n); + skip(")DE("~t~","~n~")"); + } + skip("}D("~T.mangleof~")"); + } + + void readArray(T)(scope void delegate(size_t) size_callback, scope void delegate() entry_callback) + { + skip("A("~T.mangleof~")["); + auto bidx = result.indexOf("]["); + assert(bidx > 0); + auto cnt = result[0 .. bidx].to!size_t; + result = result[bidx+2 .. $]; + + size_t i = 0; + while (result.startsWith("AE(")) { + result = result[3 .. $]; + auto idx = result.indexOf(','); + auto idx2 = result.indexOf(")("); + assert(idx > 0 && idx2 > idx); + auto t = result[0 .. idx]; + auto n = result[idx+1 .. idx2]; + result = result[idx2+2 .. $]; + assert(n == i.to!string); + entry_callback(); + skip(")AE("~t~","~n~")"); + i++; + } + skip("]A("~T.mangleof~")"); + + assert(i == cnt); + } + + T readValue(T)() + { + skip("V("~T.mangleof~")("); + auto idx = result.indexOf(')'); + assert(idx >= 0); + auto ret = result[0 .. idx].to!T; + result = result[idx+1 .. $]; + return ret; + } + + void skip(string prefix) + { + assert(result.startsWith(prefix), result); + result = result[prefix.length .. $]; + } + + bool tryReadNull() + { + if (result.startsWith("null")) { + result = result[4 .. $]; + return true; + } else return false; + } + } +} + +unittest { // basic serialization behavior + import std.typecons : Nullable; + + static void test(T)(T value, string expected) { + assert(serialize!TestSerializer(value) == expected, serialize!TestSerializer(value)); + static if (isPointer!T) { + if (value) assert(*deserialize!(TestSerializer, T)(expected) == *value); + else assert(deserialize!(TestSerializer, T)(expected) is null); + } else static if (is(T == Nullable!U, U)) { + if (value.isNull()) assert(deserialize!(TestSerializer, T)(expected).isNull); + else assert(deserialize!(TestSerializer, T)(expected) == value); + } else assert(deserialize!(TestSerializer, T)(expected) == value); + } + + test("hello", "V(Aya)(hello)"); + test(12, "V(i)(12)"); + test(12.0, "V(Aya)(12)"); + test(12.0f, "V(f)(12)"); + assert(serialize!TestSerializer(null) == "null"); + test(["hello", "world"], "A(AAya)[2][AE(Aya,0)(V(Aya)(hello))AE(Aya,0)AE(Aya,1)(V(Aya)(world))AE(Aya,1)]A(AAya)"); + test(["hello": "world"], "D(HAyaAya){DE(Aya,hello)(V(Aya)(world))DE(Aya,hello)}D(HAyaAya)"); + test(cast(int*)null, "null"); + int i = 42; + test(&i, "V(i)(42)"); + Nullable!int j; + test(j, "null"); + j = 42; + test(j, "V(i)(42)"); +} + +unittest { // basic user defined types + static struct S { string f; } + enum Sm = S.mangleof; + auto s = S("hello"); + enum s_ser = "D("~Sm~"){DE(Aya,f)(V(Aya)(hello))DE(Aya,f)}D("~Sm~")"; + assert(serialize!TestSerializer(s) == s_ser, serialize!TestSerializer(s)); + assert(deserialize!(TestSerializer, S)(s_ser) == s); + + static class C { string f; } + enum Cm = C.mangleof; + C c; + assert(serialize!TestSerializer(c) == "null"); + c = new C; + c.f = "hello"; + enum c_ser = "D("~Cm~"){DE(Aya,f)(V(Aya)(hello))DE(Aya,f)}D("~Cm~")"; + assert(serialize!TestSerializer(c) == c_ser); + assert(deserialize!(TestSerializer, C)(c_ser).f == c.f); + + enum E { hello, world } + assert(serialize!TestSerializer(E.hello) == "V(i)(0)"); + assert(serialize!TestSerializer(E.world) == "V(i)(1)"); +} + +unittest { // tuple serialization + import std.typecons : Tuple; + + static struct S(T...) { T f; } + enum Sm = S!(int, string).mangleof; + enum Tum = Tuple!(int, string).mangleof; + auto s = S!(int, string)(42, "hello"); + assert(serialize!TestSerializer(s) == + "D("~Sm~"){DE("~Tum~",f)(A("~Tum~")[2][AE(i,0)(V(i)(42))AE(i,0)AE(Aya,1)(V(Aya)(hello))AE(Aya,1)]A("~Tum~"))DE("~Tum~",f)}D("~Sm~")"); + + static struct T { @asArray S!(int, string) g; } + enum Tm = T.mangleof; + auto t = T(s); + assert(serialize!TestSerializer(t) == + "D("~Tm~"){DE("~Sm~",g)(A("~Sm~")[2][AE(i,0)(V(i)(42))AE(i,0)AE(Aya,1)(V(Aya)(hello))AE(Aya,1)]A("~Sm~"))DE("~Sm~",g)}D("~Tm~")"); +} + +unittest { // testing the various UDAs + enum E { hello, world } + enum Em = E.mangleof; + static struct S { + @byName E e; + @ignore int i; + @optional float f; + } + enum Sm = S.mangleof; + auto s = S(E.world, 42, 1.0f); + assert(serialize!TestSerializer(s) == + "D("~Sm~"){DE("~Em~",e)(V(Aya)(world))DE("~Em~",e)DE(f,f)(V(f)(1))DE(f,f)}D("~Sm~")"); +} + +unittest { // custom serialization support + // iso-ext + import std.datetime; + auto t = TimeOfDay(6, 31, 23); + assert(serialize!TestSerializer(t) == "V(Aya)(06:31:23)"); + auto d = Date(1964, 1, 23); + assert(serialize!TestSerializer(d) == "V(Aya)(1964-01-23)"); + auto dt = DateTime(d, t); + assert(serialize!TestSerializer(dt) == "V(Aya)(1964-01-23T06:31:23)"); + auto st = SysTime(dt, UTC()); + assert(serialize!TestSerializer(st) == "V(Aya)(1964-01-23T06:31:23Z)"); + + // string + struct S1 { int i; string toString() const { return "hello"; } static S1 fromString(string) { return S1.init; } } + struct S2 { int i; string toString() const { return "hello"; } } + enum S2m = S2.mangleof; + struct S3 { int i; static S3 fromString(string) { return S3.init; } } + enum S3m = S3.mangleof; + assert(serialize!TestSerializer(S1.init) == "V(Aya)(hello)"); + assert(serialize!TestSerializer(S2.init) == "D("~S2m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~S2m~")"); + assert(serialize!TestSerializer(S3.init) == "D("~S3m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~S3m~")"); + + // custom + struct C1 { int i; float toRepresentation() const { return 1.0f; } static C1 fromRepresentation(float f) { return C1.init; } } + struct C2 { int i; float toRepresentation() const { return 1.0f; } } + enum C2m = C2.mangleof; + struct C3 { int i; static C3 fromRepresentation(float f) { return C3.init; } } + enum C3m = C3.mangleof; + assert(serialize!TestSerializer(C1.init) == "V(f)(1)"); + assert(serialize!TestSerializer(C2.init) == "D("~C2m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~C2m~")"); + assert(serialize!TestSerializer(C3.init) == "D("~C3m~"){DE(i,i)(V(i)(0))DE(i,i)}D("~C3m~")"); +} + +unittest // Testing corner case: member function returning by ref +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + int i; + ref int foo() { return i; } + } + + static assert(__traits(compiles, { S().serializeToJson(); })); + static assert(__traits(compiles, { Json().deserializeJson!S(); })); + + auto s = S(1); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +unittest // Testing corner case: Variadic template constructors and methods +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + int i; + this(Args...)(Args args) {} + int foo(Args...)(Args args) { return i; } + ref int bar(Args...)(Args args) { return i; } + } + + static assert(__traits(compiles, { S().serializeToJson(); })); + static assert(__traits(compiles, { Json().deserializeJson!S(); })); + + auto s = S(1); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +unittest // Make sure serializing through properties still works +{ + import dub.internal.vibecompat.data.json; + + static struct S + { + public int i; + private int privateJ; + + @property int j() { return privateJ; } + @property void j(int j) { privateJ = j; } + } + + auto s = S(1, 2); + assert(s.serializeToJson().deserializeJson!S() == s); +} + +unittest { // test BitFlags serialization + import std.typecons : BitFlags; + + enum Flag { + a = 1<<0, + b = 1<<1, + c = 1<<2 + } + enum Flagm = Flag.mangleof; + + alias Flags = BitFlags!Flag; + enum Flagsm = Flags.mangleof; + + enum Fi_ser = "A(A"~Flagm~")[0][]A(A"~Flagm~")"; + assert(serialize!TestSerializer(Flags.init) == Fi_ser); + + enum Fac_ser = "A(A"~Flagm~")[2][AE("~Flagm~",0)(V(i)(1))AE("~Flagm~",0)AE("~Flagm~",1)(V(i)(4))AE("~Flagm~",1)]A(A"~Flagm~")"; + assert(serialize!TestSerializer(Flags(Flag.a, Flag.c)) == Fac_ser); + + struct S { @byName Flags f; } + enum Sm = S.mangleof; + enum Sac_ser = "D("~Sm~"){DE("~Flagsm~",f)(A(A"~Flagm~")[2][AE("~Flagm~",0)(V(Aya)(a))AE("~Flagm~",0)AE("~Flagm~",1)(V(Aya)(c))AE("~Flagm~",1)]A(A"~Flagm~"))DE("~Flagsm~",f)}D("~Sm~")"; + + assert(serialize!TestSerializer(S(Flags(Flag.a, Flag.c))) == Sac_ser); + + assert(deserialize!(TestSerializer, Flags)(Fi_ser) == Flags.init); + assert(deserialize!(TestSerializer, Flags)(Fac_ser) == Flags(Flag.a, Flag.c)); + assert(deserialize!(TestSerializer, S)(Sac_ser) == S(Flags(Flag.a, Flag.c))); +} diff --git a/source/dub/internal/vibecompat/data/utils.d b/source/dub/internal/vibecompat/data/utils.d index 5269fb3..a171fe3 100644 --- a/source/dub/internal/vibecompat/data/utils.d +++ b/source/dub/internal/vibecompat/data/utils.d @@ -7,24 +7,713 @@ */ module dub.internal.vibecompat.data.utils; +version (Have_vibe_d) {} +else: + public import std.traits; +/** + Checks if given type is a getter function type -template isRWPlainField(T, string M) + Returns: `true` if argument is a getter + */ +template isPropertyGetter(T...) + if (T.length == 1) { - static if( !__traits(compiles, typeof(__traits(getMember, T, M))) ){ - enum isRWPlainField = false; - } else { - //pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof); - enum isRWPlainField = isRWField!(T, M) && __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M))); + import std.traits : functionAttributes, FunctionAttribute, ReturnType, + isSomeFunction; + static if (isSomeFunction!(T[0])) { + enum isPropertyGetter = + (functionAttributes!(T[0]) & FunctionAttribute.property) != 0 + && !is(ReturnType!T == void); + } + else + enum isPropertyGetter = false; +} + +/// +unittest +{ + interface Test + { + @property int getter(); + @property void setter(int); + int simple(); + } + + static assert(isPropertyGetter!(typeof(&Test.getter))); + static assert(!isPropertyGetter!(typeof(&Test.setter))); + static assert(!isPropertyGetter!(typeof(&Test.simple))); + static assert(!isPropertyGetter!int); +} + +/** + Checks if given type is a setter function type + + Returns: `true` if argument is a setter + */ +template isPropertySetter(T...) + if (T.length == 1) +{ + import std.traits : functionAttributes, FunctionAttribute, ReturnType, + isSomeFunction; + + static if (isSomeFunction!(T[0])) { + enum isPropertySetter = + (functionAttributes!(T) & FunctionAttribute.property) != 0 + && is(ReturnType!(T[0]) == void); + } + else + enum isPropertySetter = false; +} + +/// +unittest +{ + interface Test + { + @property int getter(); + @property void setter(int); + int simple(); + } + + static assert(isPropertySetter!(typeof(&Test.setter))); + static assert(!isPropertySetter!(typeof(&Test.getter))); + static assert(!isPropertySetter!(typeof(&Test.simple))); + static assert(!isPropertySetter!int); +} + +/** + Deduces single base interface for a type. Multiple interfaces + will result in compile-time error. + + Params: + T = interface or class type + + Returns: + T if it is an interface. If T is a class, interface it implements. +*/ +template baseInterface(T) + if (is(T == interface) || is(T == class)) +{ + import std.traits : InterfacesTuple; + + static if (is(T == interface)) { + alias baseInterface = T; + } + else + { + alias Ifaces = InterfacesTuple!T; + static assert ( + Ifaces.length == 1, + "Type must be either provided as an interface or implement only one interface" + ); + alias baseInterface = Ifaces[0]; } } -template isRWField(T, string M) +/// +unittest { - enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M)); - //pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0")); + interface I1 { } + class A : I1 { } + interface I2 { } + class B : I1, I2 { } + + static assert (is(baseInterface!I1 == I1)); + static assert (is(baseInterface!A == I1)); + static assert (!is(typeof(baseInterface!B))); } -/// private -private T Tgen(T)(){ return T.init; } + +/** + Determins if a member is a public, non-static data field. +*/ +template isRWPlainField(T, string M) +{ + static if (!isRWField!(T, M)) enum isRWPlainField = false; + else { + //pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof); + enum isRWPlainField = __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M))); + } +} + +/** + Determines if a member is a public, non-static, de-facto data field. + + In addition to plain data fields, R/W properties are also accepted. +*/ +template isRWField(T, string M) +{ + import std.traits; + import std.typetuple; + + static void testAssign()() { + T t = void; + __traits(getMember, t, M) = __traits(getMember, t, M); + } + + // reject type aliases + static if (is(TypeTuple!(__traits(getMember, T, M)))) enum isRWField = false; + // reject non-public members + else static if (!isPublicMember!(T, M)) enum isRWField = false; + // reject static members + else static if (!isNonStaticMember!(T, M)) enum isRWField = false; + // reject non-typed members + else static if (!is(typeof(__traits(getMember, T, M)))) enum isRWField = false; + // reject void typed members (includes templates) + else static if (is(typeof(__traits(getMember, T, M)) == void)) enum isRWField = false; + // reject non-assignable members + else static if (!__traits(compiles, testAssign!()())) enum isRWField = false; + else static if (anySatisfy!(isSomeFunction, __traits(getMember, T, M))) { + // If M is a function, reject if not @property or returns by ref + private enum FA = functionAttributes!(__traits(getMember, T, M)); + enum isRWField = (FA & FunctionAttribute.property) != 0; + } else { + enum isRWField = true; + } +} + +unittest { + import std.algorithm; + + struct S { + alias a = int; // alias + int i; // plain RW field + enum j = 42; // manifest constant + static int k = 42; // static field + private int privateJ; // private RW field + + this(Args...)(Args args) {} + + // read-write property (OK) + @property int p1() { return privateJ; } + @property void p1(int j) { privateJ = j; } + // read-only property (NO) + @property int p2() { return privateJ; } + // write-only property (NO) + @property void p3(int value) { privateJ = value; } + // ref returning property (OK) + @property ref int p4() { return i; } + // parameter-less template property (OK) + @property ref int p5()() { return i; } + // not treated as a property by DMD, so not a field + @property int p6()() { return privateJ; } + @property void p6(int j)() { privateJ = j; } + + static @property int p7() { return k; } + static @property void p7(int value) { k = value; } + + ref int f1() { return i; } // ref returning function (no field) + + int f2(Args...)(Args args) { return i; } + + ref int f3(Args...)(Args args) { return i; } + + void someMethod() {} + + ref int someTempl()() { return i; } + } + + enum plainFields = ["i"]; + enum fields = ["i", "p1", "p4", "p5"]; + + foreach (mem; __traits(allMembers, S)) { + static if (isRWField!(S, mem)) static assert(fields.canFind(mem), mem~" detected as field."); + else static assert(!fields.canFind(mem), mem~" not detected as field."); + + static if (isRWPlainField!(S, mem)) static assert(plainFields.canFind(mem), mem~" not detected as plain field."); + else static assert(!plainFields.canFind(mem), mem~" not detected as plain field."); + } +} + +package T Tgen(T)(){ return T.init; } + + +/** + Tests if the protection of a member is public. +*/ +template isPublicMember(T, string M) +{ + import std.algorithm, std.typetuple : TypeTuple; + + static if (!__traits(compiles, TypeTuple!(__traits(getMember, T, M)))) enum isPublicMember = false; + else { + alias MEM = TypeTuple!(__traits(getMember, T, M)); + enum isPublicMember = __traits(getProtection, MEM).among("public", "export"); + } +} + +unittest { + class C { + int a; + export int b; + protected int c; + private int d; + package int e; + void f() {} + static void g() {} + private void h() {} + private static void i() {} + } + + static assert (isPublicMember!(C, "a")); + static assert (isPublicMember!(C, "b")); + static assert (!isPublicMember!(C, "c")); + static assert (!isPublicMember!(C, "d")); + static assert (!isPublicMember!(C, "e")); + static assert (isPublicMember!(C, "f")); + static assert (isPublicMember!(C, "g")); + static assert (!isPublicMember!(C, "h")); + static assert (!isPublicMember!(C, "i")); + + struct S { + int a; + export int b; + private int d; + package int e; + } + static assert (isPublicMember!(S, "a")); + static assert (isPublicMember!(S, "b")); + static assert (!isPublicMember!(S, "d")); + static assert (!isPublicMember!(S, "e")); + + S s; + s.a = 21; + assert(s.a == 21); +} + +/** + Tests if a member requires $(D this) to be used. +*/ +template isNonStaticMember(T, string M) +{ + import std.typetuple; + import std.traits; + + alias MF = TypeTuple!(__traits(getMember, T, M)); + static if (M.length == 0) { + enum isNonStaticMember = false; + } else static if (anySatisfy!(isSomeFunction, MF)) { + enum isNonStaticMember = !__traits(isStaticFunction, MF); + } else { + enum isNonStaticMember = !__traits(compiles, (){ auto x = __traits(getMember, T, M); }()); + } +} + +unittest { // normal fields + struct S { + int a; + static int b; + enum c = 42; + void f(); + static void g(); + ref int h() { return a; } + static ref int i() { return b; } + } + static assert(isNonStaticMember!(S, "a")); + static assert(!isNonStaticMember!(S, "b")); + static assert(!isNonStaticMember!(S, "c")); + static assert(isNonStaticMember!(S, "f")); + static assert(!isNonStaticMember!(S, "g")); + static assert(isNonStaticMember!(S, "h")); + static assert(!isNonStaticMember!(S, "i")); +} + +unittest { // tuple fields + struct S(T...) { + T a; + static T b; + } + + alias T = S!(int, float); + auto p = T.b; + static assert(isNonStaticMember!(T, "a")); + static assert(!isNonStaticMember!(T, "b")); + + alias U = S!(); + static assert(!isNonStaticMember!(U, "a")); + static assert(!isNonStaticMember!(U, "b")); +} + + +/** + Tests if a Group of types is implicitly convertible to a Group of target types. +*/ +bool areConvertibleTo(alias TYPES, alias TARGET_TYPES)() + if (isGroup!TYPES && isGroup!TARGET_TYPES) +{ + static assert(TYPES.expand.length == TARGET_TYPES.expand.length); + foreach (i, V; TYPES.expand) + if (!is(V : TARGET_TYPES.expand[i])) + return false; + return true; +} + +/// Test if the type $(D DG) is a correct delegate for an opApply where the +/// key/index is of type $(D TKEY) and the value of type $(D TVALUE). +template isOpApplyDg(DG, TKEY, TVALUE) { + import std.traits; + static if (is(DG == delegate) && is(ReturnType!DG : int)) { + private alias PTT = ParameterTypeTuple!(DG); + private alias PSCT = ParameterStorageClassTuple!(DG); + private alias STC = ParameterStorageClass; + // Just a value + static if (PTT.length == 1) { + enum isOpApplyDg = (is(PTT[0] == TVALUE) && PSCT[0] == STC.ref_); + } else static if (PTT.length == 2) { + enum isOpApplyDg = (is(PTT[0] == TKEY) && PSCT[0] == STC.ref_) + && (is(PTT[1] == TKEY) && PSCT[1] == STC.ref_); + } else + enum isOpApplyDg = false; + } else { + enum isOpApplyDg = false; + } +} + +/** + TypeTuple which does not auto-expand. + + Useful when you need + to multiple several type tuples as different template argument + list parameters, without merging those. +*/ +template Group(T...) +{ + alias expand = T; +} + +/// +unittest +{ + alias group = Group!(int, double, string); + static assert (!is(typeof(group.length))); + static assert (group.expand.length == 3); + static assert (is(group.expand[1] == double)); +} + +/** +*/ +template isGroup(T...) +{ + static if (T.length != 1) enum isGroup = false; + else enum isGroup = + !is(T[0]) && is(typeof(T[0]) == void) // does not evaluate to something + && is(typeof(T[0].expand.length) : size_t) // expands to something with length + && !is(typeof(&(T[0].expand))); // expands to not addressable +} + +version (unittest) // NOTE: GDC complains about template definitions in unittest blocks +{ + import std.typetuple; + + alias group = Group!(int, double, string); + alias group2 = Group!(); + + template Fake(T...) + { + int[] expand; + } + alias fake = Fake!(int, double, string); + + alias fake2 = TypeTuple!(int, double, string); + + static assert (isGroup!group); + static assert (isGroup!group2); + static assert (!isGroup!fake); + static assert (!isGroup!fake2); +} + +/* Copied from Phobos as it is private there. + */ +private template isSame(ab...) + if (ab.length == 2) +{ + static if (is(ab[0]) && is(ab[1])) + { + enum isSame = is(ab[0] == ab[1]); + } + else static if (!is(ab[0]) && + !is(ab[1]) && + is(typeof(ab[0] == ab[1]) == bool) && + (ab[0] == ab[1])) + { + static if (!__traits(compiles, &ab[0]) || + !__traits(compiles, &ab[1])) + enum isSame = (ab[0] == ab[1]); + else + enum isSame = __traits(isSame, ab[0], ab[1]); + } + else + { + enum isSame = __traits(isSame, ab[0], ab[1]); + } +} + +/** + Compares two groups for element identity + + Params: + Group1, Group2 = any instances of `Group` + + Returns: + `true` if each element of Group1 is identical to + the one of Group2 at the same index +*/ +template Compare(alias Group1, alias Group2) + if (isGroup!Group1 && isGroup!Group2) +{ + private bool implementation() + { + static if (Group1.expand.length == Group2.expand.length) { + foreach (index, element; Group1.expand) + { + static if (!isSame!(Group1.expand[index], Group2.expand[index])) { + return false; + } + } + return true; + } + else { + return false; + } + } + + enum Compare = implementation(); +} + +/// +unittest +{ + alias one = Group!(int, double); + alias two = Group!(int, double); + alias three = Group!(double, int); + static assert (Compare!(one, two)); + static assert (!Compare!(one, three)); +} + +/** + Small convenience wrapper to find and extract certain UDA from given type. + Will stop on first element which is of required type. + + Params: + UDA = type or template to search for in UDA list + Symbol = symbol to query for UDA's + allow_types = if set to `false` considers attached `UDA` types an error + (only accepts instances/values) + + Returns: aggregated search result struct with 3 field. `value` aliases found UDA. + `found` is boolean flag for having a valid find. `index` is integer index in + attribute list this UDA was found at. +*/ +template findFirstUDA(alias UDA, alias Symbol, bool allow_types = false) if (!is(UDA)) +{ + enum findFirstUDA = findNextUDA!(UDA, Symbol, 0, allow_types); +} + +/// Ditto +template findFirstUDA(UDA, alias Symbol, bool allow_types = false) +{ + enum findFirstUDA = findNextUDA!(UDA, Symbol, 0, allow_types); +} + +private struct UdaSearchResult(alias UDA) +{ + alias value = UDA; + bool found = false; + long index = -1; +} + +/** + Small convenience wrapper to find and extract certain UDA from given type. + Will start at the given index and stop on the next element which is of required type. + + Params: + UDA = type or template to search for in UDA list + Symbol = symbol to query for UDA's + idx = 0-based index to start at. Should be positive, and under the total number of attributes. + allow_types = if set to `false` considers attached `UDA` types an error + (only accepts instances/values) + + Returns: aggregated search result struct with 3 field. `value` aliases found UDA. + `found` is boolean flag for having a valid find. `index` is integer index in + attribute list this UDA was found at. + */ +template findNextUDA(alias UDA, alias Symbol, long idx, bool allow_types = false) if (!is(UDA)) +{ + import std.traits : isInstanceOf; + import std.typetuple : TypeTuple; + + private alias udaTuple = TypeTuple!(__traits(getAttributes, Symbol)); + + static assert(idx >= 0, "Index given to findNextUDA can't be negative"); + static assert(idx <= udaTuple.length, "Index given to findNextUDA is above the number of attribute"); + + public template extract(size_t index, list...) + { + static if (!list.length) enum extract = UdaSearchResult!(null)(false, -1); + else { + static if (is(list[0])) { + static if (is(UDA) && is(list[0] == UDA) || !is(UDA) && isInstanceOf!(UDA, list[0])) { + static assert (allow_types, "findNextUDA is designed to look up values, not types"); + enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } else { + static if (is(UDA) && is(typeof(list[0]) == UDA) || !is(UDA) && isInstanceOf!(UDA, typeof(list[0]))) { + import vibe.internal.meta.traits : isPropertyGetter; + static if (isPropertyGetter!(list[0])) { + enum value = list[0]; + enum extract = UdaSearchResult!(value)(true, index); + } else enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } + } + } + + enum findNextUDA = extract!(idx, udaTuple[idx .. $]); +} +/// ditto +template findNextUDA(UDA, alias Symbol, long idx, bool allow_types = false) +{ + import std.traits : isInstanceOf; + import std.typetuple : TypeTuple; + + private alias udaTuple = TypeTuple!(__traits(getAttributes, Symbol)); + + static assert(idx >= 0, "Index given to findNextUDA can't be negative"); + static assert(idx <= udaTuple.length, "Index given to findNextUDA is above the number of attribute"); + + public template extract(size_t index, list...) + { + static if (!list.length) enum extract = UdaSearchResult!(null)(false, -1); + else { + static if (is(list[0])) { + static if (is(list[0] == UDA)) { + static assert (allow_types, "findNextUDA is designed to look up values, not types"); + enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } else { + static if (is(typeof(list[0]) == UDA)) { + static if (isPropertyGetter!(list[0])) { + enum value = list[0]; + enum extract = UdaSearchResult!(value)(true, index); + } else enum extract = UdaSearchResult!(list[0])(true, index); + } else enum extract = extract!(index + 1, list[1..$]); + } + } + } + + enum findNextUDA = extract!(idx, udaTuple[idx .. $]); +} + + +/// +unittest +{ + struct Attribute { int x; } + + @("something", Attribute(42), Attribute(41)) + void symbol(); + + enum result0 = findNextUDA!(string, symbol, 0); + static assert (result0.found); + static assert (result0.index == 0); + static assert (result0.value == "something"); + + enum result1 = findNextUDA!(Attribute, symbol, 0); + static assert (result1.found); + static assert (result1.index == 1); + static assert (result1.value == Attribute(42)); + + enum result2 = findNextUDA!(int, symbol, 0); + static assert (!result2.found); + + enum result3 = findNextUDA!(Attribute, symbol, result1.index + 1); + static assert (result3.found); + static assert (result3.index == 2); + static assert (result3.value == Attribute(41)); +} + +unittest +{ + struct Attribute { int x; } + + @(Attribute) void symbol(); + + static assert (!is(findNextUDA!(Attribute, symbol, 0))); + + enum result0 = findNextUDA!(Attribute, symbol, 0, true); + static assert (result0.found); + static assert (result0.index == 0); + static assert (is(result0.value == Attribute)); +} + +unittest +{ + struct Attribute { int x; } + enum Dummy; + + @property static Attribute getter() + { + return Attribute(42); + } + + @Dummy @getter void symbol(); + + enum result0 = findNextUDA!(Attribute, symbol, 0); + static assert (result0.found); + static assert (result0.index == 1); + static assert (result0.value == Attribute(42)); +} + +/// Eager version of findNextUDA that represent all instances of UDA in a Tuple. +/// If one of the attribute is a type instead of an instance, compilation will fail. +template UDATuple(alias UDA, alias Sym) { + import std.typetuple : TypeTuple; + + private template extract(size_t maxSize, Founds...) + { + private alias LastFound = Founds[$ - 1]; + // No more to find + static if (!LastFound.found) + enum extract = Founds[0 .. $ - 1]; + else { + // For ease of use, this is a Tuple of UDA, not a tuple of UdaSearchResult!(...) + private alias Result = TypeTuple!(Founds[0 .. $ - 1], LastFound.value); + // We're at the last parameter + static if (LastFound.index == maxSize) + enum extract = Result; + else + enum extract = extract!(maxSize, Result, findNextUDA!(UDA, Sym, LastFound.index + 1)); + } + } + + private enum maxIndex = TypeTuple!(__traits(getAttributes, Sym)).length; + enum UDATuple = extract!(maxIndex, findNextUDA!(UDA, Sym, 0)); +} + +unittest +{ + import std.typetuple : TypeTuple; + + struct Attribute { int x; } + enum Dummy; + + @(Dummy, Attribute(21), Dummy, Attribute(42), Attribute(84)) void symbol() {} + @(Dummy, Attribute(21), Dummy, Attribute(42), Attribute) void wrong() {} + + alias Cmp = TypeTuple!(Attribute(21), Attribute(42), Attribute(84)); + static assert(Cmp == UDATuple!(Attribute, symbol)); + static assert(!is(UDATuple!(Attribute, wrong))); +} + +/// Avoid repeating the same error message again and again. +/// ---- +/// if (!__ctfe) +/// assert(0, onlyAsUda!func); +/// ---- +template onlyAsUda(string from /*= __FUNCTION__*/) +{ + // With default param, DMD think expression is void, even when writing 'enum string onlyAsUda = ...' + enum onlyAsUda = from~" must only be used as an attribute - not called as a runtime function."; +}