1 /* 2 author : ky-anh huynh 3 license : mit 4 date : 2019-07-17 5 purpose : a simple json validator 6 inspired by : https://github.com/rycus86/webhook-proxy/blob/a8919cc82173b8e7a4cb0a2ba8a34a14996e159c/src/endpoints.py#L141 7 */ 8 9 module dusybox.json.validator; 10 import std.json; 11 12 debug import std.stdio; 13 14 auto json_value_as_string(JSONValue value) { 15 if (value.type == JSONType..string) { 16 return value.str; 17 } 18 else { 19 import std.format; 20 return "%s".format(value); 21 } 22 } 23 24 unittest { 25 auto a = parseJSON(`{"foo": "bar"}`); 26 auto b = parseJSON(`{"foo": 1}`); 27 auto c = parseJSON(`{"foo": true}`); 28 auto d = parseJSON(`{"foo": null}`); 29 assert(a["foo"].json_value_as_string == "bar"); 30 assert(b["foo"].json_value_as_string == "1"); 31 assert(c["foo"].json_value_as_string == "true"); 32 assert(d["foo"].json_value_as_string == "null"); 33 } 34 35 bool validate_a_path(string path, JSONValue value, JSONValue rule) { 36 if (value.type == JSONType.object && rule.type == JSONType.object) { 37 if (! validate(rule, value, path)) { 38 debug writefln(":: validate_a_path '%s' with a dict value, using rule %s [FAIL]", path, rule); 39 return false; 40 } 41 } 42 else if (rule.type != JSONType..string) { 43 debug writefln(":: validate_a_path '%s' with value %s, using rule %s (which is not a string) must return false", path, value, rule); 44 return false; 45 } 46 else { 47 import std.regex, std.format; 48 if (! value.json_value_as_string.matchFirst(regex(rule.str, "m"))) { 49 debug writefln(":: validate_a_path '%s' with value %s, using rule %s (regexp) [FAIL]", path, value, rule); 50 return false; 51 } 52 else { 53 debug(3) writefln(":: validate_a_path '%s' with value %s, using rule %s (regexp) [PASS]", path, value, rule); 54 } 55 } 56 57 return true; 58 } 59 60 auto validate(string checks, string payload) { 61 return validate(checks.parseJSON, payload.parseJSON); 62 } 63 64 bool validate(JSONValue checks, JSONValue payload, string path = "root") { 65 bool status = true; 66 67 int walk_through_check(string key, ref JSONValue rule) { 68 int loopflag = 0; 69 70 JSONValue value; 71 if (key in payload) { 72 value = payload[key]; 73 } 74 75 if (value.isNull) { 76 debug writefln(":: validate(%s) key: '%s' not found in payload. Return True", path, key); 77 status = true; 78 } 79 else if (value.type == JSONType.array) { 80 debug writefln(":: validate(%s) payload value is an array %s", path, value); 81 foreach (ulong idx, item; value) { 82 if (! validate_a_path(path, item, rule)) { 83 status = false; 84 return 0; 85 } 86 } 87 } 88 else { 89 import std.format; 90 auto new_path = "%s.%s".format(path, key); 91 if (! validate_a_path(new_path, value, rule)) { 92 status = false; 93 return 0; 94 } 95 } 96 97 return loopflag; 98 } 99 100 checks.opApply(&walk_through_check); 101 debug writefln(":: validate(%s) result: %s", path, status); 102 return status; 103 } 104 105 unittest { 106 auto wrap(string st) { 107 ulong max = 32; 108 import std.regex; 109 auto st2 = st.replaceAll(regex(r"[\r\n]+"), "").replaceAll(regex(r" {2,}"), ""); 110 if (st2.length < max) { 111 max = st2.length; 112 } 113 return st2[00..max]; 114 } 115 116 auto mytest(string st1, string st2) { 117 debug writefln("<< test %s vs input %s", wrap(st1), wrap(st2)); 118 return validate(st1, st2); 119 } 120 121 122 assert(mytest(`{"foo": ".+"}`, `{"foo": true}`)); 123 assert(mytest(`{"foo": ".+"}`, `{"foo": false}`)); 124 assert(mytest(`{"foo": ".+"}`, `{"foo": 0}`)); 125 assert(mytest(`{"foo": ".+"}`, `{"foo": [0,1,"bar"]}`)); 126 assert(!mytest(`{"foo": "[0-9]+"}`, `{"foo": [0,1,"bar"]}`)); 127 assert(mytest(`{"foo": "[0-9]+"}`, `{"foo": [0,1,"2bar"]}`)); 128 assert(!mytest(`{"foo": "[0-9]{2}"}`, `{"foo": [0,1,"2bar"]}`)); 129 130 auto c = ` 131 { 132 "commit": { 133 "id": "^[0-9a-f]{40}", 134 "message": ".+", 135 "author": { 136 "name": ".+" 137 } 138 }, 139 "test_array1": ".+", 140 "test_array2": ".+" 141 } 142 `; 143 144 auto p = 145 ` 146 { 147 "commit": { 148 "id": "2bbb1c00f0700263546314747a3b47076c0fba7b", 149 "author": { 150 "email": "test@example.net", 151 "name": "Some User", 152 "foo": { 153 "bar1": {"test": "foo"}, 154 "bar2": {"test": "bar"} 155 } 156 }, 157 "message": "PID-0000: Minor updates", 158 "url": "https://git.lauxanh.us/tools/foo/commit/2bbb1c00f0700263546314747a3b47076c0fba7b", 159 "timestamp": "2019-06-20T15:56:17Z" 160 }, 161 "test_array1": [ 162 "one", 163 "two", 164 "three" 165 ], 166 "test_array2": [ 167 0, 168 1, 169 "one", 170 "two", 171 "three" 172 ] 173 } 174 `; 175 176 assert(mytest(c,p)); 177 }