@@ -85,6 +85,7 @@ enum JSONOptions
85
85
escapeNonAsciiChars = 0x2 , // / Encode non-ASCII characters with a Unicode escape sequence
86
86
doNotEscapeSlashes = 0x4 , // / Do not escape slashes ('/')
87
87
strictParsing = 0x8 , // / Strictly follow RFC-8259 grammar when parsing
88
+ preserveObjectOrder = 0x16 , // / Preserve order of object keys when parsing
88
89
}
89
90
90
91
/**
@@ -100,6 +101,7 @@ enum JSONType : byte
100
101
float_, // / ditto
101
102
array, // / ditto
102
103
object, // / ditto
104
+ orderedObject, // / ditto
103
105
true_, // / ditto
104
106
false_, // / ditto
105
107
// FIXME: Find some way to deprecate the enum members below, which does NOT
@@ -126,13 +128,21 @@ struct JSONValue
126
128
{
127
129
import std.exception : enforce;
128
130
131
+ import std.typecons : Tuple ;
132
+
133
+ alias OrderedObjectMember = Tuple ! (
134
+ string , " key" ,
135
+ JSONValue, " value" ,
136
+ );
137
+
129
138
union Store
130
139
{
131
140
string str;
132
141
long integer;
133
142
ulong uinteger;
134
143
double floating;
135
144
JSONValue[string ] object;
145
+ OrderedObjectMember[] orderedObject;
136
146
JSONValue[] array;
137
147
}
138
148
private Store store;
@@ -310,13 +320,58 @@ struct JSONValue
310
320
* ---
311
321
*
312
322
* Throws: `JSONException` for read access if `type` is not
313
- * `JSONType.object`.
323
+ * `JSONType.object` or `JSONType.orderedObject` .
314
324
*/
315
325
@property inout (JSONValue[string ]) objectNoRef() inout pure @trusted
316
326
{
317
- enforce! JSONException(type == JSONType.object,
318
- " JSONValue is not an object" );
319
- return store.object;
327
+ switch (type)
328
+ {
329
+ case JSONType.object:
330
+ return store.object;
331
+ case JSONType.orderedObject:
332
+ JSONValue[string ] result;
333
+ foreach (pair; store.orderedObject)
334
+ result[pair.key] = pair.value;
335
+ return cast (inout )result;
336
+ default :
337
+ throw new JSONException(" JSONValue is not an object or ordered object" );
338
+ }
339
+ }
340
+
341
+ /* **
342
+ * Value getter/setter for `JSONType.orderedObject`.
343
+ * Throws: `JSONException` for read access if `type` is not
344
+ * `JSONType.orderedObject`.
345
+ * Note: This is @system because of the following pattern:
346
+ ---
347
+ auto a = &(json.orderedObject());
348
+ json.uinteger = 0; // overwrite AA pointer
349
+ (*a)["hello"] = "world"; // segmentation fault
350
+ ---
351
+ */
352
+ @property ref inout (OrderedObjectMember[]) orderedObject() inout pure @system return
353
+ {
354
+ enforce! JSONException(type == JSONType.orderedObject,
355
+ " JSONValue is not an orderedObject" );
356
+ return store.orderedObject;
357
+ }
358
+ // / ditto
359
+ @property OrderedObjectMember[] orderedObject(return scope OrderedObjectMember[] v) pure nothrow @nogc @trusted // TODO make @safe
360
+ {
361
+ assign(v);
362
+ return v;
363
+ }
364
+
365
+ /* **
366
+ * Value getter for `JSONType.orderedObject`.
367
+ * Unlike `orderedObject`, this retrieves the object by value
368
+ * and can be used in @safe code.
369
+ */
370
+ @property inout (OrderedObjectMember[]) orderedObjectNoRef() inout pure @trusted
371
+ {
372
+ enforce! JSONException(type == JSONType.orderedObject,
373
+ " JSONValue is not an orderedObject" );
374
+ return store.orderedObject;
320
375
}
321
376
322
377
/* **
@@ -527,6 +582,11 @@ struct JSONValue
527
582
() @trusted { store.object = aa; }();
528
583
}
529
584
}
585
+ else static if (is (T : OrderedObjectMember[]))
586
+ {
587
+ type_tag = JSONType.orderedObject;
588
+ () @trusted { store.orderedObject = arg; }();
589
+ }
530
590
else static if (isArray! T)
531
591
{
532
592
type_tag = JSONType.array;
@@ -629,6 +689,32 @@ struct JSONValue
629
689
assert (obj1 != obj2);
630
690
}
631
691
692
+ /**
693
+ * An enum value that can be used to obtain a `JSONValue` representing
694
+ * an empty JSON object.
695
+ * Unlike `emptyObject`, the order of inserted keys is preserved.
696
+ */
697
+ enum emptyOrderedObject = {
698
+ JSONValue v;
699
+ v.orderedObject = null ;
700
+ return v;
701
+ }();
702
+ // /
703
+ @system unittest
704
+ {
705
+ JSONValue obj = JSONValue.emptyOrderedObject;
706
+ assert (obj.type == JSONType.orderedObject);
707
+ obj[" b" ] = JSONValue(2 );
708
+ obj[" a" ] = JSONValue(1 );
709
+ assert (obj[" a" ] == JSONValue(1 ));
710
+ assert (obj[" b" ] == JSONValue(2 ));
711
+
712
+ string [] keys ;
713
+ foreach (string k, JSONValue v; obj)
714
+ keys ~= k;
715
+ assert (keys == [" b" , " a" ]);
716
+ }
717
+
632
718
/**
633
719
* An enum value that can be used to obtain a `JSONValue` representing
634
720
* an empty JSON array.
@@ -703,21 +789,39 @@ struct JSONValue
703
789
* initializes it with a JSON object and then performs
704
790
* the index assignment.
705
791
*
706
- * Throws: `JSONException` if `type` is not `JSONType.object`
707
- * or `JSONType.null_`.
792
+ * Throws: `JSONException` if `type` is not `JSONType.object`,
793
+ * `JSONType.orderedObject`, or `JSONType.null_`.
708
794
*/
709
795
void opIndexAssign (T)(auto ref T value, string key)
710
796
{
711
- enforce! JSONException(type == JSONType.object || type == JSONType.null_,
712
- " JSONValue must be object or null" );
713
- JSONValue[string ] aa = null ;
714
- if (type == JSONType.object)
797
+ enforce! JSONException(
798
+ type == JSONType.object ||
799
+ type == JSONType.orderedObject ||
800
+ type == JSONType.null_,
801
+ " JSONValue must be object or null" );
802
+ if (type == JSONType.orderedObject)
715
803
{
716
- aa = this .objectNoRef;
804
+ auto arr = this .orderedObjectNoRef;
805
+ foreach (ref pair; arr)
806
+ if (pair.key == key)
807
+ {
808
+ pair.value = value;
809
+ return ;
810
+ }
811
+ arr ~= OrderedObjectMember(key, JSONValue(value));
812
+ this .orderedObject = arr;
717
813
}
814
+ else
815
+ {
816
+ JSONValue[string ] aa = null ;
817
+ if (type == JSONType.object)
818
+ {
819
+ aa = this .objectNoRef;
820
+ }
718
821
719
- aa[key] = value;
720
- this .object = aa;
822
+ aa[key] = value;
823
+ this .object = aa;
824
+ }
721
825
}
722
826
// /
723
827
@safe unittest
@@ -828,6 +932,8 @@ struct JSONValue
828
932
// / ditto
829
933
bool opEquals (ref const JSONValue rhs) const @nogc nothrow pure @trusted
830
934
{
935
+ import std.algorithm.searching : canFind;
936
+
831
937
// Default doesn't work well since store is a union. Compare only
832
938
// what should be in store.
833
939
// This is @trusted to remain nogc, nothrow, fast, and usable from @safe code.
@@ -873,7 +979,40 @@ struct JSONValue
873
979
case JSONType.string :
874
980
return type_tag == rhs.type_tag && store.str == rhs.store.str;
875
981
case JSONType.object:
876
- return type_tag == rhs.type_tag && store.object == rhs.store.object;
982
+ switch (rhs.type_tag)
983
+ {
984
+ case JSONType.object:
985
+ return store.object == rhs.store.object;
986
+ case JSONType.orderedObject:
987
+ if (store.object.length != rhs.store.orderedObject.length)
988
+ return false ;
989
+ foreach (ref pair; rhs.store.orderedObject)
990
+ if (pair.key ! in store.object || store.object[pair.key] != pair.value)
991
+ return false ;
992
+ return true ;
993
+ default :
994
+ return false ;
995
+ }
996
+ case JSONType.orderedObject:
997
+ switch (rhs.type_tag)
998
+ {
999
+ case JSONType.object:
1000
+ if (store.orderedObject.length != rhs.store.object.length)
1001
+ return false ;
1002
+ foreach (ref pair; store.orderedObject)
1003
+ if (pair.key ! in rhs.store.object || rhs.store.object[pair.key] != pair.value)
1004
+ return false ;
1005
+ return true ;
1006
+ case JSONType.orderedObject:
1007
+ if (store.orderedObject.length != rhs.store.orderedObject.length)
1008
+ return false ;
1009
+ foreach (ref pair; store.orderedObject)
1010
+ if (! rhs.store.orderedObject.canFind(pair))
1011
+ return false ;
1012
+ return true ;
1013
+ default :
1014
+ return false ;
1015
+ }
877
1016
case JSONType.array:
878
1017
return type_tag == rhs.type_tag && store.array == rhs.store.array;
879
1018
case JSONType.true_:
@@ -913,18 +1052,35 @@ struct JSONValue
913
1052
// / Implements the foreach `opApply` interface for json objects.
914
1053
int opApply (scope int delegate (string key, ref JSONValue) dg) @system
915
1054
{
916
- enforce! JSONException(type == JSONType.object,
917
- " JSONValue is not an object" );
918
- int result;
919
-
920
- foreach (string key, ref value; object)
1055
+ switch (type)
921
1056
{
922
- result = dg(key, value);
923
- if (result)
924
- break ;
925
- }
1057
+ case JSONType.object:
1058
+ int result;
926
1059
927
- return result;
1060
+ foreach (string key, ref value; object)
1061
+ {
1062
+ result = dg(key, value);
1063
+ if (result)
1064
+ break ;
1065
+ }
1066
+
1067
+ return result;
1068
+
1069
+ case JSONType.orderedObject:
1070
+ int result;
1071
+
1072
+ foreach (ref pair; orderedObject)
1073
+ {
1074
+ result = dg(pair.key, pair.value);
1075
+ if (result)
1076
+ break ;
1077
+ }
1078
+
1079
+ return result;
1080
+
1081
+ default :
1082
+ throw new JSONException(" JSONValue is not an object or orderedObject" );
1083
+ }
928
1084
}
929
1085
930
1086
/* **
@@ -1018,6 +1174,7 @@ if (isSomeFiniteCharInputRange!T)
1018
1174
Nullable! Char next;
1019
1175
int line = 1 , pos = 0 ;
1020
1176
immutable bool strict = (options & JSONOptions.strictParsing) != 0 ;
1177
+ immutable bool ordered = (options & JSONOptions.preserveObjectOrder) != 0 ;
1021
1178
1022
1179
void error (string msg)
1023
1180
{
@@ -1258,31 +1415,62 @@ if (isSomeFiniteCharInputRange!T)
1258
1415
switch (c)
1259
1416
{
1260
1417
case ' {' :
1261
- if (testChar( ' } ' ) )
1418
+ if (ordered )
1262
1419
{
1263
- value.object = null ;
1264
- break ;
1265
- }
1420
+ if (testChar(' }' ))
1421
+ {
1422
+ value.orderedObject = null ;
1423
+ break ;
1424
+ }
1266
1425
1267
- JSONValue[string ] obj;
1268
- do
1426
+ JSONValue.OrderedObjectMember[] obj;
1427
+ do
1428
+ {
1429
+ skipWhitespace();
1430
+ if (! strict && peekChar() == ' }' )
1431
+ {
1432
+ break ;
1433
+ }
1434
+ checkChar(' "' );
1435
+ string name = parseString();
1436
+ checkChar(' :' );
1437
+ JSONValue member;
1438
+ parseValue(member);
1439
+ obj ~= JSONValue.OrderedObjectMember(name, member);
1440
+ }
1441
+ while (testChar(' ,' ));
1442
+ value.orderedObject = obj;
1443
+
1444
+ checkChar(' }' );
1445
+ }
1446
+ else
1269
1447
{
1270
- skipWhitespace();
1271
- if (! strict && peekChar() == ' }' )
1448
+ if (testChar(' }' ))
1272
1449
{
1450
+ value.object = null ;
1273
1451
break ;
1274
1452
}
1275
- checkChar(' "' );
1276
- string name = parseString();
1277
- checkChar(' :' );
1278
- JSONValue member;
1279
- parseValue(member);
1280
- obj[name] = member;
1281
- }
1282
- while (testChar(' ,' ));
1283
- value.object = obj;
1284
1453
1285
- checkChar(' }' );
1454
+ JSONValue[string ] obj;
1455
+ do
1456
+ {
1457
+ skipWhitespace();
1458
+ if (! strict && peekChar() == ' }' )
1459
+ {
1460
+ break ;
1461
+ }
1462
+ checkChar(' "' );
1463
+ string name = parseString();
1464
+ checkChar(' :' );
1465
+ JSONValue member;
1466
+ parseValue(member);
1467
+ obj[name] = member;
1468
+ }
1469
+ while (testChar(' ,' ));
1470
+ value.object = obj;
1471
+
1472
+ checkChar(' }' );
1473
+ }
1286
1474
break ;
1287
1475
1288
1476
case ' [' :
@@ -1684,6 +1872,36 @@ if (isOutputRange!(Out,char))
1684
1872
}
1685
1873
break ;
1686
1874
1875
+ case JSONType.orderedObject:
1876
+ auto obj = value.orderedObjectNoRef;
1877
+ if (! obj.length)
1878
+ {
1879
+ json.put(" {}" );
1880
+ }
1881
+ else
1882
+ {
1883
+ putCharAndEOL(' {' );
1884
+ bool first = true ;
1885
+
1886
+ foreach (pair; obj)
1887
+ {
1888
+ if (! first)
1889
+ putCharAndEOL(' ,' );
1890
+ first = false ;
1891
+ putTabs(1 );
1892
+ toString(pair.key);
1893
+ json.put(' :' );
1894
+ if (pretty)
1895
+ json.put(' ' );
1896
+ toValueImpl(pair.value, indentLevel + 1 );
1897
+ }
1898
+
1899
+ putEOL();
1900
+ putTabs();
1901
+ json.put(' }' );
1902
+ }
1903
+ break ;
1904
+
1687
1905
case JSONType.array:
1688
1906
auto arr = value.arrayNoRef;
1689
1907
if (arr.empty)
@@ -2469,3 +2687,17 @@ pure nothrow @safe unittest
2469
2687
2470
2688
assert (app.data == s, app.data);
2471
2689
}
2690
+
2691
+ // https://issues.dlang.org/show_bug.cgi?id=24823 - JSONOptions.preserveObjectOrder
2692
+ @safe unittest
2693
+ {
2694
+ import std.array : appender;
2695
+
2696
+ string s = ` {"b":2,"a":1}` ;
2697
+ JSONValue j = parseJSON(s, - 1 , JSONOptions.preserveObjectOrder);
2698
+
2699
+ auto app = appender! string ();
2700
+ j.toString(app);
2701
+
2702
+ assert (app.data == s, app.data);
2703
+ }
0 commit comments