1 module dshould.json;
2 
3 import dshould.contain;
4 import dshould.ShouldType;
5 import dshould.stringcmp;
6 import dshould.thrown;
7 import std.algorithm;
8 import std.json;
9 import std.range;
10 import std.typecons;
11 
12 /**
13  * Checks if a JSON value contains another JSON value.
14  * Keys that are only in the first JSON value are ignored.
15  *
16  * In other words, every value in the second JSON value must
17  * appear in the same position in the first value.
18  *
19  * This satisfies the JSON convention that extraneous keys are ignored.
20  */
21 public void json(Should)(Should should, const JSONValue expected,
22     Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
23 if (isInstanceOf!(ShouldType, Should))
24 {
25     should.allowOnlyWords!("be", "contain").before!"json";
26 
27     should.terminateChain;
28 
29     with (should)
30     {
31         auto got = should.got();
32 
33         static if (hasWord!"contain")
34         {
35             if (!got.containsJson(expected))
36             {
37                 stringCmpError(got.toPrettyString, expected.toPrettyString, No.quote, file, line);
38             }
39         }
40         else
41         {
42             if (got != expected)
43             {
44                 stringCmpError(got.toPrettyString, expected.toPrettyString, No.quote, file, line);
45             }
46         }
47     }
48 }
49 
50 ///
51 @("should be JSON")
52 unittest
53 {
54     import dshould : be;
55 
56     `{"a": 5, "b": 6}`.parseJSON.should.be.json(`{"a": 5, "b": 6}`.parseJSON);
57     `{"a": 5, "b": 6}`.parseJSON.should.be.json(`{"b": 6, "a": 5}`.parseJSON);
58     `{"a": 5, "b": 6}`.parseJSON.should.be.json(`{"a": 5}`.parseJSON).should.throwAn!Error;
59 }
60 
61 ///
62 @("should contain JSON")
63 unittest
64 {
65     `{"a": 5, "b": 6}`.parseJSON.should.contain.json(`{}`.parseJSON);
66     `{"a": 5, "b": 6}`.parseJSON.should.contain.json(`{"b": 6}`.parseJSON);
67     `{"a": 5, "b": 6}`.parseJSON.should.contain.json(`{"c": 4}`.parseJSON).should.throwAn!Error;
68 
69     `{"x": {"a": 5, "b": 6}}`.parseJSON.should.contain.json(`{"x": {"b": 6}}`.parseJSON);
70     `{"x": {"a": 5, "b": 6}}`.parseJSON.should.contain.json(`{"x": {"c": 4}}`.parseJSON).should.throwAn!Error;
71 
72     `{"a": 5, "b": 6}`.parseJSON.should.contain.json(`{"b": 5}`.parseJSON).should.throwAn!Error;
73 
74     `[2, 3]`.parseJSON.should.contain.json(`[2, 3]`.parseJSON);
75     `[2, 3]`.parseJSON.should.contain.json(`[2]`.parseJSON).should.throwAn!Error;
76 }
77 
78 /// ditto
79 public void json(string jsonString, Should)(Should should,
80     Fence _ = Fence(), string file = __FILE__, size_t line = __LINE__)
81 if (isInstanceOf!(ShouldType, Should))
82 {
83     enum expected = jsonString.parseJSON;
84 
85     return should.json(expected, Fence(), file, line);
86 }
87 
88 ///
89 @("should be JSON")
90 unittest
91 {
92     import dshould : be;
93 
94     `{"a": 5, "b": 6}`.parseJSON.should.be.json!`{"a": 5, "b": 6}`;
95     `{"a": 5, "b": 6}`.parseJSON.should.be.json!`{"b": 6, "a": 5}`;
96     `{"a": 5, "b": 6}`.parseJSON.should.be.json!`{"a": 5}`.should.throwAn!Error;
97 }
98 
99 ///
100 @("should contain JSON literal")
101 unittest
102 {
103     `{"a": 5, "b": 6}`.parseJSON.should.contain.json!`{"a": 5}`;
104 }
105 
106 private bool containsJson(const JSONValue actual, const JSONValue expected) pure
107 {
108     if (actual.type != expected.type)
109     {
110         return false;
111     }
112 
113     const type = actual.type;
114 
115     if (type == JSONType.array)
116     {
117         return (actual.array.length == expected.array.length) &&
118             actual.array.length.iota.all!(i => actual.array[i].containsJson(expected.array[i]));
119     }
120     if (type == JSONType.object)
121     {
122         return expected.object.byKey
123             .all!(key => (key in actual.object) && actual.object[key].containsJson(expected.object[key]));
124     }
125     return actual == expected;
126 }