1 module served.lsp.jsonops;
2 
3 import std.json : StdJSONValue = JSONValue;
4 
5 public import mir.algebraic_alias.json : JsonValue = JsonAlgebraic, StringMap;
6 
7 deprecated("use serializeJson") StdJSONValue toJSON(T)(T value)
8 {
9 	import std.json : parseJSON;
10 
11 	return parseJSON(serializeJson(value));
12 }
13 
14 deprecated("use deserializeJson") T fromJSON(T)(StdJSONValue value)
15 {
16 	return deserializeJson!T(value.toString);
17 }
18 
19 /++
20 JSON serialization function.
21 +/
22 string serializeJson(T)(auto ref T value)
23 {
24 	import mir.ser.json : serializeJson;
25 
26 	static if (is(T == RootJsonToken))
27 		return value.json;
28 	else
29 		return serializeJson!T(value);
30 }
31 
32 T deserializeJson(T)(scope const(char)[] text)
33 {
34 	import mir.deser.json : deserializeJson;
35 
36 	static if (is(T == RootJsonToken))
37 		return T(text.idup);
38 	else
39 		return deserializeJson!T(text);
40 }
41 
42 /// Type that can be used to pass JSON as-is, without (de)serialization.
43 /// Only works at root level, e.g. can't be nested inside (de)serialized types.
44 struct RootJsonToken
45 {
46 	string json;
47 
48 	void[1] unserializable;
49 }
50 
51 unittest
52 {
53 	string[] tests = [
54 		`{"hello":"world"}`,
55 		`{"a":[1,"world",false,null]}`,
56 		`null`,
57 		`5`,
58 		`"ok"`,
59 		`["ok",[[[[[false]]]]]]`,
60 		`true`,
61 	];
62 
63 	foreach (v; tests)
64 	{
65 		assert(v.deserializeJson!JsonValue.serializeJson == v);
66 	}
67 }
68 
69 T jsonValueTo(T)(scope JsonValue value)
70 {
71 	return value.serializeJson.deserializeJson!T;
72 }
73 
74 JsonValue toJsonValue(StdJSONValue value)
75 {
76 	import std.json : JSONType;
77 
78 	final switch (value.type)
79 	{
80 	case JSONType.object:
81 		StringMap!JsonValue ret;
82 		foreach (key, val; value.object)
83 			ret[key] = val.toJsonValue;
84 		return JsonValue(ret);
85 	case JSONType.array:
86 		JsonValue[] ret = new JsonValue[value.array.length];
87 		foreach (i, val; value.array)
88 			ret[i] = val.toJsonValue;
89 		return JsonValue(ret);
90 	case JSONType.true_:
91 		return JsonValue(true);
92 	case JSONType.false_:
93 		return JsonValue(false);
94 	case JSONType.null_:
95 		return JsonValue(null);
96 	case JSONType..string:
97 		return JsonValue(value.str);
98 	case JSONType.float_:
99 		return JsonValue(value.floating);
100 	case JSONType.integer:
101 		return JsonValue(value.integer);
102 	case JSONType.uinteger:
103 		return JsonValue(value.uinteger);
104 	}
105 }
106 
107 StdJSONValue toStdJSONValue(JsonValue value)
108 {
109 	final switch (value.kind)
110 	{
111 	case JsonValue.Kind.object:
112 		auto obj = value.get!(StringMap!JsonValue);
113 		StdJSONValue[string] ret;
114 		foreach (kv; obj.byKeyValue)
115 			ret[kv.key] = kv.value.toStdJSONValue;
116 		return StdJSONValue(ret);
117 	case JsonValue.Kind.array:
118 		auto arr = value.get!(JsonValue[]);
119 		StdJSONValue[] ret = new StdJSONValue[arr.length];
120 		foreach (i, val; arr)
121 			ret[i] = val.toStdJSONValue;
122 		return StdJSONValue(ret);
123 	case JsonValue.Kind.boolean:
124 		return StdJSONValue(value.get!bool);
125 	case JsonValue.Kind.null_:
126 		return StdJSONValue(null);
127 	case JsonValue.Kind..string:
128 		return StdJSONValue(value.get!string);
129 	case JsonValue.Kind.float_:
130 		return StdJSONValue(value.get!double);
131 	case JsonValue.Kind.integer:
132 		return StdJSONValue(value.get!long);
133 	}
134 }
135 
136 JsonValue toJsonValue(T)(auto ref T value)
137 {
138 	return value.serializeJson.deserializeJson!JsonValue;
139 }
140 
141 private inout(char)[] skipString(ref scope return inout(char)[] jsonObject)
142 in(jsonObject.length)
143 in(jsonObject.ptr[0] == '"')
144 {
145 	auto start = jsonObject.ptr;
146 	jsonObject = jsonObject.ptr[1 .. jsonObject.length];
147 	int escape;
148 	while (jsonObject.length)
149 	{
150 		char c = jsonObject.ptr[0];
151 		if (escape)
152 		{
153 			if (c == 'u')
154 				escape += 4;
155 			escape--;
156 		}
157 		else
158 		{
159 			if (c == '"')
160 				break;
161 			else if (c == '\\')
162 				escape = 1;
163 		}
164 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
165 	}
166 	if (!jsonObject.length || jsonObject.ptr[0] != '"')
167 		throw new Exception("malformed JSON string");
168 	jsonObject = jsonObject.ptr[1 .. jsonObject.length];
169 	return start[0 .. jsonObject.ptr - start];
170 }
171 
172 private inout(char)[] skipNumber(ref scope return inout(char)[] jsonObject)
173 in(jsonObject.length)
174 {
175 	import std.ascii : isDigit;
176 
177 	auto start = jsonObject.ptr;
178 	if (jsonObject.ptr[0] == '-')
179 	{
180 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
181 		if (!jsonObject.length)
182 			throw new Exception("malformed JSON number");
183 	}
184 
185 	while (jsonObject.length && jsonObject.ptr[0].isDigit)
186 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
187 
188 	if (jsonObject.length && jsonObject.ptr[0] == '.')
189 	{
190 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
191 		if (!jsonObject.length)
192 			throw new Exception("malformed JSON number");
193 
194 		while (jsonObject.length && jsonObject.ptr[0].isDigit)
195 			jsonObject = jsonObject.ptr[1 .. jsonObject.length];
196 	}
197 
198 	if (jsonObject.length && (jsonObject.ptr[0] == 'e' || jsonObject.ptr[0] == 'E'))
199 	{
200 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
201 		if (!jsonObject.length)
202 			throw new Exception("malformed JSON number");
203 
204 		if (jsonObject.ptr[0] == '-' || jsonObject.ptr[0] == '+')
205 		{
206 			jsonObject = jsonObject.ptr[1 .. jsonObject.length];
207 			if (!jsonObject.length)
208 				throw new Exception("malformed JSON number");
209 		}
210 
211 		while (jsonObject.length && jsonObject.ptr[0].isDigit)
212 			jsonObject = jsonObject.ptr[1 .. jsonObject.length];
213 	}
214 
215 	return start[0 .. jsonObject.ptr - start];
216 }
217 
218 private inout(char)[] skipLiteral(ref scope return inout(char)[] jsonObject, string literal)
219 {
220 	if (jsonObject.length < literal.length
221 		|| jsonObject.ptr[0 .. literal.length] != literal)
222 		throw new Exception("expected literal '" ~ literal
223 			~ "', but got '" ~ jsonObject.idup ~ "'");
224 
225 	auto ret = jsonObject.ptr[0 .. literal.length];
226 	jsonObject = jsonObject.ptr[literal.length .. jsonObject.length];
227 	return ret;
228 }
229 
230 // skips until matching level of start/end tokens - skips strings
231 private inout(char)[] skipByPairs(char start, char end, string name)(ref scope return inout(char)[] jsonObject)
232 in(jsonObject.length)
233 in(jsonObject.ptr[0] == start)
234 {
235 	auto startPtr = jsonObject.ptr;
236 	int depth = 0;
237 	Loop: do
238 	{
239 		switch (jsonObject.ptr[0])
240 		{
241 		case start:
242 			depth++;
243 			break;
244 		case end:
245 			depth--;
246 			break;
247 		case '"':
248 			jsonObject.skipString();
249 			continue Loop;
250 		default:
251 			break;
252 		}
253 		jsonObject = jsonObject.ptr[1 .. jsonObject.length];
254 	} while (depth && jsonObject.length);
255 
256 	if (depth != 0)
257 		throw new Exception("malformed JSON " ~ name);
258 	return startPtr[0 .. jsonObject.ptr - startPtr];
259 }
260 
261 private inout(char)[] skipObject(ref scope return inout(char)[] jsonObject)
262 {
263 	return jsonObject.skipByPairs!('{', '}', "object");
264 }
265 
266 private inout(char)[] skipArray(ref scope return inout(char)[] jsonObject)
267 {
268 	return jsonObject.skipByPairs!('[', ']', "array");
269 }
270 
271 private inout(char)[] skipValue(ref scope return inout(char)[] jsonObject)
272 {
273 	if (!jsonObject.length)
274 		return null;
275 	switch (jsonObject.ptr[0])
276 	{
277 	case '"': // string
278 		return jsonObject.skipString();
279 	case '-': // number
280 	case '0': .. case '9': // number
281 		return jsonObject.skipNumber();
282 	case 't': // true
283 		return jsonObject.skipLiteral("true");
284 	case 'f': // false
285 		return jsonObject.skipLiteral("false");
286 	case 'n': // null
287 		return jsonObject.skipLiteral("null");
288 	case '{': // object
289 		return jsonObject.skipObject();
290 	case '[': // array
291 		return jsonObject.skipArray();
292 	default:
293 		return null;
294 	}
295 }
296 
297 private void skipWhite(ref scope return inout(char)[] jsonObject)
298 {
299 	import std.string : stripLeft;
300 
301 	jsonObject = jsonObject.stripLeft;
302 }
303 
304 /// Split the json object into keys, store the slices from the input string
305 /// into a newly created struct which contains all the keys from the `fields`
306 /// parameter.
307 ///
308 /// If a paremeter ends with _, the last _ will be removed in the string check,
309 /// so that D reserved keywords can be used.
310 template parseKeySlices(fields...)
311 {
312 	auto parseKeySlices(T)(scope return T[] jsonObject)
313 	if (is(immutable T == immutable char))
314 	in (jsonObject.looksLikeJsonObject)
315 	{
316 		import std.string : representation;
317 		import std.algorithm : canFind;
318 
319 		mixin(`struct Ret {
320 			union {
321 				T[][fields.length] _arr;
322 				struct {
323 					`, (){
324 						string fieldsStr;
325 						foreach (string field; fields)
326 							fieldsStr ~= "T[] " ~ field ~ ";\n";
327 						return fieldsStr;
328 					}(), `
329 				}
330 			}
331 		}`);
332 
333 		Ret ret;
334 
335 		jsonObject = jsonObject[1 .. $ - 1];
336 		jsonObject.skipWhite();
337 		while (jsonObject.length)
338 		{
339 			auto key = jsonObject.skipValue();
340 			if (key.length < 2 || key.ptr[0] != '"' || key.ptr[key.length - 1] != '"')
341 				throw new Exception("malformed JSON object key");
342 			jsonObject.skipWhite();
343 			if (!jsonObject.length || jsonObject.ptr[0] != ':')
344 				throw new Exception("malformed JSON");
345 			jsonObject = jsonObject.ptr[1 .. jsonObject.length];
346 			jsonObject.skipWhite();
347 			auto value = jsonObject.skipValue();
348 			jsonObject.skipWhite();
349 
350 			if (jsonObject.length)
351 			{
352 				if (jsonObject.ptr[0] != ',')
353 					throw new Exception("malformed JSON");
354 				jsonObject = jsonObject.ptr[1 .. jsonObject.length];
355 				jsonObject.skipWhite();
356 			}
357 
358 			RawKeySwitch: switch (key)
359 			{
360 				static foreach (string field; fields)
361 				{
362 				case '"' ~ (field[$ - 1] == '_' ? field[0 .. $ - 1] : field) ~ '"':
363 					mixin("ret.", field, " = value;");
364 					break RawKeySwitch;
365 				}
366 				default:
367 					if (key.representation.canFind('\\'))
368 					{
369 						// wtf escaped key
370 						DeserializedSwitch: switch (key.deserializeJson!string)
371 						{
372 							static foreach (string field; fields)
373 							{
374 							case (field[$ - 1] == '_' ? field[0 .. $ - 1] : field):
375 								mixin("ret.", field, " = value;");
376 								break DeserializedSwitch;
377 							}
378 							default:
379 								break; // not part of wanted keys
380 						}
381 					}
382 					break; // not part of wanted keys
383 			}
384 		}
385 
386 		return ret;
387 	}
388 }
389 
390 ///
391 unittest
392 {
393 	string json = `{
394 		"hello": "ther\"e",
395 		"foo": {"ok":"cool"} ,
396 		"bar": [1, 2.0,3],
397 		"extra": {
398 			"f": [
399 				1 , {
400 					"a": "b",
401 					"c": false
402 				}, {
403 					"f": [1, {
404 						"a": "b",
405 						"c": false
406 					}],
407 					"a": 10000
408 				}
409 			],
410 			"a": 10000
411 		},
412 		"yes": true,
413 		"no": false,
414 		"opt": null
415 	}`;
416 	auto parts = json.parseKeySlices!("hello", "foo", "bar", "extra", "yes", "opt", "notinjson");
417 	assert(!parts.notinjson.length);
418 	assert(parts.hello is json[13 .. 22]);
419 	assert(parts.foo is json[33 .. 46]);
420 	assert(parts.bar is json[58 .. 68]);
421 	assert(parts.extra is json[81 .. 246]);
422 	assert(parts.yes is json[257 .. 261]);
423 	assert(parts.opt is json[287 .. 291]);
424 }
425 
426 void visitJsonArray(alias fn, T)(scope T[] jsonArray)
427 if (is(immutable T == immutable char))
428 in (jsonArray.length)
429 in (jsonArray[0] == '[')
430 in (jsonArray[$ - 1] == ']')
431 {
432 	jsonArray = jsonArray[1 .. $ - 1];
433 	jsonArray.skipWhite();
434 	while (jsonArray.length)
435 	{
436 		auto value = jsonArray.skipValue();
437 		fn(value);
438 		jsonArray.skipWhite();
439 		if (jsonArray.length)
440 		{
441 			if (jsonArray.ptr[0] != ',')
442 				throw new Exception("malformed JSON array");
443 			jsonArray = jsonArray.ptr[1 .. jsonArray.length];
444 			jsonArray.skipWhite();
445 		}
446 	}
447 }
448 
449 unittest
450 {
451 	string json = `["the[r\"e",
452 		{"ok":"cool"} ,
453 		[1, 2.0,3],
454 		{
455 			"f": [
456 				1 , {
457 					"a": "b",
458 					"c": false
459 				}, {
460 					"f": [1, {
461 						"a": "b",
462 						"c": false
463 					}],
464 					"a": 10000
465 				}
466 			],
467 			"a": 10000
468 		},
469 		true,
470 		false,
471 		null
472 	]`;
473 	string[] expected = [
474 		json[1 .. 11],
475 		json[15 .. 28],
476 		json[33 .. 43],
477 		json[47 .. 212],
478 		json[216 .. 220],
479 		json[224 .. 229],
480 		json[233 .. 237],
481 	];
482 	int i;
483 	json.visitJsonArray!((item) {
484 		assert(expected[i++] is item);
485 	});
486 	assert(i == expected.length);
487 }
488 
489 bool looksLikeJsonObject(scope const(char)[] jsonString)
490 {
491 	return jsonString.length >= 2
492 		&& jsonString.ptr[0] == '{'
493 		&& jsonString.ptr[jsonString.length - 1] == '}';
494 }
495 
496 bool looksLikeJsonArray(scope const(char)[] jsonString)
497 {
498 	return jsonString.length >= 2
499 		&& jsonString.ptr[0] == '['
500 		&& jsonString.ptr[jsonString.length - 1] == ']';
501 }
502 
503 bool isValidJsonStringContent(scope const(char)[] jsonString)
504 {
505 	foreach (char c; jsonString)
506 		if (c == '"' || c == '\\' || c < 0x20)
507 			return false;
508 	return true;
509 }
510 
511 bool isEmptyJsonObject(scope const(char)[] jsonString)
512 {
513 	import std.ascii : isWhite;
514 
515 	static immutable string expected = "{}";
516 	int i = 0;
517 	foreach (char c; jsonString)
518 	{
519 		if (c.isWhite)
520 			continue;
521 
522 		if (i >= expected.length || c != expected[i])
523 			return false;
524 		i++;
525 	}
526 	return i == expected.length;
527 }
528 
529 unittest
530 {
531 	assert(isEmptyJsonObject(`{}`));
532 	assert(isEmptyJsonObject(` { } `));
533 	assert(isEmptyJsonObject(` {} `));
534 	assert(isEmptyJsonObject(`{  }`));
535 	assert(!isEmptyJsonObject(``));
536 	assert(!isEmptyJsonObject(`{`));
537 	assert(!isEmptyJsonObject(`}`));
538 	assert(!isEmptyJsonObject(`}{`));
539 	assert(!isEmptyJsonObject(`{]`));
540 }
541 
542 const(inout(char))[] escapeJsonStringContent(scope return inout(char)[] str)
543 {
544 	import std.ascii : hexDigits;
545 	import std.array;
546 
547 	if (isValidJsonStringContent(str))
548 		return str;
549 
550 	auto ret = appender!string;
551 	ret.reserve(str.length);
552 	foreach (char c; str)
553 	{
554 		if (c == '"' || c == '\\')
555 		{
556 			ret.put('\\');
557 			ret.put(c);
558 		}
559 		else if (c < 0x20)
560 		{
561 			if (c < 0x10)
562 				ret.put("\\u000");
563 			else
564 				ret.put("\\u001");
565 			ret.put(hexDigits[c & 0xF]);
566 		}
567 		else
568 		{
569 			ret.put(c);
570 		}
571 	}
572 	return ret.data;
573 }
574 
575 unittest
576 {
577 	string test = "hello";
578 	assert(test.escapeJsonStringContent is test);
579 	test = `hel"lo`;
580 	assert(test.escapeJsonStringContent == `hel\"lo`);
581 
582 	string[] expected = [
583 		`\u0000`,
584 		`\u0001`,
585 		`\u0002`,
586 		`\u0003`,
587 		`\u0004`,
588 		`\u0005`,
589 		`\u0006`,
590 		`\u0007`,
591 		`\u0008`,
592 		`\u0009`,
593 		`\u000A`,
594 		`\u000B`,
595 		`\u000C`,
596 		`\u000D`,
597 		`\u000E`,
598 		`\u000F`,
599 		`\u0010`,
600 		`\u0011`,
601 		`\u0012`,
602 		`\u0013`,
603 		`\u0014`,
604 		`\u0015`,
605 		`\u0016`,
606 		`\u0017`,
607 		`\u0018`,
608 		`\u0019`,
609 		`\u001A`,
610 		`\u001B`,
611 		`\u001C`,
612 		`\u001D`,
613 		`\u001E`,
614 		`\u001F`,
615 	];
616 	foreach (i; 0 .. 0x1F)
617 	{
618 		char[] testStr = [cast(char)i];
619 		assert(testStr.escapeJsonStringContent == expected[i]);
620 	}
621 }