diff --git a/CHANGELOG_NEXT.md b/CHANGELOG_NEXT.md index 9b49d478..7bb2cd4b 100644 --- a/CHANGELOG_NEXT.md +++ b/CHANGELOG_NEXT.md @@ -26,3 +26,5 @@ - Release 7.0.2 with type check fix for recursive schema - Test GenType compatibility with d.ts - Clean up error tags +- Set 8.0.x for rescript-schema in ppx deps +- Update github ci to run on "v*.*.\*-patch" diff --git a/packages/tests/src/benchmark/Benchmark.bs.mjs b/packages/tests/src/benchmark/Benchmark.bs.mjs index c31d4441..4c624412 100644 --- a/packages/tests/src/benchmark/Benchmark.bs.mjs +++ b/packages/tests/src/benchmark/Benchmark.bs.mjs @@ -120,22 +120,25 @@ var schema = S$RescriptSchema.recursive(function (schema) { var testData1 = { TAG: "Z", - _0: Core__Array.make(2, { + _0: Core__Array.make(25, { TAG: "Z", - _0: Core__Array.make(2, { + _0: Core__Array.make(25, { TAG: "Z", - _0: Core__Array.make(2, "Y") + _0: Core__Array.make(25, "Y") }) }) }; -Core__Array.make(2, { - TAG: "A", - _0: Core__Array.make(2, { - TAG: "A", - _0: Core__Array.make(2, "B") - }) - }); +var testData2 = { + TAG: "A", + _0: Core__Array.make(25, { + TAG: "A", + _0: Core__Array.make(25, { + TAG: "A", + _0: Core__Array.make(25, "B") + }) + }) +}; function test() { console.time("testData1 serialize"); @@ -144,7 +147,12 @@ function test() { console.time("testData1 parse"); S$RescriptSchema.parseOrRaiseWith(json, schema); console.timeEnd("testData1 parse"); - console.log(schema.parseOrThrow.toString()); + console.time("testData2 serialize"); + var json$1 = S$RescriptSchema.serializeOrRaiseWith(testData2, schema); + console.timeEnd("testData2 serialize"); + console.time("testData2 parse"); + S$RescriptSchema.parseOrRaiseWith(json$1, schema); + console.timeEnd("testData2 parse"); } test(); diff --git a/packages/tests/src/benchmark/Benchmark.res b/packages/tests/src/benchmark/Benchmark.res index f9530e6b..1735ae7e 100644 --- a/packages/tests/src/benchmark/Benchmark.res +++ b/packages/tests/src/benchmark/Benchmark.res @@ -170,9 +170,9 @@ module CrazyUnion = { ]) ) - let testData1 = Z(Array.make(~length=2, Z(Array.make(~length=2, Z(Array.make(~length=2, Y)))))) + let testData1 = Z(Array.make(~length=25, Z(Array.make(~length=25, Z(Array.make(~length=25, Y)))))) - let _testData2 = A(Array.make(~length=2, A(Array.make(~length=2, A(Array.make(~length=2, B)))))) + let testData2 = A(Array.make(~length=25, A(Array.make(~length=25, A(Array.make(~length=25, B)))))) let test = () => { Console.time("testData1 serialize") @@ -183,22 +183,34 @@ module CrazyUnion = { let _ = S.parseOrRaiseWith(json, schema) Console.timeEnd("testData1 parse") - // Console.time("testData2 serialize") - // let json = S.serializeOrRaiseWith(testData2, schema) - // Console.timeEnd("testData2 serialize") + Console.time("testData2 serialize") + let json = S.serializeOrRaiseWith(testData2, schema) + Console.timeEnd("testData2 serialize") - // Console.time("testData2 parse") - // let _ = S.parseOrRaiseWith(json, schema) - // Console.timeEnd("testData2 parse") + Console.time("testData2 parse") + let _ = S.parseOrRaiseWith(json, schema) + Console.timeEnd("testData2 parse") - Js.log((schema->Obj.magic)["parseOrThrow"]["toString"]()) + // Js.log((schema->Obj.magic)["parseOrThrow"]["toString"]()) } } +// Full // testData1 serialize: 5.414s // testData1 parse: 5.519s // testData2 serialize: 70.864ms // testData2 parse: 70.967ms + +// Wip +// testData1 serialize: 5.398s +// testData1 parse: 6.171ms +// testData2 serialize: 69.621ms +// testData2 parse: 0.878ms + +// Partial +// testData1 serialize: 1.802ms +// testData1 parse: 1.411ms +// 734 Error.make CrazyUnion.test() let data = makeTestObject() diff --git a/packages/tests/src/core/Example_test.res b/packages/tests/src/core/Example_test.res index 29ea5e5a..803e5f65 100644 --- a/packages/tests/src/core/Example_test.res +++ b/packages/tests/src/core/Example_test.res @@ -69,7 +69,7 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema=filmSchema, ~op=#Parse, - `i=>{if(!i||i.constructor!==Object){e[11](i)}let v0=i["Id"],v1=i["Title"],v2=i["Tags"],v6=i["Rating"],v7,v8=i["Age"];if(typeof v0!=="number"||Number.isNaN(v0)){e[0](v0)}if(typeof v1!=="string"){e[1](v1)}if(v2!==void 0&&(!Array.isArray(v2))){e[2](v2)}if(v2!==void 0){for(let v3=0;v32147483647||v8<-2147483648||v8%1!==0)){e[10](v8)}return {"id":v0,"title":v1,"tags":v2===void 0?e[4]:v2,"rating":v7,"deprecatedAgeRestriction":v8,}}`, + `i=>{if(!i||i.constructor!==Object){e[11](i)}let v0=i["Id"],v1=i["Title"],v2=i["Tags"],v6=i["Rating"],v7,v8=i["Age"];if(typeof v0!=="number"||Number.isNaN(v0)){e[0](v0)}if(typeof v1!=="string"){e[1](v1)}if(v2!==void 0&&(!Array.isArray(v2))){e[2](v2)}if(v2!==void 0){for(let v3=0;v32147483647||v8<-2147483648||v8%1!==0)){e[10](v8)}return {"id":v0,"title":v1,"tags":v2===void 0?e[4]:v2,"rating":v7,"deprecatedAgeRestriction":v8,}}`, ) }) diff --git a/packages/tests/src/core/S_union_test.res b/packages/tests/src/core/S_union_test.res index c1a3d86c..01e161ca 100644 --- a/packages/tests/src/core/S_union_test.res +++ b/packages/tests/src/core/S_union_test.res @@ -25,14 +25,26 @@ test("Successfully parses polymorphic variants", t => { t->Assert.deepEqual(%raw(`"apple"`)->S.parseAnyWith(schema), Ok(#apple), ()) }) -test("Parses when both schemas misses parser", t => { +test("Parses when both schemas misses parser and have the same type", t => { let schema = S.union([ - S.literal(#apple)->S.transform(_ => {serializer: _ => #apple}), + S.string->S.transform(_ => {serializer: _ => "apple"}), S.string->S.transform(_ => {serializer: _ => "apple"}), ]) t->U.assertErrorResult( %raw(`null`)->S.parseAnyWith(schema), + { + code: InvalidType({ + expected: schema->S.toUnknown, + received: %raw(`null`), + }), + operation: Parse, + path: S.Path.empty, + }, + ) + + t->U.assertErrorResult( + %raw(`"foo"`)->S.parseAnyWith(schema), { code: InvalidUnion([ U.error({ @@ -51,7 +63,45 @@ test("Parses when both schemas misses parser", t => { }, ) - t->U.assertCompiledCode(~schema, ~op=#Parse, `i=>{e[4]([e[1],e[3],]);return i}`) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{if(typeof i!=="string"){e[0](i)}else{e[5]([e[2],e[4],]);}return undefined}`, + ) +}) + +test("Parses when both schemas misses parser and have different types", t => { + let schema = S.union([ + S.literal(#apple)->S.transform(_ => {serializer: _ => #apple}), + S.string->S.transform(_ => {serializer: _ => "apple"}), + ]) + + t->U.assertErrorResult( + %raw(`null`)->S.parseAnyWith(schema), + { + code: InvalidType({ + expected: schema->S.toUnknown, + received: %raw(`null`), + }), + operation: Parse, + path: S.Path.empty, + }, + ) + + t->U.assertErrorResult( + %raw(`"abc"`)->S.parseAnyWith(schema), + { + code: InvalidOperation({description: "The S.transform parser is missing"}), + operation: Parse, + path: S.Path.empty, + }, + ) + + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{if(!(i==="apple")){if(typeof i!=="string"){e[0](i)}else{throw e[1]}}else{throw e[3]}return undefined}`, + ) }) test("Serializes when both schemas misses serializer", t => { @@ -91,7 +141,7 @@ test("Parses when second struct misses parser", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0;try{i==="apple"||e[0](i);v0=i}catch(e0){e[3]([e0,e[2],])}return v0}`, + `i=>{let v0;if(!(i==="apple")){if(typeof i!=="string"){e[0](i)}else{throw e[1]}}else{v0=i}return v0}`, ) }) @@ -258,32 +308,10 @@ module Advanced = { t->U.assertErrorResult( %raw(`"Hello world!"`)->S.parseAnyWith(shapeSchema), { - code: InvalidUnion([ - U.error({ - code: InvalidType({ - expected: circleSchema->S.toUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: squareSchema->S.toUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - U.error({ - code: InvalidType({ - expected: triangleSchema->S.toUnknown, - received: %raw(`"Hello world!"`), - }), - operation: Parse, - path: S.Path.empty, - }), - ]), + code: InvalidType({ + expected: shapeSchema->S.toUnknown, + received: %raw(`"Hello world!"`), + }), operation: Parse, path: S.Path.empty, }, @@ -377,7 +405,7 @@ module Advanced = { t->U.assertCompiledCode( ~schema=shapeSchema, ~op=#Parse, - `i=>{let v2;try{if(!i||i.constructor!==Object){e[0](i)}let v0=i["kind"],v1=i["radius"];v0==="circle"||e[1](v0);if(typeof v1!=="number"||Number.isNaN(v1)){e[2](v1)}v2={"TAG":e[3],"radius":v1,}}catch(e0){try{if(!i||i.constructor!==Object){e[4](i)}let v3=i["kind"],v4=i["x"];v3==="square"||e[5](v3);if(typeof v4!=="number"||Number.isNaN(v4)){e[6](v4)}v2={"TAG":e[7],"x":v4,}}catch(e1){try{if(!i||i.constructor!==Object){e[8](i)}let v5=i["kind"],v6=i["x"],v7=i["y"];v5==="triangle"||e[9](v5);if(typeof v6!=="number"||Number.isNaN(v6)){e[10](v6)}if(typeof v7!=="number"||Number.isNaN(v7)){e[11](v7)}v2={"TAG":e[12],"x":v6,"y":v7,}}catch(e2){e[13]([e0,e1,e2,])}}}return v2}`, + `i=>{let v2;if(!i||i.constructor!==Object){e[0](i)}else{try{if(!i||i.constructor!==Object){e[1](i)}let v0=i["kind"],v1=i["radius"];v0==="circle"||e[2](v0);if(typeof v1!=="number"||Number.isNaN(v1)){e[3](v1)}v2={"TAG":e[4],"radius":v1,}}catch(e0){try{if(!i||i.constructor!==Object){e[5](i)}let v3=i["kind"],v4=i["x"];v3==="square"||e[6](v3);if(typeof v4!=="number"||Number.isNaN(v4)){e[7](v4)}v2={"TAG":e[8],"x":v4,}}catch(e1){try{if(!i||i.constructor!==Object){e[9](i)}let v5=i["kind"],v6=i["x"],v7=i["y"];v5==="triangle"||e[10](v5);if(typeof v6!=="number"||Number.isNaN(v6)){e[11](v6)}if(typeof v7!=="number"||Number.isNaN(v7)){e[12](v7)}v2={"TAG":e[13],"x":v6,"y":v7,}}catch(e2){e[14]([e0,e1,e2,])}}}}return v2}`, ) }) @@ -413,7 +441,7 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0;try{i===0||e[0](i);v0=i}catch(e0){try{i===1||e[1](i);v0=i}catch(e1){e[2]([e0,e1,])}}return v0}`, + `i=>{let v0;if(!(i===0)){if(!(i===1)){e[0](i)}else{v0=i}}else{v0=i}return v0}`, ) }) @@ -568,18 +596,18 @@ module CrazyUnion = { ]) ) - test("Compiled code snapshot of crazy union", t => { - S.setGlobalConfig({}) - t->U.assertCompiledCode( - ~schema, - ~op=#Parse, - `i=>{let r0=i=>{let v6;try{if(!i||i.constructor!==Object){e[0](i)}let v0=i["type"],v1=i["nested"],v5=[];v0==="A"||e[1](v0);if(!Array.isArray(v1)){e[2](v1)}for(let v2=0;v2U.assertCompiledCode( - ~schema, - ~op=#Serialize, - `i=>{let r0=i=>{let v5,v6,v12;try{let v0=i["_0"],v4=[];if(i["TAG"]!==e[0]){e[1](i["TAG"])}for(let v1=0;v1 { + // S.setGlobalConfig({}) + // t->U.assertCompiledCode( + // ~schema, + // ~op=#Parse, + // `i=>{let r0=i=>{let v6;try{if(!i||i.constructor!==Object){e[0](i)}let v0=i["type"],v1=i["nested"],v5=[];v0==="A"||e[1](v0);if(!Array.isArray(v1)){e[2](v1)}for(let v2=0;v2U.assertCompiledCode( + // ~schema, + // ~op=#Serialize, + // `i=>{let r0=i=>{let v5,v6,v12;try{let v0=i["_0"],v4=[];if(i["TAG"]!==e[0]){e[1](i["TAG"])}for(let v1=0;v1 { + let codeEndRef = ref("") + let errorCodeRef = ref("") + let isAsync = ref(false) + + // TODO: Add support for async + for idx in 0 to schemas->Js.Array2.length - 1 { + let prevCode = b.code + try { + let schema = schemas->Js.Array2.unsafe_get(idx) + let errorVar = `e` ++ idx->Stdlib.Int.unsafeToString + b.code = b.code ++ `try{` + let itemOutput = b->B.parseWithTypeCheck(~schema, ~input, ~path=Path.empty) + if itemOutput.isAsync { + isAsync := true + } + + b.code = b.code ++ `${b->B.Val.set(output, itemOutput)}}catch(${errorVar}){` + codeEndRef := codeEndRef.contents ++ "}" + + errorCodeRef := errorCodeRef.contents ++ errorVar ++ "," + } catch { + | exn => + errorCodeRef := errorCodeRef.contents ++ b->B.embed(exn->InternalError.getOrRethrow) ++ "," + b.code = prevCode + } + } + + if isAsync.contents { + b->B.invalidOperation( + ~path, + ~description="S.union doesn't support async items. Please create an issue to rescript-schema if you nead the feature", + ) + } + + b.code = + b.code ++ + b->B.failWithArg( + ~path, + internalErrors => { + InvalidUnion(internalErrors) + }, + `[${errorCodeRef.contents}]`, + ) ++ + codeEndRef.contents + + let isAllSchemasBuilderFailed = codeEndRef.contents === "" + if isAllSchemasBuilderFailed { + b.code = b.code ++ ";" + } + } + let factory = schemas => { let schemas: array> = schemas->Obj.magic @@ -2854,61 +2906,68 @@ module Union = { ~rawTagged=Union(schemas), ~parseOperationBuilder=Builder.make((b, ~input, ~selfSchema, ~path) => { let schemas = selfSchema->classify->unsafeGetVariantPayload + let inputVar = b->B.Val.var(input) - let output = b->B.allocateVal - let codeEndRef = ref("") - let errorCodeRef = ref("") - let isAsync = ref(false) - - // TODO: Add support for async + let groupsByTypeFilter = Js.Dict.empty() + let typeFilters = [] for idx in 0 to schemas->Js.Array2.length - 1 { - let prevCode = b.code - try { - let schema = schemas->Js.Array2.unsafe_get(idx) - let errorVar = `e` ++ idx->Stdlib.Int.unsafeToString - b.code = b.code ++ `try{` - let itemOutput = b->B.parseWithTypeCheck(~schema, ~input, ~path=Path.empty) - if itemOutput.isAsync { - isAsync := true + let schema = schemas->Js.Array2.unsafe_get(idx) + let typeFilterCode = switch schema.maybeTypeFilter { + | Some(typeFilter) => typeFilter(~inputVar) + | None => + switch schema.rawTagged { + | Literal(literal) => + `!(${b->(literal->Literal.toInternal).checkBuilder(~inputVar, ~literal)})` + | _ => "false" + } + } + switch groupsByTypeFilter->Js.Dict.get(typeFilterCode) { + | Some(schemas) => schemas->Js.Array2.push(schema)->ignore + | None => { + typeFilters->Js.Array2.push(typeFilterCode)->ignore + groupsByTypeFilter->Js.Dict.set(typeFilterCode, [schema]) } - - b.code = b.code ++ `${b->B.Val.set(output, itemOutput)}}catch(${errorVar}){` - codeEndRef := codeEndRef.contents ++ "}" - - errorCodeRef := errorCodeRef.contents ++ errorVar ++ "," - } catch { - | exn => - errorCodeRef := - errorCodeRef.contents ++ b->B.embed(exn->InternalError.getOrRethrow) ++ "," - b.code = prevCode } } - if isAsync.contents { - b->B.invalidOperation( - ~path, - ~description="S.union doesn't support async items. Please create an issue to rescript-schema if you nead the feature", - ) - } + let output = b->B.allocateVal - b.code = - b.code ++ - b->B.failWithArg( - ~path, - internalErrors => { - InvalidUnion(internalErrors) - }, - `[${errorCodeRef.contents}]`, - ) ++ - codeEndRef.contents + let rec loopTypeFilters = idx => { + let isLastItem = idx === typeFilters->Js.Array2.length - 1 + let typeFilterCode = typeFilters->Js.Array2.unsafe_get(idx) + let schemas = groupsByTypeFilter->Js.Dict.unsafeGet(typeFilterCode) - let isAllSchemasBuilderFailed = codeEndRef.contents === "" - if isAllSchemasBuilderFailed { - b.code = b.code ++ ";" - input - } else { - output + b.code = b.code ++ `if(${typeFilterCode}){` + if isLastItem { + b.code = + b.code ++ + b->B.failWithArg( + ~path, + received => InvalidType({ + expected: selfSchema, + received, + }), + inputVar, + ) + } else { + loopTypeFilters(idx + 1) + } + b.code = b.code ++ `}else{` + switch schemas { + | [schema] => + let prevCode = b.code + try { + b.code = b.code ++ b->B.Val.set(output, b->B.parse(~schema, ~input, ~path)) + } catch { + | exn => b.code = prevCode ++ "throw " ++ b->B.embed(exn->InternalError.getOrRethrow) + } + | _ => genericParse(b, ~schemas, ~input, ~output, ~path) + } + b.code = b.code ++ `}` } + loopTypeFilters(0) + + output }), ~serializeOperationBuilder=Builder.make((b, ~input, ~selfSchema, ~path) => { let schemas = selfSchema->classify->unsafeGetVariantPayload