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 }