From 56ab331664711a3322e1ea54db449410ff1fd3ae Mon Sep 17 00:00:00 2001 From: Dmitry Zakharov Date: Fri, 8 Dec 2023 22:18:09 +0400 Subject: [PATCH] Experiment with fast json stringify --- packages/tests/src/benchmark/Benchmark.bs.mjs | 55 +- packages/tests/src/benchmark/Benchmark.res | 23 + packages/tests/src/core/S_jsonString_test.res | 34 +- .../tests/src/core/S_literal_Array_test.res | 4 +- .../tests/src/core/S_literal_Boolean_test.res | 4 +- packages/tests/src/utils/U.bs.mjs | 1 + packages/tests/src/utils/U.res | 2 +- src/S_Core.bs.mjs | 334 ++++++++++--- src/S_Core.res | 468 +++++++++++++----- 9 files changed, 730 insertions(+), 195 deletions(-) diff --git a/packages/tests/src/benchmark/Benchmark.bs.mjs b/packages/tests/src/benchmark/Benchmark.bs.mjs index 7683767e..0b311dd1 100644 --- a/packages/tests/src/benchmark/Benchmark.bs.mjs +++ b/packages/tests/src/benchmark/Benchmark.bs.mjs @@ -114,37 +114,54 @@ S$RescriptSchema.serializeWith(data, schema); console.timeEnd("s: 3"); -run(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(new (Benchmark.default.Suite)(), "Parse string", (function () { - return function () { - return S$RescriptSchema.parseAnyOrRaiseWith("Hello world!", S$RescriptSchema.string); - }; - })), "Serialize string", (function () { - return function () { - return S$RescriptSchema.serializeOrRaiseWith("Hello world!", S$RescriptSchema.string); - }; - })).add("Advanced object schema factory", makeAdvancedObjectSchema), "Parse advanced object", (function () { +run(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(addWithPrepare(new (Benchmark.default.Suite)(), "Parse string", (function () { + return function () { + return S$RescriptSchema.parseAnyOrRaiseWith("Hello world!", S$RescriptSchema.string); + }; + })), "Serialize string", (function () { + return function () { + return S$RescriptSchema.serializeOrRaiseWith("Hello world!", S$RescriptSchema.string); + }; + })).add("Advanced object schema factory", makeAdvancedObjectSchema), "Parse advanced object", (function () { + var schema = makeAdvancedObjectSchema(); + var data = makeTestObject(); + return function () { + return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + }; + })), "Create and parse advanced object", (function () { + var data = makeTestObject(); + return function () { + var schema = makeAdvancedObjectSchema(); + return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + }; + })), "Parse advanced strict object", (function () { + var schema = makeAdvancedStrictObjectSchema(); + var data = makeTestObject(); + return function () { + return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + }; + })), "Serialize advanced object", (function () { var schema = makeAdvancedObjectSchema(); var data = makeTestObject(); return function () { - return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + return S$RescriptSchema.serializeOrRaiseWith(data, schema); }; - })), "Create and parse advanced object", (function () { + })), "Stringify with JSON.stringify", (function () { var data = makeTestObject(); return function () { - var schema = makeAdvancedObjectSchema(); - return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + return JSON.stringify(data); }; - })), "Parse advanced strict object", (function () { - var schema = makeAdvancedStrictObjectSchema(); + })), "Stringify with S.serializeToJsonStringWith", (function () { var data = makeTestObject(); + var schema = makeAdvancedObjectSchema(); return function () { - return S$RescriptSchema.parseAnyOrRaiseWith(data, schema); + return S$RescriptSchema.serializeToJsonStringWith(data, schema, undefined); }; - })), "Serialize advanced object", (function () { - var schema = makeAdvancedObjectSchema(); + })), "Stringify with S.jsonString", (function () { var data = makeTestObject(); + var schema = S$RescriptSchema.jsonString(makeAdvancedObjectSchema(), undefined); return function () { - return S$RescriptSchema.serializeOrRaiseWith(data, schema); + return S$RescriptSchema.serializeToUnknownWith(data, schema); }; }))); diff --git a/packages/tests/src/benchmark/Benchmark.res b/packages/tests/src/benchmark/Benchmark.res index 9a08de90..495bf636 100644 --- a/packages/tests/src/benchmark/Benchmark.res +++ b/packages/tests/src/benchmark/Benchmark.res @@ -166,4 +166,27 @@ Suite.make() data->S.serializeOrRaiseWith(schema) } }) +// V6.2 x 279,905 ops/sec +->Suite.addWithPrepare("Stringify with JSON.stringify", () => { + let data = makeTestObject() + () => { + data->Js.Json.stringifyAny + } +}) +// V6.2 x 278,250 ops/sec +->Suite.addWithPrepare("Stringify with S.serializeToJsonStringWith", () => { + let data = makeTestObject() + let schema = makeAdvancedObjectSchema() + () => { + data->S.serializeToJsonStringWith(schema) + } +}) +// V6.2 x 277,401 ops/sec +->Suite.addWithPrepare("Stringify with S.jsonString", () => { + let data = makeTestObject() + let schema = S.jsonString(makeAdvancedObjectSchema()) + () => { + data->S.serializeToUnknownWith(schema) + } +}) ->Suite.run diff --git a/packages/tests/src/core/S_jsonString_test.res b/packages/tests/src/core/S_jsonString_test.res index 9740a442..6893c40d 100644 --- a/packages/tests/src/core/S_jsonString_test.res +++ b/packages/tests/src/core/S_jsonString_test.res @@ -7,7 +7,7 @@ test("Successfully parses JSON", t => { t->Assert.deepEqual(`"Foo"`->S.parseAnyWith(S.jsonString(schema)), Ok("Foo"), ()) }) -test("Successfully serializes JSON", t => { +test("Successfully serializes string to JSON", t => { let schema = S.string t->Assert.deepEqual( @@ -17,6 +17,36 @@ test("Successfully serializes JSON", t => { ) }) +test("Successfully serializes string literal to JSON", t => { + let schema = S.literal("foo") + + t->Assert.deepEqual( + `foo`->S.serializeToUnknownWith(S.jsonString(schema)), + Ok(%raw(`'"foo"'`)), + (), + ) +}) + +test("Successfully serializes float to JSON", t => { + let schema = S.float + + t->Assert.deepEqual( + 123.4->S.serializeToUnknownWith(S.jsonString(schema)), + Ok(%raw(`'123.4'`)), + (), + ) +}) + +test("Successfully serializes tuple", t => { + let schema = S.tuple2(S.int, S.string) + + t->Assert.deepEqual( + (12, "foo")->S.serializeToUnknownWith(S.jsonString(schema)), + Ok(%raw(`'[12,"foo"]'`)), + (), + ) +}) + test("Successfully serializes JSON object", t => { let schema = S.schema(_ => { @@ -88,7 +118,7 @@ test("Compiled async parse code snapshot", t => { test("Compiled serialize code snapshot", t => { let schema = S.jsonString(S.bool) - t->U.assertCompiledCode(~schema, ~op=#serialize, `i=>{return JSON.stringify(i)}`) + t->U.assertCompiledCode(~schema, ~op=#serialize, `i=>{return i.toString()}`) }) test("Compiled serialize code snapshot with space", t => { diff --git a/packages/tests/src/core/S_literal_Array_test.res b/packages/tests/src/core/S_literal_Array_test.res index fa5bbf2f..6970dcb7 100644 --- a/packages/tests/src/core/S_literal_Array_test.res +++ b/packages/tests/src/core/S_literal_Array_test.res @@ -99,7 +99,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#parse, - `i=>{(i===e[0]||Array.isArray(i)&&i.length===2&&i[0]===e[1]&&i[1]===e[2])||e[3](i);return i}`, + `i=>{(i===e[0]||Array.isArray(i)&&i.length===2&&i[0]===e[1]&&i[1]===true)||e[2](i);return i}`, ) }) @@ -109,7 +109,7 @@ module Common = { t->U.assertCompiledCode( ~schema, ~op=#serialize, - `i=>{(i===e[0]||Array.isArray(i)&&i.length===2&&i[0]===e[1]&&i[1]===e[2])||e[3](i);return i}`, + `i=>{(i===e[0]||Array.isArray(i)&&i.length===2&&i[0]===e[1]&&i[1]===true)||e[2](i);return i}`, ) }) } diff --git a/packages/tests/src/core/S_literal_Boolean_test.res b/packages/tests/src/core/S_literal_Boolean_test.res index c7070eec..1fc818c1 100644 --- a/packages/tests/src/core/S_literal_Boolean_test.res +++ b/packages/tests/src/core/S_literal_Boolean_test.res @@ -71,12 +71,12 @@ module Common = { test("Compiled parse code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#parse, `i=>{i===e[0]||e[1](i);return i}`) + t->U.assertCompiledCode(~schema, ~op=#parse, `i=>{i===false||e[0](i);return i}`) }) test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#serialize, `i=>{i===e[0]||e[1](i);return i}`) + t->U.assertCompiledCode(~schema, ~op=#serialize, `i=>{i===false||e[0](i);return i}`) }) } diff --git a/packages/tests/src/utils/U.bs.mjs b/packages/tests/src/utils/U.bs.mjs index e15c5036..95c1ba24 100644 --- a/packages/tests/src/utils/U.bs.mjs +++ b/packages/tests/src/utils/U.bs.mjs @@ -46,6 +46,7 @@ function cleanUpSchema(schema) { switch (key) { case "f" : case "i" : + case "j" : case "n" : case "op" : case "opa" : diff --git a/packages/tests/src/utils/U.res b/packages/tests/src/utils/U.res index a15c8903..2d410f31 100644 --- a/packages/tests/src/utils/U.res +++ b/packages/tests/src/utils/U.res @@ -36,7 +36,7 @@ let rec cleanUpSchema = schema => { ->Dict.toArray ->Array.forEach(((key, value)) => { switch key { - | "s" | "p" | "i" | "f" | "n" | "os" | "op" | "opa" => () + | "s" | "p" | "i" | "f" | "n" | "os" | "op" | "opa" | "j" => () | _ => if typeof(value) === #object && value !== %raw(`null`) { new->Dict.set( diff --git a/src/S_Core.bs.mjs b/src/S_Core.bs.mjs index 89abd9e4..5418fb68 100644 --- a/src/S_Core.bs.mjs +++ b/src/S_Core.bs.mjs @@ -486,6 +486,196 @@ function build(builder, schema, operation) { return new Function("e", "s", "return " + inlinedFunction)(b.e, symbol); } +var undefined_value = undefined; + +function undefined_checkBuilder(param, param$1, inputVar) { + return inputVar + "===void 0"; +} + +var $$undefined = { + public: "Undefined", + value: undefined_value, + checkBuilder: undefined_checkBuilder +}; + +var null_value = null; + +function null_checkBuilder(param, param$1, inputVar) { + return inputVar + "===null"; +} + +var $$null = { + public: "Null", + value: null_value, + checkBuilder: null_checkBuilder +}; + +var nan_value = NaN; + +function nan_checkBuilder(param, param$1, inputVar) { + return "Number.isNaN(" + inputVar + ")"; +} + +var nan = { + public: "NaN", + value: nan_value, + checkBuilder: nan_checkBuilder +}; + +function strictEqualCheckBuilder(b, value, inputVar) { + return inputVar + "===" + ("e[" + (b.e.push(value) - 1) + "]"); +} + +function parse(value) { + var typeOfValue = typeof value; + if (typeOfValue === "symbol") { + return { + public: { + TAG: "Symbol", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder + }; + } else if (typeOfValue === "boolean") { + var inlined = value ? "true" : "false"; + return { + public: { + TAG: "Boolean", + _0: value + }, + value: value, + checkBuilder: (function (param, param$1, inputVar) { + return inputVar + "===" + inlined; + }), + jsonString: inlined + }; + } else if (typeOfValue === "string") { + return { + public: { + TAG: "String", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder, + jsonString: JSON.stringify(JSON.stringify(value)) + }; + } else if (typeOfValue === "function") { + return { + public: { + TAG: "Function", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder + }; + } else if (typeOfValue === "object") { + if (value === null) { + return $$null; + } else if (Array.isArray(value)) { + var items = []; + var publicItems = []; + var maybeJsonString = "["; + for(var idx = 0 ,idx_finish = value.length; idx < idx_finish; ++idx){ + var itemValue = value[idx]; + var literal = parse(itemValue); + var match = maybeJsonString; + var match$1 = literal.jsonString; + maybeJsonString = match !== undefined && match$1 !== undefined ? match + ( + idx === 0 ? "" : "," + ) + match$1 : undefined; + items.push(literal); + publicItems.push(literal.public); + } + var jsonString = maybeJsonString; + return { + public: { + TAG: "Array", + _0: publicItems + }, + value: value, + checkBuilder: (function (b, value, inputVar) { + return "(" + inputVar + "===" + ("e[" + (b.e.push(value) - 1) + "]") + "||Array.isArray(" + inputVar + ")&&" + inputVar + ".length===" + items.length + ( + items.length > 0 ? "&&" + items.map(function (literal, idx) { + return literal.checkBuilder(b, literal.value, inputVar + "[" + idx + "]"); + }).join("&&") : "" + ) + ")"; + }), + jsonString: jsonString !== undefined ? jsonString + "]" : undefined + }; + } else if (value.constructor === Object) { + var items$1 = {}; + var publicItems$1 = {}; + var maybeJsonString$1 = "{"; + var fields = Object.keys(value); + var numberOfFields = fields.length; + for(var idx$1 = 0; idx$1 < numberOfFields; ++idx$1){ + var field = fields[idx$1]; + var itemValue$1 = value[field]; + var literal$1 = parse(itemValue$1); + var match$2 = maybeJsonString$1; + var match$3 = literal$1.jsonString; + maybeJsonString$1 = match$2 !== undefined && match$3 !== undefined ? match$2 + ( + idx$1 === 0 ? "" : "," + ) + JSON.stringify(field) + ":" + match$3 : undefined; + items$1[field] = literal$1; + publicItems$1[field] = literal$1.public; + } + var jsonString$1 = maybeJsonString$1; + return { + public: { + TAG: "Dict", + _0: publicItems$1 + }, + value: value, + checkBuilder: (function (b, value, inputVar) { + return "(" + inputVar + "===" + ("e[" + (b.e.push(value) - 1) + "]") + "||" + inputVar + "&&" + inputVar + ".constructor===Object&&Object.keys(" + inputVar + ").length===" + numberOfFields + ( + numberOfFields > 0 ? "&&" + fields.map(function (field) { + var literal = items$1[field]; + return literal.checkBuilder(b, literal.value, inputVar + "[" + JSON.stringify(field) + "]"); + }).join("&&") : "" + ) + ")"; + }), + jsonString: jsonString$1 !== undefined ? jsonString$1 + "}" : undefined + }; + } else { + return { + public: { + TAG: "Object", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder + }; + } + } else if (typeOfValue === "undefined") { + return $$undefined; + } else if (typeOfValue === "number") { + if (Number.isNaN(value)) { + return nan; + } else { + return { + public: { + TAG: "Number", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder, + jsonString: value.toString() + }; + } + } else { + return { + public: { + TAG: "BigInt", + _0: value + }, + value: value, + checkBuilder: strictEqualCheckBuilder + }; + } +} + function loop(_schema) { while(true) { var schema = _schema; @@ -975,6 +1165,7 @@ function setName(schema, name) { }), p: schema.p, s: schema.s, + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1010,6 +1201,7 @@ function internalRefine(schema, refiner) { return inputVar; })), path); }), + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1065,6 +1257,7 @@ function transform$1(schema, transformer) { return use(b, schema, input, path); } }), + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1131,7 +1324,6 @@ function preprocess(schema, transformer) { return input$1; } }), - f: undefined, i: 0, m: schema.m }; @@ -1175,66 +1367,39 @@ function custom(name, definer) { return input; } }), - f: undefined, i: 0, m: empty }; } -function literalCheckBuilder(b, value, inputVar) { - if (Number.isNaN(value)) { - return "Number.isNaN(" + inputVar + ")"; - } - if (value === null) { - return inputVar + "===null"; - } - if (value === (void 0)) { - return inputVar + "===void 0"; - } - var check = inputVar + "===" + ("e[" + (b.e.push(value) - 1) + "]"); - if (Array.isArray(value)) { - return "(" + check + "||Array.isArray(" + inputVar + ")&&" + inputVar + ".length===" + value.length + ( - value.length > 0 ? "&&" + value.map(function (item, idx) { - return literalCheckBuilder(b, item, inputVar + "[" + idx + "]"); - }).join("&&") : "" - ) + ")"; - } - if (!(value&&value.constructor===Object)) { - return check; - } - var keys = Object.keys(value); - var numberOfKeys = keys.length; - return "(" + check + "||" + inputVar + "&&" + inputVar + ".constructor===Object&&Object.keys(" + inputVar + ").length===" + numberOfKeys + ( - numberOfKeys > 0 ? "&&" + keys.map(function (key) { - return literalCheckBuilder(b, value[key], inputVar + "[" + JSON.stringify(key) + "]"); - }).join("&&") : "" - ) + ")"; -} - function literal(value) { - var literal$1 = classify(value); + var literal$1 = parse(value); + var publicLiteral = literal$1.public; var operationBuilder = function (b, param, path) { var inputVar = toVar(b, b.i); - b.c = b.c + (literalCheckBuilder(b, value, inputVar) + "||" + raiseWithArg(b, path, (function (input) { + b.c = b.c + (literal$1.checkBuilder(b, value, inputVar) + "||" + raiseWithArg(b, path, (function (input) { return { TAG: "InvalidLiteral", - expected: literal$1, + expected: publicLiteral, received: input }; }), inputVar) + ";"); return inputVar; }; + var jsonString = literal$1.jsonString; return { t: { TAG: "Literal", - _0: literal$1 + _0: publicLiteral }, n: (function () { - return "Literal(" + toText(literal$1) + ")"; + return "Literal(" + toText(publicLiteral) + ")"; }), p: operationBuilder, s: operationBuilder, - f: undefined, + j: jsonString !== undefined ? (function (param) { + return jsonString; + }) : undefined, i: 0, m: empty }; @@ -1316,6 +1481,7 @@ function factory(schema, definer) { var value$1 = value(literal); return use(b, schema, "e[" + (b.e.push(value$1) - 1) + "]", path); }), + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1426,6 +1592,9 @@ function factory$2(schema) { n: containerName, p: parseOperationBuilder, s: serializeOperationBuilder, + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), f: maybeTypeFilter(schema, "null"), i: 0, m: empty @@ -1657,6 +1826,16 @@ function factory$3(definer) { } return "{" + fieldsCodeRef.contents + "}"; }), + j: (function (input) { + var jsonStringRef = "'{"; + for(var idx = 0 ,idx_finish = itemDefinitions.length; idx < idx_finish; ++idx){ + var itemDefinition = itemDefinitions[idx]; + jsonStringRef = jsonStringRef + ( + idx === 0 ? "" : "," + ) + itemDefinition.l + ":'+" + itemDefinition.s.j(input + "[" + itemDefinition.l + "]") + "+'"; + } + return jsonStringRef + "}'"; + }), f: typeFilter, i: 0, m: empty @@ -1678,6 +1857,7 @@ function strip(schema) { n: schema.n, p: schema.p, s: schema.s, + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1700,6 +1880,7 @@ function strict(schema) { n: schema.n, p: schema.p, s: schema.s, + j: schema.j, f: schema.f, i: 0, m: schema.m @@ -1724,7 +1905,9 @@ var schema = { n: primitiveName, p: builder, s: builder, - f: undefined, + j: (function (param) { + return "null"; + }), i: 0, m: empty }; @@ -1734,7 +1917,6 @@ var schema$1 = { n: primitiveName, p: noop, s: noop, - f: undefined, i: false, m: empty }; @@ -1767,6 +1949,9 @@ var schema$2 = { n: primitiveName, p: noop, s: noop, + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), f: typeFilter$1, i: 0, m: empty @@ -1901,12 +2086,11 @@ function trim(schema) { function factory$4(schema, spaceOpt) { var space = spaceOpt !== undefined ? spaceOpt : 0; - try { - validateJsonableSchema(schema, schema, true); - } - catch (raw_exn){ - var exn = Caml_js_exceptions.internalToOCamlException(raw_exn); - getOrRethrow(exn); + var v = schema.j; + var toJsonString; + if (v !== undefined) { + toJsonString = v; + } else { var message = "The schema " + schema.n() + " passed to S.jsonString is not compatible with JSON"; throw new Error("[rescript-schema] " + message); } @@ -1926,9 +2110,11 @@ function factory$4(schema, spaceOpt) { }), s: (function (b, param, path) { var input = b.i; - return "JSON.stringify(" + use(b, schema, input, path) + ( - space > 0 ? ",null," + space : "" - ) + ")"; + if (space !== 0) { + return "JSON.stringify(" + use(b, schema, input, path) + ",null," + space + ")"; + } else { + return toJsonString(input); + } }), f: typeFilter$1, i: 0, @@ -1945,6 +2131,9 @@ var schema$3 = { n: primitiveName, p: noop, s: noop, + j: (function (input) { + return "(" + input + "?\"true\":\"false\")"; + }), f: typeFilter$2, i: 0, m: empty @@ -1970,6 +2159,9 @@ var schema$4 = { n: primitiveName, p: noop, s: noop, + j: (function (input) { + return input + ".toString()"; + }), f: typeFilter$3, i: 0, m: empty @@ -2031,6 +2223,9 @@ var schema$5 = { n: primitiveName, p: noop, s: noop, + j: (function (input) { + return input + ".toString()"; + }), f: typeFilter$4, i: 0, m: empty @@ -2117,6 +2312,9 @@ function factory$5(schema) { })) + "}"); return outputVar; }), + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), f: typeFilter$5, i: 0, m: empty @@ -2206,6 +2404,9 @@ function factory$6(schema) { })) + "}"); return outputVar; }), + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), f: typeFilter, i: 0, m: empty @@ -2245,8 +2446,15 @@ function factory$7(definer) { var itemDefinitionsSet$1 = itemDefinitionsSet; var schemas$1 = schemas; var length = schemas$1.length; + var isJsonableRef = true; for(var idx = 0; idx < length; ++idx){ - if (!schemas$1[idx]) { + var schema = schemas$1[idx]; + if (schema) { + if (schema.j === undefined) { + isJsonableRef = false; + } + + } else { var inlinedInputLocation = "\"" + idx + "\""; var itemDefinition_p = "[" + inlinedInputLocation + "]"; var itemDefinition = { @@ -2256,8 +2464,8 @@ function factory$7(definer) { }; schemas$1[idx] = unit; itemDefinitionsSet$1.add(itemDefinition); + isJsonableRef = false; } - } var itemDefinitions = Array.from(itemDefinitionsSet$1); return { @@ -2334,6 +2542,16 @@ function factory$7(definer) { } return outputVar; }), + j: isJsonableRef ? (function (input) { + var jsonStringRef = "'['+"; + for(var idx = 0 ,idx_finish = schemas$1.length; idx < idx_finish; ++idx){ + var schema = schemas$1[idx]; + jsonStringRef = jsonStringRef + ( + idx === 0 ? "" : "+','+" + ) + schema.j(input + "[" + idx + "]"); + } + return jsonStringRef + "+']'"; + }) : undefined, f: typeFilter$5, i: 0, m: empty @@ -2449,7 +2667,9 @@ function factory$8(schemas) { }), "[" + errorVarsRef + "]") + codeEndRef; return outputVar; }), - f: undefined, + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), i: 0, m: empty }; @@ -2516,7 +2736,9 @@ var json = { return "e[" + (b.e.push(parse) - 1) + "](" + input + ")"; }), s: noop, - f: undefined, + j: (function (input) { + return "JSON.stringify(" + input + ")"; + }), i: 0, m: empty }; @@ -2550,7 +2772,7 @@ function $$catch(schema, getFallbackValue) { })); }), s: schema.s, - f: undefined, + j: schema.j, i: 0, m: schema.m }; @@ -3216,7 +3438,7 @@ var dict = factory$6; var option = factory$1; -var $$null = factory$2; +var $$null$1 = factory$2; var jsonString = factory$4; @@ -3322,7 +3544,7 @@ export { list , dict , option , - $$null , + $$null$1 as $$null, jsonString , union , $$catch , diff --git a/src/S_Core.res b/src/S_Core.res index 85edd420..2cddcb3a 100644 --- a/src/S_Core.res +++ b/src/S_Core.res @@ -260,6 +260,7 @@ module Literal = { | NaN => `NaN` | Undefined => `undefined` | Null => `null` + // FIXME: Why isn't it a string??? | Number(v) => v->Float.unsafeToString | Boolean(v) => v->Bool.unsafeToString | BigInt(v) => v->BigInt.unsafeToString @@ -335,8 +336,10 @@ type rec t<'value> = { mutable parseOperationBuilder: builder, @as("s") mutable serializeOperationBuilder: builder, + @as("j") + maybeToJsonString?: string => string, @as("f") - maybeTypeFilter: option<(~inputVar: string) => string>, + maybeTypeFilter?: (~inputVar: string) => string, @as("i") mutable isAsyncParse: isAsyncParse, @as("m") @@ -777,6 +780,210 @@ module Builder = { // TODO: Split validation code and transformation code module B = Builder.Ctx +module InternalLiteral = { + open Stdlib + + type rec t = { + public: Literal.t, + value: unknown, + checkBuilder: (B.t, ~value: unknown, ~inputVar: string) => string, + jsonString?: string, + } + + let undefined = { + public: Undefined, + value: %raw(`undefined`), + checkBuilder: (_, ~value as _, ~inputVar) => `${inputVar}===void 0`, + } + + let null = { + public: Null, + value: %raw(`null`), + checkBuilder: (_, ~value as _, ~inputVar) => `${inputVar}===null`, + } + + let nan = { + public: NaN, + value: %raw(`NaN`), + checkBuilder: (_, ~value as _, ~inputVar) => `Number.isNaN(${inputVar})`, + } + + let strictEqualCheckBuilder = (b, ~value, ~inputVar) => `${inputVar}===${b->B.embed(value)}` + + let string = value => { + { + public: String(value), + value: value->castAnyToUnknown, + jsonString: Stdlib.Inlined.Value.fromString(Stdlib.Inlined.Value.fromString(value)), + checkBuilder: strictEqualCheckBuilder, + } + } + + let boolean = value => { + let inlined = value ? "true" : "false" + { + public: Boolean(value), + value: value->castAnyToUnknown, + jsonString: inlined, + checkBuilder: (_, ~value as _, ~inputVar) => `${inputVar}===${inlined}`, + } + } + + let number = value => { + { + public: Number(value), + value: value->castAnyToUnknown, + jsonString: value->Js.Float.toString, + checkBuilder: strictEqualCheckBuilder, + } + } + + let symbol = value => { + { + public: Symbol(value), + value: value->castAnyToUnknown, + checkBuilder: strictEqualCheckBuilder, + } + } + + let bigint = value => { + { + public: BigInt(value), + value: value->castAnyToUnknown, + checkBuilder: strictEqualCheckBuilder, + } + } + + let function = value => { + { + public: Function(value), + value: value->castAnyToUnknown, + checkBuilder: strictEqualCheckBuilder, + } + } + + let object = value => { + { + public: Object(value), + value: value->castAnyToUnknown, + checkBuilder: strictEqualCheckBuilder, + } + } + + let rec parse = (value): t => { + let value = value->castAnyToUnknown + let typeOfValue = value->Type.typeof + switch typeOfValue { + | #undefined => undefined + | #object if value === %raw(`null`) => null + | #object if value->Stdlib.Array.isArray => array(value->(Obj.magic: unknown => array)) + | #object + if (value->(Obj.magic: 'a => {"constructor": unknown}))["constructor"] === %raw("Object") => + dict(value->(Obj.magic: unknown => Js.Dict.t)) + | #object => object(value->(Obj.magic: unknown => Js.Types.obj_val)) + | #function => function(value->(Obj.magic: unknown => Js.Types.function_val)) + | #string => string(value->(Obj.magic: unknown => string)) + | #number if value->(Obj.magic: unknown => float)->Js.Float.isNaN => nan + | #number => number(value->(Obj.magic: unknown => float)) + | #boolean => boolean(value->(Obj.magic: unknown => bool)) + | #symbol => symbol(value->(Obj.magic: unknown => Js.Types.symbol)) + | #bigint => bigint(value->(Obj.magic: unknown => Js.Types.bigint_val)) + } + } + and dict = value => { + let items = Js.Dict.empty() + let publicItems = Js.Dict.empty() + let maybeJsonString = ref(Some("{")) + let fields = value->Js.Dict.keys + let numberOfFields = fields->Js.Array2.length + for idx in 0 to numberOfFields - 1 { + let field = fields->Js.Array2.unsafe_get(idx) + let itemValue = value->Js.Dict.unsafeGet(field) + let literal = itemValue->parse + maybeJsonString.contents = switch (maybeJsonString.contents, literal.jsonString) { + | (Some(jsonString), Some(itemJsonString)) => + Some( + jsonString ++ + (idx === 0 ? "" : ",") ++ + field->Stdlib.Inlined.Value.fromString ++ + ":" ++ + itemJsonString, + ) + | _ => None + } + items->Js.Dict.set(field, literal) + publicItems->Js.Dict.set(field, literal.public) + } + + { + public: Dict(publicItems), + value: value->castAnyToUnknown, + jsonString: ?switch maybeJsonString.contents { + | Some(jsonString) => Some(jsonString ++ "}") + | None => None + }, + checkBuilder: (b, ~value, ~inputVar) => + `(${inputVar}===${b->B.embed( + value, + )}||${inputVar}&&${inputVar}.constructor===Object&&Object.keys(${inputVar}).length===${numberOfFields->Stdlib.Int.unsafeToString}` ++ + (numberOfFields > 0 + ? "&&" ++ + fields + ->Js.Array2.map(field => { + let literal = items->Js.Dict.unsafeGet(field) + b->literal.checkBuilder( + ~value=literal.value, + ~inputVar=`${inputVar}[${field->Stdlib.Inlined.Value.fromString}]`, + ) + }) + ->Js.Array2.joinWith("&&") + : "") ++ ")", + } + } + and array = value => { + let items = [] + let publicItems = [] + let maybeJsonString = ref(Some("[")) + for idx in 0 to value->Js.Array2.length - 1 { + let itemValue = value->Js.Array2.unsafe_get(idx) + let literal = itemValue->parse + maybeJsonString.contents = switch (maybeJsonString.contents, literal.jsonString) { + | (Some(jsonString), Some(itemJsonString)) => + Some(jsonString ++ (idx === 0 ? "" : ",") ++ itemJsonString) + | _ => None + } + items->Js.Array2.push(literal)->ignore + publicItems->Js.Array2.push(literal.public)->ignore + } + + { + public: Array(publicItems), + value: value->castAnyToUnknown, + jsonString: ?switch maybeJsonString.contents { + | Some(jsonString) => Some(jsonString ++ "]") + | None => None + }, + checkBuilder: (b, ~value, ~inputVar) => + `(${inputVar}===${b->B.embed( + value, + )}||Array.isArray(${inputVar})&&${inputVar}.length===${items + ->Js.Array2.length + ->Stdlib.Int.unsafeToString}` ++ + (items->Js.Array2.length > 0 + ? "&&" ++ + items + ->Js.Array2.mapi((literal, idx) => + b->literal.checkBuilder( + ~value=literal.value, + ~inputVar=`${inputVar}[${idx->Stdlib.Int.unsafeToString}]`, + ) + ) + ->Js.Array2.joinWith("&&") + : "") ++ ")", + } + } +} + let toLiteral = { let rec loop = schema => { switch schema->classify { @@ -825,6 +1032,7 @@ let isAsyncParse = schema => { } } +// FIXME: recursive ? let rec validateJsonableSchema = (schema, ~rootSchema, ~isRoot=false) => { if isRoot || rootSchema !== schema { switch schema->classify { @@ -832,12 +1040,18 @@ let rec validateJsonableSchema = (schema, ~rootSchema, ~isRoot=false) => { | Int | Float | Bool - | Never - | JSON => () + | // FIXME: + Never + | // FIXME: + JSON => () + // FIXME: | Dict(schema) - | Null(schema) - | Array(schema) => + | // FIXME: + Null(schema) + | // FIXME: + Array(schema) => schema->validateJsonableSchema(~rootSchema) + // FIXME: | Object({fieldNames, fields}) => for idx in 0 to fieldNames->Js.Array2.length - 1 { let fieldName = fieldNames->Js.Array2.unsafe_get(idx) @@ -858,10 +1072,11 @@ let rec validateJsonableSchema = (schema, ~rootSchema, ~isRoot=false) => { try { schema->validateJsonableSchema(~rootSchema) } catch { - // TODO: Should throw with the nested schema instead of prepending path? + // FIXME: Should throw with the nested schema instead of prepending path? | exn => exn->InternalError.prependLocationOrRethrow(i->Js.Int.toString) } }) + // FIXME: | Union(childrenSchemas) => childrenSchemas->Js.Array2.forEach(schema => schema->validateJsonableSchema(~rootSchema)) | Literal(l) if l->Literal.isJsonable => () @@ -881,12 +1096,14 @@ let make = ( ~parseOperationBuilder, ~serializeOperationBuilder, ~maybeTypeFilter, + ~toJsonString=?, ) => { tagged, parseOperationBuilder, serializeOperationBuilder, isAsyncParse: Unknown, - maybeTypeFilter, + ?maybeTypeFilter, + maybeToJsonString: ?toJsonString, name, metadataMap, } @@ -898,13 +1115,15 @@ let makeWithNoopSerializer = ( ~metadataMap, ~parseOperationBuilder, ~maybeTypeFilter, + ~toJsonString=?, ) => { name, tagged, parseOperationBuilder, serializeOperationBuilder: Builder.noop, + ?maybeTypeFilter, + maybeToJsonString: ?toJsonString, isAsyncParse: Unknown, - maybeTypeFilter, metadataMap, } @@ -1180,6 +1399,7 @@ let setName = (schema, name) => { ~serializeOperationBuilder=schema.serializeOperationBuilder, ~tagged=schema.tagged, ~maybeTypeFilter=schema.maybeTypeFilter, + ~toJsonString=?schema.maybeToJsonString, ~metadataMap=schema.metadataMap, ) } @@ -1219,6 +1439,7 @@ let internalRefine = (schema, refiner) => { ) }), ~maybeTypeFilter=schema.maybeTypeFilter, + ~toJsonString=?schema.maybeToJsonString, ~metadataMap=schema.metadataMap, ) } @@ -1287,6 +1508,7 @@ let transform: ( } }), ~maybeTypeFilter=schema.maybeTypeFilter, + ~toJsonString=?schema.maybeToJsonString, ~metadataMap=schema.metadataMap, ) } @@ -1407,64 +1629,18 @@ let custom = (name, definer) => { ) } -let rec literalCheckBuilder = (b, ~value, ~inputVar) => { - if value->castUnknownToAny->Js.Float.isNaN { - `Number.isNaN(${inputVar})` - } else if value === %raw(`null`) { - `${inputVar}===null` - } else if value === %raw(`void 0`) { - `${inputVar}===void 0` - } else { - let check = `${inputVar}===${b->B.embed(value)}` - if value->Stdlib.Array.isArray { - let value = value->(Obj.magic: unknown => array) - `(${check}||Array.isArray(${inputVar})&&${inputVar}.length===${value - ->Js.Array2.length - ->Stdlib.Int.unsafeToString}` ++ - (value->Js.Array2.length > 0 - ? "&&" ++ - value - ->Js.Array2.mapi((item, idx) => - b->literalCheckBuilder( - ~value=item, - ~inputVar=`${inputVar}[${idx->Stdlib.Int.unsafeToString}]`, - ) - ) - ->Js.Array2.joinWith("&&") - : "") ++ ")" - } else if %raw(`value&&value.constructor===Object`) { - let value = value->(Obj.magic: unknown => Js.Dict.t) - let keys = value->Js.Dict.keys - let numberOfKeys = keys->Js.Array2.length - `(${check}||${inputVar}&&${inputVar}.constructor===Object&&Object.keys(${inputVar}).length===${numberOfKeys->Stdlib.Int.unsafeToString}` ++ - (numberOfKeys > 0 - ? "&&" ++ - keys - ->Js.Array2.map(key => { - b->literalCheckBuilder( - ~value=value->Js.Dict.unsafeGet(key), - ~inputVar=`${inputVar}[${key->Stdlib.Inlined.Value.fromString}]`, - ) - }) - ->Js.Array2.joinWith("&&") - : "") ++ ")" - } else { - check - } - } -} - let literal = value => { let value = value->castAnyToUnknown - let literal = value->Literal.classify + let literal = value->InternalLiteral.parse + let publicLiteral = literal.public let operationBuilder = Builder.make((b, ~selfSchema as _, ~path) => { let inputVar = b->B.useInputVar b.code = b.code ++ - `${b->literalCheckBuilder(~value, ~inputVar)}||${b->B.raiseWithArg( + `${b->literal.checkBuilder(~value, ~inputVar)}||${b->B.raiseWithArg( ~path, input => InvalidLiteral({ - expected: literal, + expected: publicLiteral, received: input, }), inputVar, @@ -1472,11 +1648,15 @@ let literal = value => { inputVar }) make( - ~name=() => `Literal(${literal->Literal.toText})`, + ~name=() => `Literal(${publicLiteral->Literal.toText})`, ~metadataMap=Metadata.Map.empty, - ~tagged=Literal(literal), + ~tagged=Literal(publicLiteral), ~parseOperationBuilder=operationBuilder, ~serializeOperationBuilder=operationBuilder, + ~toJsonString=?switch literal.jsonString { + | Some(jsonString) => Some(_ => jsonString) + | None => None + }, ~maybeTypeFilter=None, ) } @@ -1607,6 +1787,7 @@ module Variant = { } }), ~maybeTypeFilter=schema.maybeTypeFilter, + ~toJsonString=?schema.maybeToJsonString, ~metadataMap=schema.metadataMap, ) } @@ -1727,6 +1908,8 @@ module Null = { ~parseOperationBuilder=Option.parseOperationBuilder, ~serializeOperationBuilder=Option.serializeOperationBuilder, ~maybeTypeFilter=Option.maybeTypeFilter(~schema, ~inlinedNoneValue="null"), + //FIXME: + ~toJsonString=input => `JSON.stringify(${input})`, ) } } @@ -2074,6 +2257,22 @@ module Object = { `{${fieldsCodeRef.contents}}` }), ~maybeTypeFilter=Some(typeFilter), + ~toJsonString=input => { + let jsonStringRef = ref(`'{`) + for idx in 0 to itemDefinitions->Js.Array2.length - 1 { + let itemDefinition = itemDefinitions->Js.Array2.unsafe_get(idx) + jsonStringRef.contents = + jsonStringRef.contents ++ + (idx === 0 ? `` : `,`) ++ + itemDefinition.inlinedInputLocation ++ + `:` ++ + `'+` ++ + (itemDefinition.schema.maybeToJsonString->(Obj.magic: option<'a> => 'a))( + `${input}[${itemDefinition.inlinedInputLocation}]`, + ) ++ `+'` + } + jsonStringRef.contents ++ `}'` + }, ) } @@ -2087,6 +2286,7 @@ module Object = { ~serializeOperationBuilder=schema.serializeOperationBuilder, ~maybeTypeFilter=schema.maybeTypeFilter, ~metadataMap=schema.metadataMap, + ~toJsonString=?schema.maybeToJsonString, ) | _ => schema } @@ -2102,6 +2302,7 @@ module Object = { ~serializeOperationBuilder=schema.serializeOperationBuilder, ~maybeTypeFilter=schema.maybeTypeFilter, ~metadataMap=schema.metadataMap, + ~toJsonString=?schema.maybeToJsonString, ) // TODO: Should it throw for non Object schemas? | _ => schema @@ -2132,6 +2333,7 @@ module Never = { ~parseOperationBuilder=builder, ~serializeOperationBuilder=builder, ~maybeTypeFilter=None, + ~toJsonString=_ => `null`, ) } @@ -2143,7 +2345,6 @@ module Unknown = { serializeOperationBuilder: Builder.noop, isAsyncParse: Value(false), metadataMap: Metadata.Map.empty, - maybeTypeFilter: None, } } @@ -2192,6 +2393,7 @@ module String = { ~tagged=String, ~parseOperationBuilder=Builder.noop, ~maybeTypeFilter=Some(typeFilter), + ~toJsonString=input => `JSON.stringify(${input})`, ) let min = (schema, length, ~message as maybeMessage=?) => { @@ -2348,15 +2550,12 @@ module String = { module JsonString = { let factory = (schema, ~space=0) => { let schema = schema->toUnknown - try { - schema->validateJsonableSchema(~rootSchema=schema, ~isRoot=true) - } catch { - | exn => { - let _ = exn->InternalError.getOrRethrow - InternalError.panic( - `The schema ${schema.name()} passed to S.jsonString is not compatible with JSON`, - ) - } + let toJsonString = switch schema.maybeToJsonString { + | Some(v) => v + | None => + InternalError.panic( + `The schema ${schema.name()} passed to S.jsonString is not compatible with JSON`, + ) } make( ~name=primitiveName, @@ -2377,9 +2576,15 @@ module JsonString = { }), ~serializeOperationBuilder=Builder.make((b, ~selfSchema as _, ~path) => { let input = b->B.useInput - `JSON.stringify(${b->B.use(~schema, ~input, ~path)}${space > 0 - ? `,null,${space->Stdlib.Int.unsafeToString}` - : ""})` + switch space { + | 0 => toJsonString(input) + | _ => + `JSON.stringify(${b->B.use( + ~schema, + ~input, + ~path, + )},null,${space->Stdlib.Int.unsafeToString})` + } }), ~maybeTypeFilter=Some(String.typeFilter), ) @@ -2395,6 +2600,7 @@ module Bool = { ~tagged=Bool, ~parseOperationBuilder=Builder.noop, ~maybeTypeFilter=Some(typeFilter), + ~toJsonString=input => `(${input}?"true":"false")`, ) } @@ -2431,6 +2637,7 @@ module Int = { ~tagged=Int, ~parseOperationBuilder=Builder.noop, ~maybeTypeFilter=Some(typeFilter), + ~toJsonString=input => `${input}.toString()`, ) let min = (schema, minValue, ~message as maybeMessage=?) => { @@ -2512,6 +2719,7 @@ module Float = { ~tagged=Float, ~parseOperationBuilder=Builder.noop, ~maybeTypeFilter=Some(typeFilter), + ~toJsonString=input => `${input}.toString()`, ) let min = (schema, minValue, ~message as maybeMessage=?) => { @@ -2636,6 +2844,8 @@ module Array = { } }), ~maybeTypeFilter=Some(typeFilter), + //FIXME: + ~toJsonString=input => `JSON.stringify(${input})`, ) } @@ -2705,16 +2915,18 @@ module Dict = { b.code = b.code ++ - `${outputVar}={};for(let ${keyVar} in ${inputVar}){${b->B.scope(b => { - let itemOutputVar = - b->B.withPathPrepend( - ~path, - ~dynamicLocationVar=keyVar, - (b, ~path) => - b->B.useWithTypeFilter(~schema, ~input=`${inputVar}[${keyVar}]`, ~path), - ) - `${outputVar}[${keyVar}]=${itemOutputVar}` - })}}` + `${outputVar}={};for(let ${keyVar} in ${inputVar}){${b->B.scope( + b => { + let itemOutputVar = + b->B.withPathPrepend( + ~path, + ~dynamicLocationVar=keyVar, + (b, ~path) => + b->B.useWithTypeFilter(~schema, ~input=`${inputVar}[${keyVar}]`, ~path), + ) + `${outputVar}[${keyVar}]=${itemOutputVar}` + }, + )}}` let isAsync = schema.isAsyncParse->(Obj.magic: isAsyncParse => bool) if isAsync { @@ -2741,21 +2953,25 @@ module Dict = { b.code = b.code ++ - `${outputVar}={};for(let ${keyVar} in ${inputVar}){${b->B.scope(b => { - let itemOutputVar = - b->B.withPathPrepend( - ~path, - ~dynamicLocationVar=keyVar, - (b, ~path) => b->B.use(~schema, ~input=`${inputVar}[${keyVar}]`, ~path), - ) + `${outputVar}={};for(let ${keyVar} in ${inputVar}){${b->B.scope( + b => { + let itemOutputVar = + b->B.withPathPrepend( + ~path, + ~dynamicLocationVar=keyVar, + (b, ~path) => b->B.use(~schema, ~input=`${inputVar}[${keyVar}]`, ~path), + ) - `${outputVar}[${keyVar}]=${itemOutputVar}` - })}}` + `${outputVar}[${keyVar}]=${itemOutputVar}` + }, + )}}` outputVar } }), ~maybeTypeFilter=Some(Object.typeFilter), + //FIXME: + ~toJsonString=input => `JSON.stringify(${input})`, ) } } @@ -2825,8 +3041,10 @@ module Tuple = { let definition = definer((ctx :> ctx))->(Obj.magic: 'any => Definition.t) let {itemDefinitionsSet, schemas} = ctx let length = schemas->Js.Array2.length + let isJsonableRef = ref(true) for idx in 0 to length - 1 { - if schemas->Js.Array2.unsafe_get(idx)->Obj.magic->not { + let schema = schemas->Js.Array2.unsafe_get(idx) + if schema->Obj.magic->not { let schema = unit->toUnknown let inlinedInputLocation = `"${idx->Stdlib.Int.unsafeToString}"` let itemDefinition: Object.itemDefinition = { @@ -2836,6 +3054,9 @@ module Tuple = { } schemas->Js.Array2.unsafe_set(idx, schema) itemDefinitionsSet->Stdlib.Set.add(itemDefinition)->ignore + isJsonableRef.contents = false + } else if schema.maybeToJsonString === None { + isJsonableRef.contents = false } } let itemDefinitions = itemDefinitionsSet->Stdlib.Set.toArray @@ -2944,6 +3165,25 @@ module Tuple = { outputVar }), ~maybeTypeFilter=Some(Array.typeFilter), + ~toJsonString=?switch isJsonableRef.contents { + | true => + Some( + input => { + let jsonStringRef = ref(`'['+`) + for idx in 0 to schemas->Js.Array2.length - 1 { + let schema = schemas->Js.Array2.unsafe_get(idx) + jsonStringRef.contents = + jsonStringRef.contents ++ + (idx === 0 ? `` : `+','+`) ++ + (schema.maybeToJsonString->(Obj.magic: option<'a> => 'a))( + `${input}[${idx->Stdlib.Int.unsafeToString}]`, + ) + } + jsonStringRef.contents ++ `+']'` + }, + ) + | false => None + }, ~metadataMap=Metadata.Map.empty, ) } @@ -3062,26 +3302,24 @@ module Union = { b.code = b.code ++ - `try{${b->B.scope( - b => { - let itemOutput = b->B.use(~schema=itemSchema, ~input=inputVar, ~path=Path.empty) - let itemOutput = switch itemSchema.maybeTypeFilter { - | Some(typeFilter) => - let itemOutputVar = b->B.toVar(itemOutput) - b.code = - b.code ++ - b->B.typeFilterCode( - ~schema=itemSchema, - ~typeFilter, - ~inputVar=itemOutputVar, - ~path=Path.empty, - ) - itemOutputVar - | None => itemOutput - } - `${outputVar}=${itemOutput}` - }, - )}}catch(${errorVar}){if(${b->B.isInternalError(errorVar)}){` + `try{${b->B.scope(b => { + let itemOutput = b->B.use(~schema=itemSchema, ~input=inputVar, ~path=Path.empty) + let itemOutput = switch itemSchema.maybeTypeFilter { + | Some(typeFilter) => + let itemOutputVar = b->B.toVar(itemOutput) + b.code = + b.code ++ + b->B.typeFilterCode( + ~schema=itemSchema, + ~typeFilter, + ~inputVar=itemOutputVar, + ~path=Path.empty, + ) + itemOutputVar + | None => itemOutput + } + `${outputVar}=${itemOutput}` + })}}catch(${errorVar}){if(${b->B.isInternalError(errorVar)}){` codeEndRef.contents = `}else{throw ${errorVar}}}` ++ codeEndRef.contents } @@ -3098,6 +3336,8 @@ module Union = { outputVar }), ~maybeTypeFilter=None, + //FIXME: + ~toJsonString=input => `JSON.stringify(${input})`, ) } } @@ -3116,6 +3356,7 @@ let json = makeWithNoopSerializer( ~tagged=JSON, ~metadataMap=Metadata.Map.empty, ~maybeTypeFilter=None, + ~toJsonString=input => `JSON.stringify(${input})`, ~parseOperationBuilder=Builder.make((b, ~selfSchema, ~path) => { let rec parse = (input, ~path=path) => { switch input->Stdlib.Type.typeof { @@ -3215,6 +3456,7 @@ let catch = (schema, getFallbackValue) => { ~tagged=schema.tagged, ~maybeTypeFilter=None, ~metadataMap=schema.metadataMap, + ~toJsonString=?schema.maybeToJsonString, ) }