diff --git a/std/json.d b/std/json.d index 9dcec89b2ce..a1b1e245c2d 100644 --- a/std/json.d +++ b/std/json.d @@ -85,6 +85,7 @@ enum JSONOptions escapeNonAsciiChars = 0x2, /// Encode non-ASCII characters with a Unicode escape sequence doNotEscapeSlashes = 0x4, /// Do not escape slashes ('/') strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing + preserveObjectOrder = 0x16, /// Preserve order of object keys when parsing } /** @@ -126,13 +127,30 @@ struct JSONValue { import std.exception : enforce; + import std.typecons : Tuple; + + alias OrderedObjectMember = Tuple!( + string, "key", + JSONValue, "value", + ); + union Store { + struct Object + { + bool isOrdered; + union + { + JSONValue[string] unordered; + OrderedObjectMember[] ordered; + } + } + string str; long integer; ulong uinteger; double floating; - JSONValue[string] object; + Object object; JSONValue[] array; } private Store store; @@ -272,9 +290,9 @@ struct JSONValue } /*** - * Value getter/setter for `JSONType.object`. + * Value getter/setter for unordered `JSONType.object`. * Throws: `JSONException` for read access if `type` is not - * `JSONType.object`. + * `JSONType.object` or the object is ordered. * Note: This is @system because of the following pattern: --- auto a = &(json.object()); @@ -286,7 +304,9 @@ struct JSONValue { enforce!JSONException(type == JSONType.object, "JSONValue is not an object"); - return store.object; + enforce!JSONException(!store.object.isOrdered, + "JSONValue object is ordered, cannot return by ref"); + return store.object.unordered; } /// ditto @property JSONValue[string] object(return scope JSONValue[string] v) pure nothrow @nogc @trusted // TODO make @safe @@ -296,7 +316,7 @@ struct JSONValue } /*** - * Value getter for `JSONType.object`. + * Value getter for unordered `JSONType.object`. * Unlike `object`, this retrieves the object by value * and can be used in @safe code. * @@ -316,7 +336,71 @@ struct JSONValue { enforce!JSONException(type == JSONType.object, "JSONValue is not an object"); - return store.object; + if (store.object.isOrdered) + { + // Convert to unordered + JSONValue[string] result; + foreach (pair; store.object.ordered) + result[pair.key] = pair.value; + return cast(inout) result; + } + else + return store.object.unordered; + } + + /*** + * Value getter/setter for ordered `JSONType.object`. + * Throws: `JSONException` for read access if `type` is not + * `JSONType.object` or the object is unordered. + * Note: This is @system because of the following pattern: + --- + auto a = &(json.orderedObject()); + json.uinteger = 0; // overwrite AA pointer + (*a)["hello"] = "world"; // segmentation fault + --- + */ + @property ref inout(OrderedObjectMember[]) orderedObject() inout pure @system return + { + enforce!JSONException(type == JSONType.object, + "JSONValue is not an object"); + enforce!JSONException(store.object.isOrdered, + "JSONValue object is unordered, cannot return by ref"); + return store.object.ordered; + } + /// ditto + @property OrderedObjectMember[] orderedObject(return scope OrderedObjectMember[] v) pure nothrow @nogc @trusted // TODO make @safe + { + assign(v); + return v; + } + + /*** + * Value getter for ordered `JSONType.object`. + * Unlike `orderedObject`, this retrieves the object by value + * and can be used in @safe code. + */ + @property inout(OrderedObjectMember[]) orderedObjectNoRef() inout pure @trusted + { + enforce!JSONException(type == JSONType.object, + "JSONValue is not an object"); + if (store.object.isOrdered) + return store.object.ordered; + else + { + // Convert to ordered + OrderedObjectMember[] result; + foreach (key, value; store.object.unordered) + result ~= OrderedObjectMember(key, value); + return cast(inout) result; + } + } + + /// Returns `true` if the order of keys of the represented object is being preserved. + @property bool isOrdered() const pure @trusted + { + enforce!JSONException(type == JSONType.object, + "JSONValue is not an object"); + return store.object.isOrdered; } /*** @@ -517,16 +601,30 @@ struct JSONValue static if (is(Value : JSONValue)) { JSONValue[string] t = arg; - () @trusted { store.object = t; }(); + () @trusted { + store.object.isOrdered = false; + store.object.unordered = t; + }(); } else { JSONValue[string] aa; foreach (key, value; arg) aa[key] = JSONValue(value); - () @trusted { store.object = aa; }(); + () @trusted { + store.object.isOrdered = false; + store.object.unordered = aa; + }(); } } + else static if (is(T : OrderedObjectMember[])) + { + type_tag = JSONType.object; + () @trusted { + store.object.isOrdered = true; + store.object.ordered = arg; + }(); + } else static if (isArray!T) { type_tag = JSONType.array; @@ -629,6 +727,33 @@ struct JSONValue assert(obj1 != obj2); } + /** + * An enum value that can be used to obtain a `JSONValue` representing + * an empty JSON object. + * Unlike `emptyObject`, the order of inserted keys is preserved. + */ + enum emptyOrderedObject = { + JSONValue v; + v.orderedObject = null; + return v; + }(); + /// + @system unittest + { + JSONValue obj = JSONValue.emptyOrderedObject; + assert(obj.type == JSONType.object); + assert(obj.isOrdered); + obj["b"] = JSONValue(2); + obj["a"] = JSONValue(1); + assert(obj["a"] == JSONValue(1)); + assert(obj["b"] == JSONValue(2)); + + string[] keys; + foreach (string k, JSONValue v; obj) + keys ~= k; + assert(keys == ["b", "a"]); + } + /** * An enum value that can be used to obtain a `JSONValue` representing * an empty JSON array. @@ -708,16 +833,33 @@ struct JSONValue */ void opIndexAssign(T)(auto ref T value, string key) { - enforce!JSONException(type == JSONType.object || type == JSONType.null_, - "JSONValue must be object or null"); - JSONValue[string] aa = null; - if (type == JSONType.object) + enforce!JSONException( + type == JSONType.object || + type == JSONType.null_, + "JSONValue must be object or null"); + if (type == JSONType.object && isOrdered) { - aa = this.objectNoRef; + auto arr = this.orderedObjectNoRef; + foreach (ref pair; arr) + if (pair.key == key) + { + pair.value = value; + return; + } + arr ~= OrderedObjectMember(key, JSONValue(value)); + this.orderedObject = arr; } + else + { + JSONValue[string] aa = null; + if (type == JSONType.object) + { + aa = this.objectNoRef; + } - aa[key] = value; - this.object = aa; + aa[key] = value; + this.object = aa; + } } /// @safe unittest @@ -828,6 +970,8 @@ struct JSONValue /// ditto bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted { + import std.algorithm.searching : canFind; + // Default doesn't work well since store is a union. Compare only // what should be in store. // This is @trusted to remain nogc, nothrow, fast, and usable from @safe code. @@ -873,7 +1017,45 @@ struct JSONValue case JSONType.string: return type_tag == rhs.type_tag && store.str == rhs.store.str; case JSONType.object: - return type_tag == rhs.type_tag && store.object == rhs.store.object; + if (rhs.type_tag != JSONType.object) + return false; + if (store.object.isOrdered) + { + if (rhs.store.object.isOrdered) + { + if (store.object.ordered.length != rhs.store.object.ordered.length) + return false; + foreach (ref pair; store.object.ordered) + if (!rhs.store.object.ordered.canFind(pair)) + return false; + return true; + } + else + { + if (store.object.ordered.length != rhs.store.object.unordered.length) + return false; + foreach (ref pair; store.object.ordered) + if (pair.key !in rhs.store.object.unordered || + rhs.store.object.unordered[pair.key] != pair.value) + return false; + return true; + } + } + else + { + if (rhs.store.object.isOrdered) + { + if (store.object.unordered.length != rhs.store.object.ordered.length) + return false; + foreach (ref pair; rhs.store.object.ordered) + if (pair.key !in store.object.unordered || + store.object.unordered[pair.key] != pair.value) + return false; + return true; + } + else + return store.object.unordered == rhs.store.object.unordered; + } case JSONType.array: return type_tag == rhs.type_tag && store.array == rhs.store.array; case JSONType.true_: @@ -914,14 +1096,27 @@ struct JSONValue int opApply(scope int delegate(string key, ref JSONValue) dg) @system { enforce!JSONException(type == JSONType.object, - "JSONValue is not an object"); + "JSONValue is not an object"); + int result; - foreach (string key, ref value; object) + if (isOrdered) { - result = dg(key, value); - if (result) - break; + foreach (ref pair; orderedObject) + { + result = dg(pair.key, pair.value); + if (result) + break; + } + } + else + { + foreach (string key, ref value; object) + { + result = dg(key, value); + if (result) + break; + } } return result; @@ -1018,6 +1213,7 @@ if (isSomeFiniteCharInputRange!T) Nullable!Char next; int line = 1, pos = 0; immutable bool strict = (options & JSONOptions.strictParsing) != 0; + immutable bool ordered = (options & JSONOptions.preserveObjectOrder) != 0; void error(string msg) { @@ -1258,31 +1454,62 @@ if (isSomeFiniteCharInputRange!T) switch (c) { case '{': - if (testChar('}')) + if (ordered) { - value.object = null; - break; - } + if (testChar('}')) + { + value.orderedObject = null; + break; + } - JSONValue[string] obj; - do + JSONValue.OrderedObjectMember[] obj; + do + { + skipWhitespace(); + if (!strict && peekChar() == '}') + { + break; + } + checkChar('"'); + string name = parseString(); + checkChar(':'); + JSONValue member; + parseValue(member); + obj ~= JSONValue.OrderedObjectMember(name, member); + } + while (testChar(',')); + value.orderedObject = obj; + + checkChar('}'); + } + else { - skipWhitespace(); - if (!strict && peekChar() == '}') + if (testChar('}')) { + value.object = null; break; } - checkChar('"'); - string name = parseString(); - checkChar(':'); - JSONValue member; - parseValue(member); - obj[name] = member; - } - while (testChar(',')); - value.object = obj; - checkChar('}'); + JSONValue[string] obj; + do + { + skipWhitespace(); + if (!strict && peekChar() == '}') + { + break; + } + checkChar('"'); + string name = parseString(); + checkChar(':'); + JSONValue member; + parseValue(member); + obj[name] = member; + } + while (testChar(',')); + value.object = obj; + + checkChar('}'); + } break; case '[': @@ -1638,49 +1865,82 @@ if (isOutputRange!(Out,char)) final switch (value.type) { case JSONType.object: - auto obj = value.objectNoRef; - if (!obj.length) + if (value.isOrdered) { - json.put("{}"); - } - else - { - putCharAndEOL('{'); - bool first = true; - - void emit(R)(R names) + auto obj = value.orderedObjectNoRef; + if (!obj.length) { - foreach (name; names) + json.put("{}"); + } + else + { + putCharAndEOL('{'); + bool first = true; + + foreach (pair; obj) { - auto member = obj[name]; if (!first) putCharAndEOL(','); first = false; putTabs(1); - toString(name); + toString(pair.key); json.put(':'); if (pretty) json.put(' '); - toValueImpl(member, indentLevel + 1); + toValueImpl(pair.value, indentLevel + 1); } - } - import std.algorithm.sorting : sort; - // https://issues.dlang.org/show_bug.cgi?id=14439 - // auto names = obj.keys; // aa.keys can't be called in @safe code - auto names = new string[obj.length]; - size_t i = 0; - foreach (k, v; obj) + putEOL(); + putTabs(); + json.put('}'); + } + } + else + { + auto obj = value.objectNoRef; + if (!obj.length) { - names[i] = k; - i++; + json.put("{}"); } - sort(names); - emit(names); + else + { + putCharAndEOL('{'); + bool first = true; - putEOL(); - putTabs(); - json.put('}'); + void emit(R)(R names) + { + foreach (name; names) + { + auto member = obj[name]; + if (!first) + putCharAndEOL(','); + first = false; + putTabs(1); + toString(name); + json.put(':'); + if (pretty) + json.put(' '); + toValueImpl(member, indentLevel + 1); + } + } + + import std.algorithm.sorting : sort; + // https://issues.dlang.org/show_bug.cgi?id=14439 + // auto names = obj.keys; // aa.keys can't be called in @safe code + auto names = new string[obj.length]; + size_t i = 0; + foreach (k, v; obj) + { + names[i] = k; + i++; + } + sort(names); + emit(names); + + putEOL(); + putTabs(); + json.put('}'); + } } break; @@ -2469,3 +2729,17 @@ pure nothrow @safe unittest assert(app.data == s, app.data); } + +// https://issues.dlang.org/show_bug.cgi?id=24823 - JSONOptions.preserveObjectOrder +@safe unittest +{ + import std.array : appender; + + string s = `{"b":2,"a":1}`; + JSONValue j = parseJSON(s, -1, JSONOptions.preserveObjectOrder); + + auto app = appender!string(); + j.toString(app); + + assert(app.data == s, app.data); +}