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 }