diff --git a/packages/tests/src/core/Example_test.res b/packages/tests/src/core/Example_test.res index c454bb3d..9feb2637 100644 --- a/packages/tests/src/core/Example_test.res +++ b/packages/tests/src/core/Example_test.res @@ -77,6 +77,6 @@ test("Compiled serialize code snapshot", t => { t->U.assertCompiledCode( ~schema=filmSchema, ~op=#Serialize, - `i=>{let v0=i["tags"],v3=i["rating"],v4=i["deprecatedAgeRestriction"],v5;if(v3!=="G"){if(v3!=="PG"){if(v3!=="PG13"){if(v3!=="R"){e[0](v3)}}}}if(v4!==void 0){v5=e[1](v4)}return {"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":v3,"Age":v5,}}`, + `i=>{let v0=i["tags"],v3=i["rating"];if(v3!=="G"){if(v3!=="PG"){if(v3!=="PG13"){if(v3!=="R"){e[0](v3)}}}}return {"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":v3,"Age":i["deprecatedAgeRestriction"],}}`, ) }) diff --git a/packages/tests/src/core/S_array_test.res b/packages/tests/src/core/S_array_test.res index ab8ed367..ab9aba16 100644 --- a/packages/tests/src/core/S_array_test.res +++ b/packages/tests/src/core/S_array_test.res @@ -68,18 +68,19 @@ module CommonWithNested = { test("Compiled serialize code snapshot", t => { let schema = S.array(S.string) + t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) + let schema = S.array(S.option(S.string)) t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) }) test("Compiled serialize code snapshot with transform", t => { - let schema = S.array(S.option(S.string)) + let schema = S.array(S.null(S.string)) - // TODO: Simplify t->U.assertCompiledCode( ~schema, ~op=#Serialize, - `i=>{let v5=[];for(let v0=0;v0{let v5=[];for(let v0=0;v0 { let schema = S.dict(S.string) + t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) + let schema = S.dict(S.option(S.string)) t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) }) test("Compiled serialize code snapshot with transform", t => { - let schema = S.dict(S.option(S.string)) + let schema = S.dict(S.null(S.string)) t->U.assertCompiledCode( ~schema, ~op=#Serialize, - `i=>{let v5={};for(let v0 in i){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=e[0](v2)}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5[v0]=v4}return v5}`, + `i=>{let v5={};for(let v0 in i){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=v2}else{v3=null}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5[v0]=v4}return v5}`, ) }) diff --git a/packages/tests/src/core/S_null_test.res b/packages/tests/src/core/S_null_test.res index 542fefc0..e5d70807 100644 --- a/packages/tests/src/core/S_null_test.res +++ b/packages/tests/src/core/S_null_test.res @@ -66,18 +66,12 @@ module Common = { ) }) - let serializeCode = `let v0;if(i!==void 0){v0=i}else{v0=null}return v0` test("Compiled serialize code snapshot", t => { - let schema = factory() - t->U.assertCompiledCode(~schema, ~op=#Serialize, `i=>{${serializeCode}}`) - }) - - test("Compiled reverse code snapshot", t => { let schema = factory() t->U.assertCompiledCode( - ~schema=schema->S.\"~experimantalReverse", - ~op=#Parse, - `i=>{if(i!==void 0&&(typeof i!=="string")){e[0](i)}${serializeCode}}`, + ~schema, + ~op=#Serialize, + `i=>{let v0;if(i!==void 0){v0=i}else{v0=null}return v0}`, ) }) @@ -163,3 +157,16 @@ test("Serializes Some(None) to null for null nested in option", t => { `i=>{let v2;if(i!==void 0){let v0=e[0](i),v1;if(v0!==void 0){v1=v0}else{v1=null}v2=v1}return v2}`, ) }) + +test("Serializes Some(None) to null for null nested in null", t => { + let schema = S.null(S.null(S.bool)) + + t->Assert.deepEqual(Some(None)->S.serializeToUnknownWith(schema), Ok(%raw(`null`)), ()) + t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`null`)), ()) + + t->U.assertCompiledCode( + ~schema, + ~op=#Serialize, + `i=>{let v2;if(i!==void 0){let v0=e[0](i),v1;if(v0!==void 0){v1=v0}else{v1=null}v2=v1}else{v2=null}return v2}`, + ) +}) diff --git a/packages/tests/src/core/S_option_test.res b/packages/tests/src/core/S_option_test.res index 3ceb215a..4e7f750c 100644 --- a/packages/tests/src/core/S_option_test.res +++ b/packages/tests/src/core/S_option_test.res @@ -55,11 +55,7 @@ module Common = { test("Compiled serialize code snapshot", t => { let schema = factory() - t->U.assertCompiledCode( - ~schema, - ~op=#Serialize, - `i=>{let v0;if(i!==void 0){v0=e[0](i)}return v0}`, - ) + t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) }) test("Reverse to self", t => { @@ -126,4 +122,51 @@ test("Serializes Some(None) to undefined for option nested in null", t => { t->Assert.deepEqual(Some(None)->S.serializeToUnknownWith(schema), Ok(%raw(`undefined`)), ()) t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`null`)), ()) + + t->U.assertCompiledCode( + ~schema, + ~op=#Serialize, + `i=>{let v0;if(i!==void 0){v0=e[0](i)}else{v0=null}return v0}`, + ) +}) + +test("Applies valFromOption for Some()", t => { + let schema = S.option(S.literal()) + + t->Assert.deepEqual(Some()->S.serializeToUnknownWith(schema), Ok(%raw(`undefined`)), ()) + t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`undefined`)), ()) + + t->U.assertCompiledCode( + ~schema, + ~op=#Serialize, + `i=>{let v0;if(i!==void 0){v0=e[0](i)}return v0}`, + ) +}) + +test("Doesn't apply valFromOption for non-undefined literals in option", t => { + let schema: S.t>> = S.option(S.literal(%raw(`null`))) + + // Note: It'll fail without a type annotation, but we can't do anything here + t->Assert.deepEqual(Some(%raw(`null`))->S.serializeToUnknownWith(schema), Ok(%raw(`null`)), ()) + t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`undefined`)), ()) + + t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize) +}) + +test("Applies valFromOption for unknown in option", t => { + let schema = S.option(S.unknown) + + t->Assert.deepEqual( + Some(%raw(`undefined`))->S.serializeToUnknownWith(schema), + Ok(%raw(`undefined`)), + (), + ) + t->Assert.deepEqual(Some(%raw(`"foo"`))->S.serializeToUnknownWith(schema), Ok(%raw(`"foo"`)), ()) + t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`undefined`)), ()) + + t->U.assertCompiledCode( + ~schema, + ~op=#Serialize, + `i=>{let v0;if(i!==void 0){v0=e[0](i)}return v0}`, + ) }) diff --git a/packages/tests/src/core/S_union_test.res b/packages/tests/src/core/S_union_test.res index 5517f9b1..b68ef67e 100644 --- a/packages/tests/src/core/S_union_test.res +++ b/packages/tests/src/core/S_union_test.res @@ -521,7 +521,7 @@ module CknittelBugReport = { t->U.assertCompiledCode( ~schema, ~op=#Serialize, - `i=>{let v2=i;try{let v0=i["_0"]["payload"]["a"],v1;if(i["TAG"]!=="A"){e[0](i["TAG"])}if(v0!==void 0){v1=e[1](v0)}v2={"payload":{"a":v1,},}}catch(e0){try{let v3=i["_0"]["payload"]["b"],v4;if(i["TAG"]!=="B"){e[2](i["TAG"])}if(v3!==void 0){v4=e[3](v3)}v2={"payload":{"b":v4,},}}catch(e1){e[4]([e0,e1,])}}return v2}`, + `i=>{let v0=i;try{if(i["TAG"]!=="A"){e[0](i["TAG"])}v0={"payload":{"a":i["_0"]["payload"]["a"],},}}catch(e0){try{if(i["TAG"]!=="B"){e[1](i["TAG"])}v0={"payload":{"b":i["_0"]["payload"]["b"],},}}catch(e1){e[2]([e0,e1,])}}return v0}`, ) let x = { diff --git a/src/S_Core.bs.mjs b/src/S_Core.bs.mjs index a75382db..42719e94 100644 --- a/src/S_Core.bs.mjs +++ b/src/S_Core.bs.mjs @@ -1214,34 +1214,34 @@ function $$default(schema) { return schema.m[defaultMetadataId]; } -function builder(b, input, selfSchema, path) { - var isNullInput = selfSchema.t.TAG === "Null"; - var reversed = selfSchema.r(); - var isNullOutput = reversed.t.TAG === "Null"; - var childSchema = selfSchema.t._0; - var bb = scope(b); - var itemInput; - if (!isNullOutput && (b.g.o === "SerializeToJson" || b.g.o === "SerializeToUnknown")) { - var value = Caml_option.valFromOption; - itemInput = val(bb, "e[" + (bb.g.e.push(value) - 1) + "](" + $$var(b, input) + ")"); - } else { - itemInput = input; - } - var itemOutput = childSchema.b(bb, itemInput, childSchema, path); - var itemCode = allocateScope(bb); - var inputLiteral = isNullInput ? "null" : "void 0"; - var ouputLiteral = isNullOutput ? "null" : "void 0"; - var isTransformed = inputLiteral !== ouputLiteral || itemOutput !== input; - var output = isTransformed ? ({ - s: b, - a: itemOutput.a - }) : input; - if (itemCode !== "" || isTransformed) { - b.c = b.c + ("if(" + $$var(b, input) + "!==" + inputLiteral + "){" + itemCode + set(b, output, itemOutput) + "}" + ( - inputLiteral !== ouputLiteral || output.a ? "else{" + set(b, output, val(b, ouputLiteral)) + "}" : "" - )); - } - return output; +function makeBuilder(isNullInput, isNullOutput) { + return function (b, input, selfSchema, path) { + var childSchema = selfSchema.t._0; + var childSchemaTag = childSchema.t.TAG; + var bb = scope(b); + var itemInput; + if ((b.g.o === "SerializeToJson" || b.g.o === "SerializeToUnknown") && (childSchema.t === "Unknown" || childSchemaTag === "Option" || childSchemaTag === "Literal" && childSchema.t._0.value === (void 0))) { + var value = Caml_option.valFromOption; + itemInput = val(bb, "e[" + (bb.g.e.push(value) - 1) + "](" + $$var(b, input) + ")"); + } else { + itemInput = input; + } + var itemOutput = childSchema.b(bb, itemInput, childSchema, path); + var itemCode = allocateScope(bb); + var inputLiteral = isNullInput ? "null" : "void 0"; + var ouputLiteral = isNullOutput ? "null" : "void 0"; + var isTransformed = inputLiteral !== ouputLiteral || itemOutput !== input; + var output = isTransformed ? ({ + s: b, + a: itemOutput.a + }) : input; + if (itemCode !== "" || isTransformed) { + b.c = b.c + ("if(" + $$var(b, input) + "!==" + inputLiteral + "){" + itemCode + set(b, output, itemOutput) + "}" + ( + inputLiteral !== ouputLiteral || output.a ? "else{" + set(b, output, val(b, ouputLiteral)) + "}" : "" + )); + } + return output; + }; } function maybeTypeFilter(schema, inlinedNoneValue) { @@ -1258,7 +1258,7 @@ function factory(schema) { return makeSchema(containerName, { TAG: "Option", _0: schema - }, empty, builder, maybeTypeFilter(schema, "void 0"), onlyChild(factory, schema)); + }, empty, makeBuilder(false, false), maybeTypeFilter(schema, "void 0"), onlyChild(factory, schema)); } function getWithDefault(schema, $$default) { @@ -1301,8 +1301,12 @@ function factory$1(schema) { return makeSchema(containerName, { TAG: "Null", _0: schema - }, empty, builder, maybeTypeFilter(schema, "null"), (function () { - return factory(schema.r()); + }, empty, makeBuilder(true, false), maybeTypeFilter(schema, "null"), (function () { + var child = schema.r(); + return makeReverseSchema(containerName, { + TAG: "Option", + _0: child + }, empty, makeBuilder(false, true), maybeTypeFilter(schema, "void 0")); })); } @@ -1310,7 +1314,7 @@ function nullable(schema) { return factory(factory$1(schema)); } -function builder$1(b, input, selfSchema, path) { +function builder(b, input, selfSchema, path) { b.c = b.c + failWithArg(b, path, (function (input) { return { TAG: "InvalidType", @@ -1321,7 +1325,7 @@ function builder$1(b, input, selfSchema, path) { return input; } -var schema = makeSchema(primitiveName, "Never", empty, builder$1, undefined, toSelf); +var schema = makeSchema(primitiveName, "Never", empty, builder, undefined, toSelf); var metadataId = "rescript-schema:Array.refinements"; @@ -1372,7 +1376,7 @@ function typeFilter$1(_b, inputVar) { return "!" + inputVar + "||" + inputVar + ".constructor!==Object"; } -function builder$2(b, input, selfSchema, path) { +function builder$1(b, input, selfSchema, path) { var asyncOutputs = []; var outputs = new WeakMap(); var parseItems = function (b, input, schema, path) { @@ -1624,7 +1628,7 @@ function factory$3(definer) { }, n: name, r: reverse, - b: builder$2, + b: builder$1, f: typeFilter$1, i: 0, d: definer, @@ -1693,7 +1697,7 @@ function tuple(definer) { TAG: "Tuple", items: items, definition: definition - }, empty, builder$2, (function (b, inputVar) { + }, empty, builder$1, (function (b, inputVar) { return typeFilter(b, inputVar) + ("||" + inputVar + ".length!==" + length); }), reverse); } diff --git a/src/S_Core.res b/src/S_Core.res index 3c5e99fb..a28cbe38 100644 --- a/src/S_Core.res +++ b/src/S_Core.res @@ -354,9 +354,6 @@ let unsafeGetErrorPayload = variant => (variant->Obj.magic)["_1"] @inline let isLiteralSchema = schema => schema.tagged->unsafeGetVarianTag === "Literal" -@inline -let isNullSchema = schema => schema.tagged->unsafeGetVarianTag === "Null" - type globalConfig = { @as("r") mutable recCounter: int, @@ -788,7 +785,9 @@ module Builder = { module B = Builder.B module Reverse = { - let toSelf = () => %raw(`this`) + let toSelf = () => { + %raw(`this`) + } let onlyChild = (~factory, ~schema) => { () => { @@ -1411,7 +1410,7 @@ module Metadata = { ~builder=schema.builder, ~tagged=schema.tagged, ~maybeTypeFilter=schema.maybeTypeFilter, - ~metadataMap, // FIXME: Verify that we can reuse the same metadata object + ~metadataMap, ) }, ) @@ -1536,7 +1535,7 @@ let setName = (schema, name) => { ~tagged=schema.tagged, ~maybeTypeFilter=schema.maybeTypeFilter, ~metadataMap=schema.metadataMap, - ~reverse=() => schema.reverse(), // FIXME: test better + ~reverse=() => schema.reverse(), // TODO: test better ) } @@ -1655,7 +1654,6 @@ let transform: (t<'input>, s<'output> => transformDefinition<'input, 'output>) = } }, ~maybeTypeFilter=None, - // FIXME: Test how metadata should work for reversed schemas ~metadataMap=Metadata.Map.empty, ) }, @@ -1692,7 +1690,7 @@ let custom = (name, definer) => { ~maybeTypeFilter=None, ~reverse=() => { makeReverseSchema( - ~name=() => name, // FIXME: Test that it should have the custom name + ~name=() => name, ~tagged=Unknown, ~builder=Builder.make((b, ~input, ~selfSchema, ~path) => { switch definer(b->B.effectCtx(~selfSchema, ~path)) { @@ -1765,46 +1763,48 @@ module Option = { let default = schema => schema->Metadata.get(~id=defaultMetadataId) - let builder = Builder.make((b, ~input, ~selfSchema, ~path) => { - let isNullInput = selfSchema->isNullSchema - let reversed = selfSchema.reverse() - let isNullOutput = reversed->isNullSchema - let childSchema = selfSchema->classify->unsafeGetVariantPayload - - let bb = b->B.scope - // TODO: Improve the logic - let itemInput = if ( - !isNullOutput && - (b.global.operation === SerializeToJson || b.global.operation === SerializeToUnknown) - ) { - bb->B.val(`${bb->B.embed(%raw("Caml_option.valFromOption"))}(${b->B.Val.var(input)})`) - } else { - input - } + let makeBuilder = (~isNullInput, ~isNullOutput) => + Builder.make((b, ~input, ~selfSchema, ~path) => { + let childSchema = selfSchema->classify->unsafeGetVariantPayload + let childSchemaTag = childSchema->classify->unsafeGetVarianTag + + let bb = b->B.scope + let itemInput = if ( + (b.global.operation === SerializeToJson || b.global.operation === SerializeToUnknown) && + (childSchema->classify === Unknown || + childSchemaTag === "Option" || + (childSchemaTag === "Literal" && + (childSchema->classify->unsafeGetVariantPayload: literal)->Literal.value === + %raw(`void 0`))) + ) { + bb->B.val(`${bb->B.embed(%raw("Caml_option.valFromOption"))}(${b->B.Val.var(input)})`) + } else { + input + } - let itemOutput = bb->B.parse(~schema=childSchema, ~input=itemInput, ~path) - let itemCode = bb->B.allocateScope + let itemOutput = bb->B.parse(~schema=childSchema, ~input=itemInput, ~path) + let itemCode = bb->B.allocateScope - let inputLiteral = isNullInput ? "null" : "void 0" - let ouputLiteral = isNullOutput ? "null" : "void 0" + let inputLiteral = isNullInput ? "null" : "void 0" + let ouputLiteral = isNullOutput ? "null" : "void 0" - let isTransformed = inputLiteral !== ouputLiteral || itemOutput !== input + let isTransformed = inputLiteral !== ouputLiteral || itemOutput !== input - let output = isTransformed ? {_scope: b, isAsync: itemOutput.isAsync} : input + let output = isTransformed ? {_scope: b, isAsync: itemOutput.isAsync} : input - if itemCode !== "" || isTransformed { - b.code = - b.code ++ - `if(${b->B.Val.var(input)}!==${inputLiteral}){${itemCode}${b->B.Val.set( - output, - itemOutput, - )}}${inputLiteral !== ouputLiteral || output.isAsync - ? `else{${b->B.Val.set(output, b->B.val(ouputLiteral))}}` - : ""}` - } + if itemCode !== "" || isTransformed { + b.code = + b.code ++ + `if(${b->B.Val.var(input)}!==${inputLiteral}){${itemCode}${b->B.Val.set( + output, + itemOutput, + )}}${inputLiteral !== ouputLiteral || output.isAsync + ? `else{${b->B.Val.set(output, b->B.val(ouputLiteral))}}` + : ""}` + } - output - }) + output + }) let maybeTypeFilter = (~schema, ~inlinedNoneValue) => { switch schema.maybeTypeFilter { @@ -1824,7 +1824,7 @@ module Option = { ~name=containerName, ~metadataMap=Metadata.Map.empty, ~tagged=Option(schema), - ~builder, + ~builder=makeBuilder(~isNullInput=false, ~isNullOutput=false), ~maybeTypeFilter=maybeTypeFilter(~schema, ~inlinedNoneValue="void 0"), ~reverse=Reverse.onlyChild(~factory, ~schema), ) @@ -1883,10 +1883,17 @@ module Null = { ~name=containerName, ~metadataMap=Metadata.Map.empty, ~tagged=Null(schema), - ~builder=Option.builder, + ~builder=Option.makeBuilder(~isNullInput=true, ~isNullOutput=false), ~maybeTypeFilter=Option.maybeTypeFilter(~schema, ~inlinedNoneValue="null"), ~reverse=() => { - Option.factory(schema.reverse()) + let child = schema.reverse() + makeReverseSchema( + ~name=containerName, + ~tagged=Option(child), + ~builder=Option.makeBuilder(~isNullInput=false, ~isNullOutput=true), + ~maybeTypeFilter=Option.maybeTypeFilter(~schema, ~inlinedNoneValue="void 0"), + ~metadataMap=Metadata.Map.empty, + ) }, ) } @@ -2599,7 +2606,7 @@ module Object = { jsParse, jsParseAsync, jsSerialize, - reverse: schema.reverse, // FIXME: test + reverse: schema.reverse, } // TODO: Should it throw for non Object schemas? | _ => schema @@ -3026,7 +3033,7 @@ let rec preprocess = (schema, transformer) => { ~builder=schema.builder, ~maybeTypeFilter=schema.maybeTypeFilter, ~metadataMap=schema.metadataMap, - ~reverse=schema.reverse, // FIXME: Might break with memo + ~reverse=schema.reverse, ) | _ => makeSchema( @@ -3070,7 +3077,7 @@ let rec preprocess = (schema, transformer) => { } }, ~maybeTypeFilter=None, - // FIXME: Test how metadata should work for reversed schemas + // TODO: Test how metadata should work for reversed schemas ~metadataMap=Metadata.Map.empty, ) }, diff --git a/test.js b/test.js index 5cce8d70..df714e1d 100644 --- a/test.js +++ b/test.js @@ -1,8 +1,6 @@ (i) => { let v0 = i["tags"], - v3 = i["rating"], - v4 = i["deprecatedAgeRestriction"], - v5; + v3 = i["rating"]; if (v3 !== "G") { if (v3 !== "PG") { if (v3 !== "PG13") { @@ -12,8 +10,11 @@ } } } - if (v4 !== void 0) { - v5 = e[1](v4); - } - return { Id: i["id"], Title: i["title"], Tags: v0, Rating: v3, Age: v5 }; + return { + Id: i["id"], + Title: i["title"], + Tags: v0, + Rating: v3, + Age: i["deprecatedAgeRestriction"], + }; };