diff --git a/CHANGELOG_NEXT.md b/CHANGELOG_NEXT.md index 62dd89f4..b44f2736 100644 --- a/CHANGELOG_NEXT.md +++ b/CHANGELOG_NEXT.md @@ -9,3 +9,4 @@ - 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. diff --git a/IDEAS.md b/IDEAS.md index 249e5a4a..de2b9504 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -26,7 +26,6 @@ let trimContract: S.contract string> = S.contract(s => { - Change operation to include AsyncParse and simplify init functions (throw when asyncTransfor applied for SyncParse) - Make S.serializeToJsonString super fast -- Make operations more treeshakable by starting passing the actual operation to the initialOperation function. Or add a condition (verify performance) - Rename `InvalidJsonStruct` error, since after `rescript-struct`->`rescript-schema` it became misleading - Add S.bigint diff --git a/packages/tests/src/core/S_Error_message_test.res b/packages/tests/src/core/S_Error_message_test.res index d85ddaa8..ee0802e2 100644 --- a/packages/tests/src/core/S_Error_message_test.res +++ b/packages/tests/src/core/S_Error_message_test.res @@ -67,7 +67,7 @@ test("UnexpectedAsync error", t => { operation: Parse, path: S.Path.empty, })->S.Error.message, - "Failed parsing at root. Reason: Encountered unexpected asynchronous transform or refine. Use S.parseAsyncWith instead of S.parseWith", + "Failed parsing at root. Reason: Encountered unexpected async transform or refine. Use ParseAsync operation instead", (), ) }) @@ -91,7 +91,7 @@ test("ExcessField error", t => { operation: Parse, path: S.Path.empty, })->S.Error.message, - `Failed parsing at root. Reason: Encountered disallowed excess key "unknownKey" on an object. Use Deprecated to ignore a specific field, or S.Object.strip to ignore excess keys completely`, + `Failed parsing at root. Reason: Encountered disallowed excess key "unknownKey" on an object`, (), ) }) diff --git a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_test.res b/packages/tests/src/core/S_parseAnyAsyncInStepsWith_test.res index bb66a98e..c9db25d1 100644 --- a/packages/tests/src/core/S_parseAnyAsyncInStepsWith_test.res +++ b/packages/tests/src/core/S_parseAnyAsyncInStepsWith_test.res @@ -478,17 +478,38 @@ module Union = { ])->Promise.thenResolve(_ => ()) }) - test("[Union] Fails to parse since it's not supported", t => { + 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->Assert.throws( - () => { - 1->S.parseAnyOrRaiseWith(schema) - }, - ~expectations={ - message: "Failed parsing at root. Reason: S.union doesn\'t support async items. Please create an issue to rescript-schema if you nead the feature", + 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, }, - (), ) }) diff --git a/packages/tests/src/core/S_test.ts b/packages/tests/src/core/S_test.ts index 28bd1c32..fb4e8372 100644 --- a/packages/tests/src/core/S_test.ts +++ b/packages/tests/src/core/S_test.ts @@ -693,7 +693,7 @@ test("Fails to parse strict object with exccess fields", (t) => { }, { name: "RescriptSchemaError", - message: `Failed parsing at root. Reason: Encountered disallowed excess key "bar" on an object. Use Deprecated to ignore a specific field, or S.Object.strip to ignore excess keys completely`, + message: `Failed parsing at root. Reason: Encountered disallowed excess key "bar" on an object`, } ); }); diff --git a/src/S_Core.bs.mjs b/src/S_Core.bs.mjs index 46f0d855..73816007 100644 --- a/src/S_Core.bs.mjs +++ b/src/S_Core.bs.mjs @@ -224,6 +224,10 @@ function transform(b, input, operation) { ) + "})"); } +function raise(b, code, path) { + throw new RescriptSchemaError(code, b.g.o, path); +} + function embedSyncOperation(b, input, fn) { return transform(b, input, (function (b, input) { return map(b, "e[" + (b.g.e.push(fn) - 1) + "]", input); @@ -231,6 +235,9 @@ function embedSyncOperation(b, input, fn) { } function embedAsyncOperation(b, input, fn) { + if (b.g.o !== "ParseAsync") { + raise(b, "UnexpectedAsync", ""); + } return transform(b, input, (function (b, input) { var val = map(b, "e[" + (b.g.e.push(fn) - 1) + "]", input); val.a = true; @@ -238,10 +245,6 @@ function embedAsyncOperation(b, input, fn) { })); } -function raise(b, code, path) { - throw new RescriptSchemaError(code, b.g.o, path); -} - function failWithArg(b, path, fn, arg) { return "e[" + (b.g.e.push(function (arg) { return raise(b, fn(arg), path); @@ -376,10 +379,6 @@ function noopOperation(i) { return i; } -function unexpectedAsyncOperation(param) { - throw new RescriptSchemaError("UnexpectedAsync", "Parse", ""); -} - function build(builder, schema, operation) { var b = { c: "", @@ -407,9 +406,6 @@ function build(builder, schema, operation) { } schema.i = output.a; } - if (operation === "Parse" && output.a) { - return unexpectedAsyncOperation; - } if (b.c === "" && output === input) { return noopOperation; } @@ -624,7 +620,7 @@ function isAsyncParse(schema) { return v; } try { - build(schema.p, schema, "Parse"); + build(schema.p, schema, "ParseAsync"); return schema.i; } catch (raw_exn){ @@ -2171,7 +2167,7 @@ function reason(error, nestedLevelOpt) { var nestedLevel = nestedLevelOpt !== undefined ? nestedLevelOpt : 0; var reason$1 = error.code; if (typeof reason$1 !== "object") { - return "Encountered unexpected asynchronous transform or refine. Use S.parseAsyncWith instead of S.parseWith"; + return "Encountered unexpected async transform or refine. Use ParseAsync operation instead"; } switch (reason$1.TAG) { case "OperationFailed" : @@ -2183,7 +2179,7 @@ function reason(error, nestedLevelOpt) { case "InvalidLiteral" : return "Expected " + reason$1.expected.s + ", received " + parseInternal(reason$1.received).s; case "ExcessField" : - return "Encountered disallowed excess key " + JSON.stringify(reason$1._0) + " on an object. Use Deprecated to ignore a specific field, or S.Object.strip to ignore excess keys completely"; + return "Encountered disallowed excess key " + JSON.stringify(reason$1._0) + " on an object"; case "InvalidUnion" : var lineBreak = "\n" + " ".repeat((nestedLevel << 1)); var reasonsDict = {}; diff --git a/src/S_Core.res b/src/S_Core.res index 625e398d..96b3dd76 100644 --- a/src/S_Core.res +++ b/src/S_Core.res @@ -547,6 +547,10 @@ module Builder = { } } + let raise = (b: b, ~code, ~path) => { + Stdlib.Exn.raiseAny(InternalError.make(~code, ~operation=b.global.operation, ~path)) + } + let embedSyncOperation = (b: b, ~input, ~fn: 'input => 'output) => { b->transform(~input, (b, ~input) => { b->Val.map(b->embed(fn), input) @@ -554,6 +558,9 @@ module Builder = { } let embedAsyncOperation = (b: b, ~input, ~fn: 'input => unit => promise<'output>) => { + if b.global.operation !== ParseAsync { + b->raise(~code=UnexpectedAsync, ~path=Path.empty) + } b->transform(~input, (b, ~input) => { let val = b->Val.map(b->embed(fn), input) val.isAsync = true @@ -561,10 +568,6 @@ module Builder = { }) } - let raise = (b: b, ~code, ~path) => { - Stdlib.Exn.raiseAny(InternalError.make(~code, ~operation=b.global.operation, ~path)) - } - let failWithArg = (b: b, ~path, fn: 'arg => errorCode, arg) => { `${b->embed(arg => { b->raise(~path, ~code=fn(arg)) @@ -721,11 +724,6 @@ module Builder = { let noopOperation = i => i->Obj.magic - let unexpectedAsyncOperation = _ => - Stdlib.Exn.raiseAny( - InternalError.make(~path=Path.empty, ~code=UnexpectedAsync, ~operation=Parse), - ) - @inline let intitialInputVar = "i" @@ -757,11 +755,7 @@ module Builder = { schema.isAsyncParse = Value(output.isAsync) } - // FIXME: - - if operation === Parse && output.isAsync { - unexpectedAsyncOperation - } else if b.code === "" && output === input { + if b.code === "" && output === input { noopOperation } else { let inlinedFunction = `${intitialInputVar}=>{${b.code}return ${b->B.Val.inline(output)}}` @@ -1059,7 +1053,7 @@ let isAsyncParse = schema => { switch schema.isAsyncParse { | Unknown => try { - let _ = schema.parseOperationBuilder->Builder.build(~schema, ~operation=Parse) + let _ = schema.parseOperationBuilder->Builder.build(~schema, ~operation=ParseAsync) schema.isAsyncParse->(Obj.magic: isAsyncParse => bool) } catch { | exn => { @@ -3138,9 +3132,9 @@ module Error = { switch error.code { | OperationFailed(reason) => reason | InvalidOperation({description}) => description - | UnexpectedAsync => "Encountered unexpected asynchronous transform or refine. Use S.parseAsyncWith instead of S.parseWith" + | UnexpectedAsync => "Encountered unexpected async transform or refine. Use ParseAsync operation instead" | ExcessField(fieldName) => - `Encountered disallowed excess key ${fieldName->Stdlib.Inlined.Value.fromString} on an object. Use Deprecated to ignore a specific field, or S.Object.strip to ignore excess keys completely` + `Encountered disallowed excess key ${fieldName->Stdlib.Inlined.Value.fromString} on an object` | InvalidType({expected, received}) => `Expected ${expected.name()}, received ${received->Literal.parse->Literal.toString}` | InvalidLiteral({expected, received}) =>