Skip to content

Commit

Permalink
Add primitive async support for S.union
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Jul 12, 2024
1 parent 6f4f4ca commit ec6fe04
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 166 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
- 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:
- Change the default `unknownKeys` strategy for Object from `Strip` to `Strict`
- 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:

Expand Down
2 changes: 1 addition & 1 deletion packages/tests/src/benchmark/Benchmark.res
Original file line number Diff line number Diff line change
Expand Up @@ -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"]())
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/tests/src/core/Example_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -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;v3<v2.length;++v3){let v5=v2[v3];try{if(typeof v5!=="string"){e[3](v5)}}catch(v4){if(v4&&v4.s===s){v4.path="[\\"Tags\\"]"+\'["\'+v3+\'"]\'+v4.path}throw v4}}}if(!(v6==="G")){if(!(v6==="PG")){if(!(v6==="PG13")){if(!(v6==="R")){e[5](v6)}else{v7=v6}}else{v7=v6}}else{v7=v6}}else{v7=v6}if(v8!==void 0&&(typeof v8!=="number"||v8>2147483647||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;v3<v2.length;++v3){let v5=v2[v3];try{if(typeof v5!=="string"){e[3](v5)}}catch(v4){if(v4&&v4.s===s){v4.path="[\\"Tags\\"]"+\'["\'+v3+\'"]\'+v4.path}throw v4}}}if(!(v6==="G")){if(!(v6==="PG")){if(!(v6==="PG13")){if(!(v6==="R")){e[5](v6)}else{v6==="R"||e[6](v6);}}else{v6==="PG13"||e[7](v6);}}else{v6==="PG"||e[8](v6);}}else{v6==="G"||e[9](v6);}if(v7!==void 0&&(typeof v7!=="number"||v7>2147483647||v7<-2147483648||v7%1!==0)){e[10](v7)}return {"id":v0,"title":v1,"tags":v2===void 0?e[4]:v2,"rating":v6,"deprecatedAgeRestriction":v7,}}`,
)
})

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -935,4 +938,3 @@
// )
// })
// }

25 changes: 12 additions & 13 deletions packages/tests/src/core/S_union_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
)
})

Expand Down Expand Up @@ -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}`,
)
})

Expand Down Expand Up @@ -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}`,
)
})

Expand Down Expand Up @@ -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}`,
)
})

Expand Down Expand Up @@ -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 => {
Expand Down
Loading

2 comments on commit ec6fe04

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: ec6fe04 Previous: 06df40b Ratio
Parse string 819831313 ops/sec (±0.12%) 818372259 ops/sec (±0.13%) 1.00
Serialize string 819705108 ops/sec (±0.08%) 819110535 ops/sec (±0.05%) 1.00
Advanced object schema factory 468782 ops/sec (±0.12%) 449156 ops/sec (±0.56%) 0.96
Parse advanced object 56539664 ops/sec (±0.78%) 45100200 ops/sec (±0.16%) 0.80
Assert advanced object 173905544 ops/sec (±0.08%)
Create and parse advanced object 94035 ops/sec (±0.23%) 35041 ops/sec (±1.18%) 0.37
Parse advanced strict object 25417888 ops/sec (±0.29%) 22748126 ops/sec (±0.24%) 0.89
Assert advanced strict object 30664658 ops/sec (±0.56%)
Serialize advanced object 75744539 ops/sec (±0.24%) 806595235 ops/sec (±0.11%) 10.65

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Performance Alert ⚠️

Possible performance regression was detected for benchmark.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.50.

Benchmark suite Current: ec6fe04 Previous: 06df40b Ratio
Serialize advanced object 75744539 ops/sec (±0.24%) 806595235 ops/sec (±0.11%) 10.65

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.