diff --git a/CHANGELOG_NEXT.md b/CHANGELOG_NEXT.md index 7bb2cd4b..eeaf187a 100644 --- a/CHANGELOG_NEXT.md +++ b/CHANGELOG_NEXT.md @@ -9,7 +9,7 @@ - Move operations from functions to Schema methods - Add `serializeToJsonOrThrow` - Update operation type to be more detailed and feature it in the error message. -- S.union still doesn't support schemas with async, but treats them differently. Please don't try to use them, since the behavior is not predictable. +- S.union supports schemas with async - Added `S.assertOrRaiseWith` or `schema.assert` for js/ts users. It doesn't return parsed value, but that makes the function 2-3 times faster, depending on the schema. - Improved S.recursive implementation - Added `S.setGlobalConfig`. Now it's possible to customize the behavior of the library: @@ -17,6 +17,7 @@ - Disable NaN check for numbers - Removed `parseAsyncInStepsWith` and `parseAnyAsyncInStepsWith` to reduce internal library complexity. Let me know if you need it. I can re-implement it in a future version in a simpler way. - Refactored parse async. Fixed some bugs and made it more performant. +- Improved performance and errors of S.union schema // TODO: diff --git a/packages/tests/src/benchmark/Benchmark.res b/packages/tests/src/benchmark/Benchmark.res index 1735ae7e..92480f50 100644 --- a/packages/tests/src/benchmark/Benchmark.res +++ b/packages/tests/src/benchmark/Benchmark.res @@ -191,7 +191,7 @@ module CrazyUnion = { let _ = S.parseOrRaiseWith(json, schema) Console.timeEnd("testData2 parse") - // Js.log((schema->Obj.magic)["parseOrThrow"]["toString"]()) + // Console.log((schema->Obj.magic)["parseOrThrow"]["toString"]()) } } diff --git a/packages/tests/src/core/Example_test.res b/packages/tests/src/core/Example_test.res index 803e5f65..8ca8d7fb 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=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||v7<-2147483648||v7%1!==0)){e[10](v7)}return {"id":v0,"title":v1,"tags":v2===void 0?e[4]:v2,"rating":v6,"deprecatedAgeRestriction":v7,}}`, ) }) diff --git a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.bs.mjs b/packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.bs.mjs deleted file mode 100644 index d856702b..00000000 --- a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.bs.mjs +++ /dev/null @@ -1,2 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE -/* This output is empty. Its source's type definitions, externals and/or unused code got optimized away. */ diff --git a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.res b/packages/tests/src/core/S_parseAsync_test.res similarity index 85% rename from packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.res rename to packages/tests/src/core/S_parseAsync_test.res index e47f2214..86ee6142 100644 --- a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_deprecatedTest.res +++ b/packages/tests/src/core/S_parseAsync_test.res @@ -1,16 +1,16 @@ -// open Ava -// open RescriptCore - -// let validAsyncRefine = S.transform(_, _ => { -// asyncParser: value => () => value->Promise.resolve, -// }) -// let invalidSyncRefine = S.refine(_, s => _ => s.fail("Sync user error")) -// let unresolvedPromise = Promise.make((_, _) => ()) -// let makeInvalidPromise = (s: S.s<'a>) => -// Promise.resolve()->Promise.then(() => s.fail("Async user error")) -// let invalidAsyncRefine = S.transform(_, s => { -// asyncParser: _ => () => makeInvalidPromise(s), -// }) +open Ava +open RescriptCore + +let validAsyncRefine = S.transform(_, _ => { + asyncParser: value => () => value->Promise.resolve, +}) +let invalidSyncRefine = S.refine(_, s => _ => s.fail("Sync user error")) +let unresolvedPromise = Promise.make((_, _) => ()) +let makeInvalidPromise = (s: S.s<'a>) => + Promise.resolve()->Promise.then(() => s.fail("Async user error")) +let invalidAsyncRefine = S.transform(_, s => { + asyncParser: _ => () => makeInvalidPromise(s), +}) // asyncTest("Successfully parses without asyncRefine", t => { // let schema = S.string @@ -461,116 +461,119 @@ // }) // } -// module Union = { -// Failing.asyncTest("[Union] Successfully parses", t => { -// let schema = S.union([S.literal(1), S.literal(2)->validAsyncRefine, S.literal(3)]) - -// Promise.all([ -// (1->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { -// t->Assert.deepEqual(result, Ok(1), ()) -// }), -// (2->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { -// t->Assert.deepEqual(result, Ok(2), ()) -// }), -// (3->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { -// t->Assert.deepEqual(result, Ok(3), ()) -// }), -// ])->Promise.thenResolve(_ => ()) -// }) - -// test("[Union] Passes with Parse operation. Async item should fail", t => { -// let schema = S.union([S.literal(2)->validAsyncRefine, S.literal(2), S.literal(3)]) - -// t->Assert.deepEqual(2->S.parseAnyWith(schema), Ok(2), ()) -// }) - -// test("[Union] Fails with Parse operation", t => { -// let schema = S.union([S.literal(1), S.literal(2)->validAsyncRefine, S.literal(3)]) - -// t->U.assertErrorResult( -// 2->S.parseAnyWith(schema), -// { -// code: InvalidUnion([ -// U.error({ -// code: InvalidLiteral({expected: S.Literal.parse(1.), received: %raw("2")}), -// path: S.Path.empty, -// operation: Parse, -// }), -// U.error({ -// code: UnexpectedAsync, -// path: S.Path.empty, -// operation: Parse, -// }), -// U.error({ -// code: InvalidLiteral({expected: S.Literal.parse(3.), received: %raw("2")}), -// path: S.Path.empty, -// operation: Parse, -// }), -// ]), -// operation: Parse, -// path: S.Path.empty, -// }, -// ) -// }) - -// Failing.asyncTest( -// "[Union] Doesn't return sync error when fails to parse sync part of async item", -// t => { -// let schema = S.union([S.literal(1), S.literal(2)->validAsyncRefine, S.literal(3)]) -// let input = %raw("true") - -// (input->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { -// t->U.assertErrorResult( -// result, -// { -// code: InvalidUnion([ -// U.error({ -// code: InvalidLiteral({expected: S.Literal.parse(1.), received: input}), -// path: S.Path.empty, -// operation: ParseAsync, -// }), -// U.error({ -// code: InvalidLiteral({expected: S.Literal.parse(2.), received: input}), -// path: S.Path.empty, -// operation: ParseAsync, -// }), -// U.error({ -// code: InvalidLiteral({expected: S.Literal.parse(3.), received: input}), -// path: S.Path.empty, -// operation: ParseAsync, -// }), -// ]), -// operation: ParseAsync, -// path: S.Path.empty, -// }, -// ) -// }) -// }, -// ) - -// Failing.test("[Union] Parses async items in parallel", t => { -// let actionCounter = ref(0) - -// let schema = S.union([ -// S.literal(2)->S.transform(_ => { -// asyncParser: _ => () => { -// actionCounter.contents = actionCounter.contents + 1 -// unresolvedPromise -// }, -// }), -// S.literal(2)->S.transform(_ => { -// asyncParser: _ => () => { -// actionCounter.contents = actionCounter.contents + 1 -// unresolvedPromise -// }, -// }), -// ]) - -// 2->S.parseAnyAsyncWith(schema)->ignore - -// t->Assert.deepEqual(actionCounter.contents, 2, ()) -// }) -// } +module Union = { + // Failing.asyncTest("[Union] Successfully parses", t => { + // let schema = S.union([S.literal(1), S.literal(2)->validAsyncRefine, S.literal(3)]) + + // Promise.all([ + // (1->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { + // t->Assert.deepEqual(result, Ok(1), ()) + // }), + // (2->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { + // t->Assert.deepEqual(result, Ok(2), ()) + // }), + // (3->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { + // t->Assert.deepEqual(result, Ok(3), ()) + // }), + // ])->Promise.thenResolve(_ => ()) + // }) + + test("[Union] Passes with Parse operation. Async item should fail", t => { + let schema = S.union([S.literal(2)->validAsyncRefine, S.literal(2), S.literal(3)]) + + t->Assert.deepEqual(2->S.parseAnyWith(schema), Ok(2), ()) + }) + + test("[Union] Fails with Parse operation", t => { + let schema = S.union([ + S.literal(2)->validAsyncRefine, + S.literal(2)->validAsyncRefine, + S.literal(3), + ]) + + t->U.assertErrorResult( + 2->S.parseAnyWith(schema), + { + code: InvalidUnion([ + U.error({ + code: UnexpectedAsync, + path: S.Path.empty, + operation: Parse, + }), + U.error({ + code: UnexpectedAsync, + path: S.Path.empty, + operation: Parse, + }), + ]), + operation: Parse, + path: S.Path.empty, + }, + ) + }) + + // Failing.asyncTest( + // "[Union] Doesn't return sync error when fails to parse sync part of async item", + // t => { + // let schema = S.union([S.literal(1), S.literal(2)->validAsyncRefine, S.literal(3)]) + // let input = %raw("true") + + // (input->S.parseAnyAsyncInStepsWith(schema)->Result.getExn)()->Promise.thenResolve(result => { + // t->U.assertErrorResult( + // result, + // { + // code: InvalidUnion([ + // U.error({ + // code: InvalidLiteral({expected: S.Literal.parse(1.), received: input}), + // path: S.Path.empty, + // operation: ParseAsync, + // }), + // U.error({ + // code: InvalidLiteral({expected: S.Literal.parse(2.), received: input}), + // path: S.Path.empty, + // operation: ParseAsync, + // }), + // U.error({ + // code: InvalidLiteral({expected: S.Literal.parse(3.), received: input}), + // path: S.Path.empty, + // operation: ParseAsync, + // }), + // ]), + // operation: ParseAsync, + // path: S.Path.empty, + // }, + // ) + // }) + // }, + // ) + + test("[Union] Parses async items in serial", t => { + let actionCounter = ref(0) + + let schema = S.union([ + S.literal(2)->S.transform(_ => { + asyncParser: _ => { + () => { + actionCounter.contents = actionCounter.contents + 1 + unresolvedPromise + } + }, + }), + S.literal(2)->S.transform(_ => { + asyncParser: _ => { + () => { + actionCounter.contents = actionCounter.contents + 1 + unresolvedPromise + } + }, + }), + ]) + + 2->S.parseAnyAsyncWith(schema)->ignore + + t->Assert.deepEqual(actionCounter.contents, 1, ()) + }) +} // module Array = { // asyncTest("[Array] Successfully parses", t => { @@ -935,4 +938,3 @@ // ) // }) // } - diff --git a/packages/tests/src/core/S_union_test.res b/packages/tests/src/core/S_union_test.res index 01e161ca..bc7f9f43 100644 --- a/packages/tests/src/core/S_union_test.res +++ b/packages/tests/src/core/S_union_test.res @@ -66,7 +66,7 @@ test("Parses when both schemas misses parser and have the same type", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{if(typeof i!=="string"){e[0](i)}else{e[5]([e[2],e[4],]);}return undefined}`, + `i=>{if(typeof i!=="string"){e[0](i)}else{e[5]([e[2],e[4],]);}return i}`, ) }) @@ -100,7 +100,7 @@ test("Parses when both schemas misses parser and have different types", t => { 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}`, + `i=>{if(!(i==="apple")){if(typeof i!=="string"){e[0](i)}else{throw e[1]}}else{throw e[3]}return i}`, ) }) @@ -141,7 +141,7 @@ test("Parses when second struct misses parser", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0;if(!(i==="apple")){if(typeof i!=="string"){e[0](i)}else{throw e[1]}}else{v0=i}return v0}`, + `i=>{if(!(i==="apple")){if(typeof i!=="string"){e[0](i)}else{throw e[1]}}else{i==="apple"||e[2](i);}return i}`, ) }) @@ -405,7 +405,7 @@ module Advanced = { t->U.assertCompiledCode( ~schema=shapeSchema, ~op=#Parse, - `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}`, + `i=>{let v2=i;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}`, ) }) @@ -441,22 +441,21 @@ test("Compiled parse code snapshot", t => { t->U.assertCompiledCode( ~schema, ~op=#Parse, - `i=>{let v0;if(!(i===0)){if(!(i===1)){e[0](i)}else{v0=i}}else{v0=i}return v0}`, + `i=>{if(!(i===0)){if(!(i===1)){e[0](i)}else{i===1||e[1](i);}}else{i===0||e[2](i);}return i}`, ) }) -// It shouldn't compile since it throw InvalidOperation error -Failing.test("Compiled async parse code snapshot", _t => { - let _schema = S.union([ +test("Compiled async parse code snapshot", t => { + let schema = S.union([ S.literal(0)->S.transform(_ => {asyncParser: i => () => Promise.resolve(i)}), S.literal(1), ]) - // t->U.assertCompiledCode( - // ~schema, - // ~op=#Parse, - // `i=>{let v0=e[1](i),v1;try{i===0||e[0](i);throw v0}catch(v2){if(v2&&v2.s===s||v2===v0){try{i===1||e[2](i);v1=()=>Promise.resolve(i)}catch(v3){if(v3&&v3.s===s){v1=()=>Promise.any([v2===v0?v2():Promise.reject(v2),Promise.reject(v3)]).catch(t=>{e[3](t.errors)})}else{throw v3}}}else{throw v2}}return v1}`, - // ) + t->U.assertCompiledCode( + ~schema, + ~op=#Parse, + `i=>{let v0=i;if(!(i===0)){if(!(i===1)){e[0](i)}else{i===1||e[1](i);}}else{i===0||e[2](i);v0=e[3](i)}return Promise.resolve(v0)}`, + ) }) test("Compiled serialize code snapshot", t => { diff --git a/src/S_Core.bs.mjs b/src/S_Core.bs.mjs index fe0e0940..250f6723 100644 --- a/src/S_Core.bs.mjs +++ b/src/S_Core.bs.mjs @@ -1930,7 +1930,6 @@ function factory$7(definer) { function genericParse(b, schemas, input, output, path) { var codeEndRef = ""; var errorCodeRef = ""; - var isAsync = false; for(var idx = 0 ,idx_finish = schemas.length; idx < idx_finish; ++idx){ var prevCode = b.c; try { @@ -1938,9 +1937,6 @@ function genericParse(b, schemas, input, output, path) { var errorVar = "e" + idx; b.c = b.c + "try{"; var itemOutput = parseWithTypeCheck(b, schema, input, ""); - if (itemOutput.a) { - isAsync = true; - } b.c = b.c + (set(b, output, itemOutput) + "}catch(" + errorVar + "){"); codeEndRef = codeEndRef + "}"; errorCodeRef = errorCodeRef + errorVar + ","; @@ -1952,9 +1948,6 @@ function genericParse(b, schemas, input, output, path) { b.c = prevCode; } } - if (isAsync) { - invalidOperation(b, path, "S.union doesn't support async items. Please create an issue to rescript-schema if you nead the feature"); - } b.c = b.c + failWithArg(b, path, (function (internalErrors) { return { TAG: "InvalidUnion", @@ -2010,7 +2003,7 @@ function factory$8(schemas) { groupsByTypeFilter[typeFilterCode] = [schema]; } } - var output = allocateVal(b); + var output = val(b, inputVar); var loopTypeFilters = function (idx) { var isLastItem = idx === (typeFilters.length - 1 | 0); var typeFilterCode = typeFilters[idx]; @@ -2034,7 +2027,11 @@ function factory$8(schemas) { var schema = schemas[0]; var prevCode = b.c; try { - b.c = b.c + set(b, output, schema.p(b, input, schema, path)); + var schemaOutput = schema.p(b, input, schema, path); + if (schemaOutput !== input) { + b.c = b.c + set(b, output, schemaOutput); + } + } catch (raw_exn){ var exn = Caml_js_exceptions.internalToOCamlException(raw_exn); @@ -2045,7 +2042,11 @@ function factory$8(schemas) { b.c = b.c + "}"; }; loopTypeFilters(0); - return output; + if (output.a) { + return asyncVal(b, "Promise.resolve(" + inline(b, output) + ")"); + } else { + return output; + } }), (function (b, input, selfSchema, path) { var schemas = selfSchema.r._0; var output = allocateVal(b); diff --git a/src/S_Core.res b/src/S_Core.res index d32e1ab5..df1cc381 100644 --- a/src/S_Core.res +++ b/src/S_Core.res @@ -2844,7 +2844,6 @@ module Union = { let genericParse = (b, ~schemas, ~input, ~output, ~path) => { 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 { @@ -2854,9 +2853,6 @@ module Union = { 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 ++ "}" @@ -2869,13 +2865,6 @@ module Union = { } } - 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( @@ -2930,7 +2919,7 @@ module Union = { } } - let output = b->B.allocateVal + let output = b->B.val(inputVar) let rec loopTypeFilters = idx => { let isLastItem = idx === typeFilters->Js.Array2.length - 1 @@ -2957,7 +2946,10 @@ module Union = { | [schema] => let prevCode = b.code try { - b.code = b.code ++ b->B.Val.set(output, b->B.parse(~schema, ~input, ~path)) + let schemaOutput = b->B.parse(~schema, ~input, ~path) + if schemaOutput !== input { + b.code = b.code ++ b->B.Val.set(output, schemaOutput) + } } catch { | exn => b.code = prevCode ++ "throw " ++ b->B.embed(exn->InternalError.getOrRethrow) } @@ -2967,7 +2959,11 @@ module Union = { } loopTypeFilters(0) - output + if output.isAsync { + b->B.asyncVal(`Promise.resolve(${b->B.Val.inline(output)})`) + } else { + output + } }), ~serializeOperationBuilder=Builder.make((b, ~input, ~selfSchema, ~path) => { let schemas = selfSchema->classify->unsafeGetVariantPayload