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 }