1 module jsonned;
2 
3 import jsonned.binding;
4 
5 struct JsonneD {
6 	import core.memory : pureFree, pureMalloc;
7 	import std.typecons : nullable, Nullable;
8 	import std.string : toStringz, fromStringz;
9 
10 	JsonnetVm* vm;
11 
12 	@disable this(this);
13 
14 	static JsonneD opCall() {
15 		JsonneD ret;
16 		ret.vm = jsonnet_make();
17 		return ret;
18 	}
19 
20 	~this() {
21 		if(this.vm !is null) {
22 			jsonnet_destroy(this.vm);
23 		}
24 	}
25 
26 	void destroy() {
27 		jsonnet_destroy(this.vm);
28 		this.vm = null;
29 	}
30 
31 	/** Set the maximum stack depth. */
32 	void setMaxStack(uint v) {
33 		jsonnet_max_stack(this.vm, v);
34 	}
35 
36 	/** Set the number of objects required before a garbage collection cycle is allowed. */
37 	void setGCminObjects(uint v) {
38 		jsonnet_gc_min_objects(this.vm, v);
39 	}
40 
41 	/** Run the garbage collector after this amount of growth in the number of objects. */
42 	void setGCgrowthTrigger(double v) {
43 		jsonnet_gc_growth_trigger(this.vm, v);
44 	}
45 
46 	/** Expect a string as output and don't JSON encode it. */
47 	void expectStringOutput(bool v) {
48 		jsonnet_string_output(this.vm, v);
49 	}
50 
51 	/** Clean up a JSON subtree.
52 	 *
53 	 * This is useful if you want to abort with an error mid-way through building a complex value.
54 	 */
55 	void jsonDestroy(JsonnetJsonValue *v) {
56 		jsonnet_json_destroy(this.vm, v);
57 	}
58 
59 	struct JsonnetValue {
60 		struct Payload {
61 			JsonnetJsonValue* value;
62 			JsonneD* vm;
63 			long cnt;
64 		}
65 
66 		Payload* payload;
67 
68 		this(JsonnetJsonValue* value, JsonneD* vm) {
69 			this.payload = cast(Payload*)pureMalloc(Payload.sizeof);
70 			this.payload.cnt = 1;
71 			this.payload.vm = vm;
72 			this.payload.value = value;
73 		}
74 
75 		this(this) {
76 			this.increment();
77 		}
78 
79 		~this() {
80 			this.decrement();
81 		}
82 
83 		private void increment() {
84 			if(this.payload) {
85 				this.payload.cnt++;
86 			}
87 		}
88 
89 		private void decrement() {
90 			if(this.payload) {
91 				this.payload.cnt--;
92 				if(this.payload.cnt <= 0) {
93 					this.payload.vm.jsonDestroy(this.payload.value);
94 					pureFree(this.payload);
95 					this.payload = null;
96 				}
97 			}
98 		}
99 
100 		void opAssign(JsonnetValue other) {
101 			other.increment();
102 			this.decrement();
103 			this.payload = other.payload;
104 		}
105 
106 		/** If the value is a string, return it as UTF8 otherwise return NULL.
107 		 */
108 		Nullable!string extractString() {
109 			const char* str = jsonnet_json_extract_string(this.payload.vm.vm,
110 					this.payload.value);
111 
112 			return str is null
113 				? Nullable!(string).init
114 				: nullable(str.fromStringz().idup);
115 		}
116 
117 		/** If the value is a number, return 1 and store the number in out, otherwise return 0.
118 		 */
119 		Nullable!double extractNumber() {
120 			double o;
121 			bool worked = cast(bool)jsonnet_json_extract_number(
122 					this.payload.vm.vm, this.payload.value, &o);
123 
124 			return worked ? nullable(o) : Nullable!(double).init;
125 		}
126 
127 		/** Return 0 if the value is false, 1 if it is true, and 2 if it is not a bool.
128 		 */
129 		Nullable!bool extractBool() {
130 			int v = jsonnet_json_extract_bool(this.payload.vm.vm,
131 					this.payload.value);
132 			return v == 2 ? Nullable!(bool).init : nullable(cast(bool)v);
133 		}
134 
135 		/** Return 1 if the value is null, else 0.
136 		 */
137 		bool extractNull() {
138 			return cast(bool)jsonnet_json_extract_null(this.payload.vm.vm,
139 					this.payload.value);
140 		}
141 
142 		/** Add v to the end of the array.
143 		* v is no longer valid after the append
144 		*/
145 		void arrayAppend()(auto ref JsonnetValue v) {
146 			jsonnet_json_array_append(this.payload.vm.vm, this.payload.value,
147 					v.payload.value);
148 			pureFree(v.payload);
149 			v.payload = null;
150 		}
151 
152 		/** Add the field f to the object, bound to v.
153 		*
154 		* v is no longer valid after the append
155 		* This replaces any previous binding of the field.
156 		*/
157 		void objectAppend()(string f, auto ref JsonnetValue v) {
158 			jsonnet_json_object_append(this.payload.vm.vm, this.payload.value,
159 					toStringz(f), v.payload.value);
160 			pureFree(v.payload);
161 			v.payload = null;
162 		}
163 	}
164 
165 	/** Convert the given UTF8 string to a JsonnetJsonValue.
166 	 */
167 	JsonnetValue makeString(string v) {
168 		JsonnetJsonValue* t = jsonnet_json_make_string(this.vm, v.toStringz());
169 		return JsonnetValue(t, &this);
170 	}
171 
172 	///
173 	unittest {
174 		string s = "Hello World";
175 		JsonneD jd = JsonneD();
176 		JsonnetValue sv = jd.makeString(s);
177 		Nullable!string ss = sv.extractString();
178 		assert(!ss.isNull());
179 		assert(s == ss);
180 	}
181 
182 	///
183 	unittest {
184 		string s = "Hello World";
185 		JsonneD jd = JsonneD();
186 		JsonnetValue copy;
187 
188 		{
189 			JsonnetValue sv = jd.makeString(s);
190 			copy = sv;
191 		}
192 
193 		Nullable!string ss = copy.extractString();
194 		assert(!ss.isNull());
195 		assert(s == ss);
196 	}
197 
198 	/** Convert the given double to a JsonnetJsonValue.
199 	 */
200 	JsonnetValue makeNumber(double v) {
201 		JsonnetJsonValue* t = jsonnet_json_make_number(this.vm, v);
202 		return JsonnetValue(t, &this);
203 	}
204 
205 	///
206 	unittest {
207 		import std.math : approxEqual;
208 		double s = 13.37;
209 		JsonneD jd = JsonneD();
210 		JsonnetValue sv = jd.makeNumber(s);
211 		Nullable!double ss = sv.extractNumber();
212 		assert(!ss.isNull());
213 		assert(approxEqual(s, ss));
214 	}
215 
216 	/** Convert the given bool (1 or 0) to a JsonnetJsonValue.
217 	 */
218 	JsonnetValue makeBool(bool v) {
219 		JsonnetJsonValue* t = jsonnet_json_make_bool(this.vm, v);
220 		return JsonnetValue(t, &this);
221 	}
222 
223 	///
224 	unittest {
225 		JsonneD jd = JsonneD();
226 		JsonnetValue sv = jd.makeBool(true);
227 		assert(sv.extractBool().get());
228 
229 		sv = jd.makeBool(false);
230 		assert(!sv.extractBool().get());
231 	}
232 
233 	/** Make a JsonnetJsonValue representing null.
234 	 */
235 	JsonnetValue makeNull() {
236 		JsonnetJsonValue* t = jsonnet_json_make_null(this.vm);
237 		return JsonnetValue(t, &this);
238 	}
239 
240 	///
241 	unittest {
242 		JsonneD jd = JsonneD();
243 		JsonnetValue sv = jd.makeNull();
244 		assert( sv.extractBool().isNull);
245 		assert( sv.extractString().isNull);
246 		assert(sv.extractNull());
247 	}
248 
249 	/** Make a JsonnetJsonValue representing an array.
250 	 *
251 	 * Assign elements with jsonnet_json_array_append.
252 	 */
253 	JsonnetValue makeArray() {
254 		JsonnetJsonValue* t = jsonnet_json_make_array(this.vm);
255 		return JsonnetValue(t, &this);
256 	}
257 
258 	///
259 	unittest {
260 		JsonneD jd = JsonneD();
261 		JsonnetValue arr = jd.makeArray();
262 
263 		auto one = jd.makeNumber(1.0);
264 		auto two = jd.makeNumber(2.0);
265 		arr.arrayAppend(one);
266 		arr.arrayAppend(two);
267 	}
268 
269 	/** Make a JsonnetJsonValue representing an object with the given number of
270 	 * fields.
271 	 *
272 	 * Every index of the array must have a unique value assigned with
273 	 * jsonnet_json_array_element.
274 	 */
275 	JsonnetValue makeObject() {
276 		JsonnetJsonValue* t = jsonnet_json_make_object(this.vm);
277 		return JsonnetValue(t, &this);
278 	}
279 
280 	///
281 	unittest {
282 		JsonneD jd = JsonneD();
283 		JsonnetValue obj = jd.makeObject();
284 		obj.objectAppend("foo", jd.makeString("Hello"));
285 		obj.objectAppend("bar", jd.makeString("World"));
286 		obj.objectAppend("args", jd.makeNumber(13.37));
287 	}
288 
289 	/** Override the callback used to locate imports.
290 	 */
291 	void importCallback(JsonnetImportCallback cb, void* ctx) {
292 		jsonnet_import_callback(this.vm, cb, ctx);
293 	}
294 
295 	/** Register a native extension.
296 	 *
297 	 * This will appear in Jsonnet as a function type and can be accessed from std.nativeExt("foo").
298 	 *
299 	 * DO NOT register native callbacks with side-effects!  Jsonnet is a lazy functional language and
300 	 * will call your function when you least expect it, more times than you expect, or not at all.
301 	 *
302 	 * \param vm The vm.
303 	 * \param name The name of the function as visible to Jsonnet code, e.g. "foo".
304 	 * \param cb The PURE function that implements the behavior you want.
305 	 * \param ctx User pointer, stash non-global state you need here.
306 	 * \param params NULL-terminated array of the names of the params.  Must be valid identifiers.
307 	 */
308 	void nativeCallback(string name, JsonnetNativeCallback cb,
309 			void* ctx, const(char)** params)
310 	{
311 		jsonnet_native_callback(this.vm, cast(const(char)*)toStringz(name), cb,
312 				ctx, params);
313 	}
314 
315 	/** Bind a Jsonnet external var to the given string.
316 	 *
317 	 * Argument values are copied so memory should be managed by caller.
318 	 */
319 	void extVar(string key, string val) {
320 		jsonnet_ext_var(this.vm, toStringz(key), toStringz(val));
321 	}
322 
323 	/** Bind a Jsonnet external var to the given code.
324 	 *
325 	 * Argument values are copied so memory should be managed by caller.
326 	 */
327 	void extCode(string key, string val) {
328 		jsonnet_ext_code(this.vm, toStringz(key), toStringz(val));
329 	}
330 
331 	/** Bind a string top-level argument for a top-level parameter.
332 	 *
333 	 * Argument values are copied so memory should be managed by caller.
334 	 */
335 	void tlaVar(string key, string val) {
336 		jsonnet_tla_var(this.vm, toStringz(key), toStringz(val));
337 	}
338 
339 	/** Bind a code top-level argument for a top-level parameter.
340 	 *
341 	 * Argument values are copied so memory should be managed by caller.
342 	 */
343 	void tlaCode(string key, string val) {
344 		jsonnet_tla_code(this.vm, toStringz(key), toStringz(val));
345 	}
346 
347 	/** Set the number of lines of stack trace to display (0 for all of them). */
348 	void maxTrace(uint v) {
349 		jsonnet_max_trace(this.vm, v);
350 	}
351 
352 	/** Add to the default import callback's library search path.
353 	 *
354 	 * The search order is last to first, so more recently appended paths take precedence.
355 	 */
356 	void jpathAdd(string v) {
357 		jsonnet_jpath_add(this.vm, toStringz(v));
358 	}
359 
360 	///
361 	struct JsonnetResult {
362 		Nullable!string rslt;
363 		string error;
364 	}
365 
366 	/** Evaluate a file containing Jsonnet code, return a JSON string.
367 	 *
368 	 * The returned string should be cleaned up with jsonnet_realloc.
369 	 *
370 	 * \param filename Path to a file containing Jsonnet code.
371 	 * \param error Return by reference whether or not there was an error.
372 	 * \returns Either JSON or the error message.
373 	 */
374 	JsonnetResult evaluateFile(string filename) {
375 		int error;
376 		char* rslt = jsonnet_evaluate_file(this.vm, toStringz(filename), &error);
377 		return evalImpl(rslt, error);
378 	}
379 
380 	/** Evaluate a string containing Jsonnet code, return a JSON string.
381 	 *
382 	 * The returned string should be cleaned up with jsonnet_realloc.
383 	 *
384 	 * \param filename Path to a file (used in error messages).
385 	 * \param snippet Jsonnet code to execute.
386 	 * \param error Return by reference whether or not there was an error.
387 	 * \returns Either JSON or the error message.
388 	 */
389 	JsonnetResult evaluateSnippet(string filename, string snippet) {
390 		int error;
391 		char* rslt = jsonnet_evaluate_snippet(this.vm, toStringz(filename),
392 				toStringz(snippet), &error);
393 		return evalImpl(rslt, error);
394 	}
395 
396 	JsonnetResult evalImpl(char* rslt, int error) {
397 		scope(exit) {
398 			jsonnet_realloc(this.vm, rslt, 0);
399 		}
400 		JsonnetResult ret;
401 		if(error) {
402 			ret.error = fromStringz(rslt).idup;
403 		} else {
404 			ret.rslt = nullable(fromStringz(rslt).idup);
405 		}
406 		return ret;
407 	}
408 
409 	///
410 	struct JsonnetInterleafedResult {
411 		string filename;
412 		JsonnetResult json;
413 	}
414 
415 	/** Evaluate a file containing Jsonnet code, return a number of named JSON files.
416 	 *
417 	 * The returned character buffer contains an even number of strings, the filename and JSON for each
418 	 * JSON file interleaved.  It should be cleaned up with jsonnet_realloc.
419 	 *
420 	 * \param filename Path to a file containing Jsonnet code.
421 	 * \param error Return by reference whether or not there was an error.
422 	 * \returns Either the error, or a sequence of strings separated by \0, terminated with \0\0.
423 	 */
424 	JsonnetInterleafedResult[] evaluteFileMulti(string filename) {
425 		int error;
426 		char* rslt = jsonnet_evaluate_file_multi(this.vm, toStringz(filename),
427 				&error);
428 		return evalImplMulti(rslt, error);
429 	}
430 
431 	JsonnetInterleafedResult[] evalImplMulti(char* rslt, int error) {
432 		import std.array : array;
433 		import std.algorithm : startsWith;
434 		import std.exception : assumeUnique, enforce;
435 		import std.regex : regex, splitter, isRegexFor, Regex;
436 		import std.range : evenChunks, ElementEncodingType;
437 		import std.typecons : Yes;
438 		import std.traits : Unqual;
439 
440 		void addPair(ref JsonnetInterleafedResult[] target, char* data,
441 				size_t[2] idx, size_t i)
442 		{
443 			JsonnetResult e;
444 			e.rslt = nullable(data[idx[1] .. i].idup);
445 			JsonnetInterleafedResult t;
446 			t.json = e;
447 			t.filename = data[idx[0] .. idx[1] - 1].idup;
448 			target ~= t;
449 		}
450 
451 		scope(exit) {
452 			jsonnet_realloc(this.vm, rslt, 0);
453 		}
454 
455 		JsonnetInterleafedResult[] ret;
456 		if(error) {
457 			JsonnetResult e;
458 			e.error = fromStringz(rslt).idup;
459 			JsonnetInterleafedResult t;
460 			t.json = e;
461 			ret ~= t;
462 		} else {
463 			size_t[2] s;
464 			int idx;
465 			size_t i;
466 			for(i = 0; !rslt[i .. i + 2].startsWith("\0\0"); ++i) {
467 				if(rslt[i] == '\0') {
468 					if(idx == 0) {
469 						s[1] = i + 1;
470 					} else if(idx == 1) {
471 						addPair(ret, rslt, s, i);
472 
473 						s[0] = i + 1;
474 					}
475 					idx = (idx + 1) % 2;
476 				}
477 			}
478 			if(idx == 1) {
479 				addPair(ret, rslt, s, i);
480 			}
481 		}
482 		return ret;
483 	}
484 
485 	/** Evaluate a string containing Jsonnet code, return a number of named JSON files.
486 	 *
487 	 * The returned character buffer contains an even number of strings, the filename and JSON for each
488 	 * JSON file interleaved.  It should be cleaned up with jsonnet_realloc.
489 	 *
490 	 * \param filename Path to a file containing Jsonnet code.
491 	 * \param snippet Jsonnet code to execute.
492 	 * \param error Return by reference whether or not there was an error.
493 	 * \returns Either the error, or a sequence of strings separated by \0, terminated with \0\0.
494 	 */
495 	JsonnetInterleafedResult[] evaluteSnippetMulti(string filename,
496 			string snippet)
497 	{
498 		int error;
499 		char* rslt = jsonnet_evaluate_snippet_multi(this.vm, toStringz(filename),
500 				toStringz(snippet), &error);
501 		return evalImplMulti(rslt, error);
502 	}
503 
504 	/** Evaluate a file containing Jsonnet code, return a number of JSON files.
505 	 *
506 	 * The returned character buffer contains several strings.  It should be cleaned up with
507 	 * jsonnet_realloc.
508 	 *
509 	 * \param filename Path to a file containing Jsonnet code.
510 	 * \param error Return by reference whether or not there was an error.
511 	 * \returns Either the error, or a sequence of strings separated by \0, terminated with \0\0.
512 	 */
513 	JsonnetResult evaluateFileString(string filename) {
514 		int error;
515 		char* rslt = jsonnet_evaluate_file_stream(this.vm, toStringz(filename),
516 				&error);
517 		return evalImpl(rslt, error);
518 	}
519 
520 	/** Evaluate a string containing Jsonnet code, return a number of JSON files.
521 	 *
522 	 * The returned character buffer contains several strings.  It should be cleaned up with
523 	 * jsonnet_realloc.
524 	 *
525 	 * \param filename Path to a file containing Jsonnet code.
526 	 * \param snippet Jsonnet code to execute.
527 	 * \param error Return by reference whether or not there was an error.
528 	 * \returns Either the error, or a sequence of strings separated by \0, terminated with \0\0.
529 	 */
530 	JsonnetInterleafedResult[] evaluateSnippetStream(string filename,
531 			string snippet)
532 	{
533 		int error;
534 		char* rslt = jsonnet_evaluate_snippet_stream(this.vm,
535 				toStringz(filename), toStringz(snippet), &error);
536 		return evalImplMulti(rslt, error);
537 	}
538 }
539 
540 import std.stdio;
541 import std.array : replace;
542 import std.format : format;
543 
544 ///
545 unittest {
546 	JsonneD jn = JsonneD();
547 }
548 
549 ///
550 unittest {
551 	JsonneD jn = JsonneD();
552 	auto r = jn.evaluateFile("tests/a0.jsonnet");
553 }
554 
555 ///
556 unittest {
557 	import std.json;
558 	JsonneD jn = JsonneD();
559 	string s = `
560 	{
561 		person1: {
562 			name: "Alice",
563 			welcome: "Hello " + self.name + "!",
564 		},
565 		person2: self.person1 {
566 			name: std.extVar("OTHER_NAME"),
567 		},
568 	}`;
569 	jn.extVar("OTHER_NAME", "Robert Schadek");
570 
571 	auto eval = jn.evaluateSnippet("foo.json", s);
572 	JSONValue exp = parseJSON(`
573 	{
574 		"person1": {
575 			"name": "Alice",
576 			"welcome": "Hello Alice!"
577 		},
578 		"person2": {
579 			"name": "Robert Schadek",
580 			"welcome" : "Hello Robert Schadek!"
581 		}
582 	}`);
583 
584 	assert(!eval.rslt.isNull);
585 	JSONValue r = parseJSON(eval.rslt.get());
586 
587 	assert(exp == r, format("\nexp:\n%s\neva:\n%s", exp.toPrettyString(),
588 				r.toPrettyString()));
589 }
590 
591 ///
592 unittest {
593 	import std.json;
594 	JsonneD jn = JsonneD();
595 	auto rs = jn.evaluteFileMulti("tests/m0.jsonnnet");
596 	assert(rs.length == 2, format("%s", rs.length));
597 	assert(rs[0].filename == "a.json");
598 	assert(rs[1].filename == "b.json");
599 }
600 
601 ///
602 unittest {
603 	import std.json;
604 	string s = `
605 {
606   "a.json": {
607     x: 1,
608     y: $["b.json"].y,
609   },
610   "b.json": {
611     x: $["a.json"].x,
612     y: 2,
613   },
614 }
615 	`;
616 	JsonneD jn = JsonneD();
617 
618 	auto rs = jn.evaluteSnippetMulti("foo.json", s);
619 	assert(rs.length == 2, format("%s", rs.length));
620 	assert(rs[0].filename == "a.json");
621 	assert(rs[1].filename == "b.json");
622 }