Skip to content

Commit

Permalink
Fix tuple parsing nested in an object schema
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Oct 11, 2024
1 parent e90bfce commit 64b5b1d
Show file tree
Hide file tree
Showing 9 changed files with 113 additions and 54 deletions.
1 change: 1 addition & 0 deletions IDEAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ let trimContract: S.contract<string => string> = S.contract(s => {
- Reverse operations for schemas where a single field is used multiple times
- S.to/S.variant don't allow to distructure tuples anymore
- Shorthand version for S.schema in Js/Ts API TODO: Update docs to use it by default
- Fix tuple parsing nested in an object schema

## v10

Expand Down
37 changes: 27 additions & 10 deletions packages/tests/src/core/S_object_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -1071,7 +1071,7 @@ test("Object schema parsing checks order", t => {
})

module Compiled = {
test("Compiled parse code snapshot for simple object", t => {
test("Compiled code snapshot for simple object", t => {
let schema = S.object(s =>
{
"foo": s.field("foo", S.string),
Expand All @@ -1084,35 +1084,52 @@ module Compiled = {
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[2](i)}let v0=i["foo"],v1=i["bar"];if(typeof v0!=="string"){e[0](v0)}if(typeof v1!=="boolean"){e[1](v1)}return {"foo":v0,"bar":v1}}`,
)
t->U.assertCompiledCode(~schema, ~op=#Serialize, `i=>{return {"foo":i["foo"],"bar":i["bar"]}}`)
})

test("Compiled parse code snapshot for simple object with async", t => {
test("Compiled code snapshot for refined nested object", t => {
let schema = S.object(s =>
{
"foo": s.field(
"foo",
S.unknown->S.transform(_ => {asyncParser: i => () => Promise.resolve(i)}),
"foo": s.field("foo", S.literal(12)),
"bar": s.field(
"bar",
S.object(
s => {
{"baz": s.field("baz", S.string)}
},
)->S.refine(_ => _ => ()),
),
"bar": s.field("bar", S.bool),
}
)

t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[2](i)}let v0=i["bar"];if(typeof v0!=="boolean"){e[1](v0)}return Promise.all([e[0](i["foo"])]).then(a=>({"foo":a[0],"bar":v0}))}`,
`i=>{if(!i||i.constructor!==Object){e[4](i)}let v0=i["foo"],v1=i["bar"];if(v0!==12){e[0](v0)}if(!v1||v1.constructor!==Object){e[1](v1)}let v2=v1["baz"],v3;if(typeof v2!=="string"){e[2](v2)}v3={"baz":v2};e[3](v3);return {"foo":v0,"bar":v3}}`,
)
t->U.assertCompiledCode(
~schema,
~op=#Serialize,
`i=>{let v0=i["bar"];e[0](v0);return {"foo":i["foo"],"bar":{"baz":v0["baz"]}}}`, // FIXME: Should validate literal here
)
})

test("Compiled serialize code snapshot for simple object", t => {
test("Compiled parse code snapshot for simple object with async", t => {
let schema = S.object(s =>
{
"foo": s.field("foo", S.string),
"foo": s.field(
"foo",
S.unknown->S.transform(_ => {asyncParser: i => () => Promise.resolve(i)}),
),
"bar": s.field("bar", S.bool),
}
)

t->U.assertCompiledCode(~schema, ~op=#Serialize, `i=>{return {"foo":i["foo"],"bar":i["bar"]}}`)
t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[2](i)}let v0=i["bar"];if(typeof v0!=="boolean"){e[1](v0)}return Promise.all([e[0](i["foo"])]).then(a=>({"foo":a[0],"bar":v0}))}`,
)
})

test("Compiled parse code snapshot for simple object with strict unknown keys", t => {
Expand Down
6 changes: 3 additions & 3 deletions packages/tests/src/core/S_recursive_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ test("Parses multiple nested recursive object inside of another object", t => {
t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[8](i)}let v0=i["recursive1"],v9,v10=i["recursive2"],v19;if(!v0||v0.constructor!==Object){e[0](v0)}let r0=v0=>{let v1=v0["Id"],v2=v0["Children"],v7=[];if(typeof v1!=="string"){e[1](v1)}if(!Array.isArray(v2)){e[2](v2)}for(let v3=0;v3<v2.length;++v3){let v5=v2[v3],v6;try{if(!v5||v5.constructor!==Object){e[3](v5)}v6=r0(v5)}catch(v4){if(v4&&v4.s===s){v4.path="[\\"Children\\"]"+\'["\'+v3+\'"]\'+v4.path}throw v4}v7.push(v6)}return {"id":v1,"children":v7}};try{v9=r0(v0)}catch(v8){if(v8&&v8.s===s){v8.path="[\\"recursive1\\"]"+v8.path}throw v8}if(!v10||v10.constructor!==Object){e[4](v10)}let r1=v10=>{let v11=v10["Id"],v12=v10["Children"],v17=[];if(typeof v11!=="string"){e[5](v11)}if(!Array.isArray(v12)){e[6](v12)}for(let v13=0;v13<v12.length;++v13){let v15=v12[v13],v16;try{if(!v15||v15.constructor!==Object){e[7](v15)}v16=r1(v15)}catch(v14){if(v14&&v14.s===s){v14.path="[\\"Children\\"]"+\'["\'+v13+\'"]\'+v14.path}throw v14}v17.push(v16)}return {"id":v11,"children":v17}};try{v19=r1(v10)}catch(v18){if(v18&&v18.s===s){v18.path="[\\"recursive2\\"]"+v18.path}throw v18}return {"recursive1":v9,"recursive2":v19}}`,
`i=>{if(!i||i.constructor!==Object){e[8](i)}let v0=i["recursive1"],v10=i["recursive2"];if(!v0||v0.constructor!==Object){e[0](v0)}let v9;let r0=v0=>{let v1=v0["Id"],v2=v0["Children"];if(typeof v1!=="string"){e[1](v1)}if(!Array.isArray(v2)){e[2](v2)}let v7=[];for(let v3=0;v3<v2.length;++v3){let v5=v2[v3],v6;try{if(!v5||v5.constructor!==Object){e[3](v5)}v6=r0(v5)}catch(v4){if(v4&&v4.s===s){v4.path="[\\"Children\\"]"+\'["\'+v3+\'"]\'+v4.path}throw v4}v7.push(v6)}return {"id":v1,"children":v7}};try{v9=r0(v0)}catch(v8){if(v8&&v8.s===s){v8.path="[\\"recursive1\\"]"+v8.path}throw v8}if(!v10||v10.constructor!==Object){e[4](v10)}let v19;let r1=v10=>{let v11=v10["Id"],v12=v10["Children"];if(typeof v11!=="string"){e[5](v11)}if(!Array.isArray(v12)){e[6](v12)}let v17=[];for(let v13=0;v13<v12.length;++v13){let v15=v12[v13],v16;try{if(!v15||v15.constructor!==Object){e[7](v15)}v16=r1(v15)}catch(v14){if(v14&&v14.s===s){v14.path="[\\"Children\\"]"+\'["\'+v13+\'"]\'+v14.path}throw v14}v17.push(v16)}return {"id":v11,"children":v17}};try{v19=r1(v10)}catch(v18){if(v18&&v18.s===s){v18.path="[\\"recursive2\\"]"+v18.path}throw v18}return {"recursive1":v9,"recursive2":v19}}`,
)

t->Assert.deepEqual(
Expand Down Expand Up @@ -510,7 +510,7 @@ asyncTest("Successfully parses recursive object with async parse function", t =>
t->U.assertCompiledCode(
~schema=nodeSchema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[4](i)}let r0=i=>{let v0=i["Id"],v1=i["Children"],v6=[];if(typeof v0!=="string"){e[0](v0)}if(!Array.isArray(v1)){e[2](v1)}for(let v2=0;v2<v1.length;++v2){let v4=v1[v2],v5;try{if(!v4||v4.constructor!==Object){e[3](v4)}v5=r0(v4).catch(v3=>{if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3})}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v6.push(v5)}return Promise.all([e[1](v0),Promise.all(v6)]).then(a=>({"id":a[0],"children":a[1]}))};return r0(i)}`,
`i=>{if(!i||i.constructor!==Object){e[4](i)}let r0=i=>{let v0=i["Id"],v1=i["Children"];if(typeof v0!=="string"){e[0](v0)}if(!Array.isArray(v1)){e[2](v1)}let v6=[];for(let v2=0;v2<v1.length;++v2){let v4=v1[v2],v5;try{if(!v4||v4.constructor!==Object){e[3](v4)}v5=r0(v4).catch(v3=>{if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3})}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v6.push(v5)}return Promise.all([e[1](v0),Promise.all(v6)]).then(a=>({"id":a[0],"children":a[1]}))};return r0(i)}`,
)

%raw(`{
Expand Down Expand Up @@ -586,6 +586,6 @@ test("Compiled parse code snapshot", t => {
t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[3](i)}let r0=i=>{let v0=i["Id"],v1=i["Children"],v6=[];if(typeof v0!=="string"){e[0](v0)}if(!Array.isArray(v1)){e[1](v1)}for(let v2=0;v2<v1.length;++v2){let v4=v1[v2],v5;try{if(!v4||v4.constructor!==Object){e[2](v4)}v5=r0(v4)}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v6.push(v5)}return {"id":v0,"children":v6}};return r0(i)}`,
`i=>{if(!i||i.constructor!==Object){e[3](i)}let r0=i=>{let v0=i["Id"],v1=i["Children"];if(typeof v0!=="string"){e[0](v0)}if(!Array.isArray(v1)){e[1](v1)}let v6=[];for(let v2=0;v2<v1.length;++v2){let v4=v1[v2],v5;try{if(!v4||v4.constructor!==Object){e[2](v4)}v5=r0(v4)}catch(v3){if(v3&&v3.s===s){v3.path="[\\"Children\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v6.push(v5)}return {"id":v0,"children":v6}};return r0(i)}`,
)
})
2 changes: 1 addition & 1 deletion packages/tests/src/core/S_refine_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ module Issue79 = {
t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{if(!i||i.constructor!==Object){e[2](i)}let v0=i["myField"],v2,v3;if(v0!==void 0&&(v0!==null&&(typeof v0!=="string"))){e[0](v0)}if(v0!==void 0){let v1;if(v0!==null){v1=v0}else{v1=void 0}v2=v1}v3=v2;e[1](v3);return v3}`,
`i=>{if(!i||i.constructor!==Object){e[2](i)}let v0=i["myField"],v3;if(v0!==void 0&&(v0!==null&&(typeof v0!=="string"))){e[0](v0)}let v2;if(v0!==void 0){let v1;if(v0!==null){v1=v0}else{v1=void 0}v2=v1}v3=v2;e[1](v3);return v3}`,
)

t->Assert.deepEqual(jsonString->S.parseJsonStringWith(schema), Ok(Some("test")), ())
Expand Down
59 changes: 37 additions & 22 deletions packages/tests/src/core/S_schema_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -84,29 +84,44 @@ test("Tuple with embeded", t => {
})

test("Nested embeded object", t => {
t->U.assertEqualSchemas(
S.schema(s =>
{
"nested": {
"foo": "bar",
"zoo": s.matches(S.int),
},
}
),
S.object(s =>
{
"nested": s.field(
"nested",
S.object(
s =>
{
"foo": s.field("foo", S.literal("bar")),
"zoo": s.field("zoo", S.int),
},
),
let schema = S.schema(s =>
{
"nested": {
"foo": "bar",
"zoo": s.matches(S.int),
},
}
)
let objectSchema = S.object(s =>
{
"nested": s.field(
"nested",
S.object(
s =>
{
"foo": s.field("foo", S.literal("bar")),
"zoo": s.field("zoo", S.int),
},
),
}
),
),
}
)
t->U.assertEqualSchemas(schema, objectSchema)

t->Assert.is(
schema->U.getCompiledCodeString(~op=#Parse),
objectSchema->U.getCompiledCodeString(~op=#Parse),
(),
)
t->Assert.is(
schema->U.getCompiledCodeString(~op=#Serialize),
`i=>{let v0=i["nested"];let v1=v0["foo"];if(v1!=="bar"){e[0](v1)}return {"nested":{"foo":v1,"zoo":v0["zoo"]}}}`,
(),
)
t->Assert.is(
objectSchema->U.getCompiledCodeString(~op=#Serialize),
`i=>{return {"nested":{"foo":i["nested"]["foo"],"zoo":i["nested"]["zoo"]}}}`, // FIXME: validate literals for S.tuple schemas
(),
)
})

Expand Down
28 changes: 28 additions & 0 deletions packages/tests/src/core/S_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,34 @@ test("Successfully parses tagged object", (t) => {
>(true);
});

test("Successfully parses and reverse convert object with optional field", (t) => {
const schema = S.object({
bar: S.optional(S.boolean),
});
const value = S.parseWith({}, schema);
t.deepEqual(value, { bar: undefined });

const reversed = S.convertWith(value, S.reverse(schema));
t.deepEqual(reversed, { bar: undefined });

expectType<
TypeEqual<
typeof schema,
S.Schema<{
bar: boolean | undefined;
}>
>
>(true);
expectType<
TypeEqual<
typeof value,
{
bar: boolean | undefined;
}
>
>(true);
});

test("Successfully parses object with field names transform", (t) => {
const schema = S.object((s) => ({
foo: s.field("Foo", S.string),
Expand Down
2 changes: 1 addition & 1 deletion packages/tests/src/core/S_union_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -610,7 +610,7 @@ module CrazyUnion = {
t->U.assertCompiledCode(
~schema,
~op=#Parse,
`i=>{let r0=i=>{let v6=i;if(!i||i.constructor!==Object){if(i!=="B"){if(i!=="C"){if(i!=="D"){if(i!=="E"){if(i!=="F"){if(i!=="G"){if(i!=="H"){if(i!=="I"){if(i!=="J"){if(i!=="K"){if(i!=="L"){if(i!=="M"){if(i!=="N"){if(i!=="O"){if(i!=="P"){if(i!=="Q"){if(i!=="R"){if(i!=="S"){if(i!=="T"){if(i!=="U"){if(i!=="V"){if(i!=="W"){if(i!=="X"){if(i!=="Y"){e[7](i)}}}}}}}}}}}}}}}}}}}}}}}}}else{try{let v0=i["type"],v1=i["nested"],v5=[];if(v0!=="A"){e[0](v0)}if(!Array.isArray(v1)){e[1](v1)}for(let v2=0;v2<v1.length;++v2){let v4;try{v4=r0(v1[v2])}catch(v3){if(v3&&v3.s===s){v3.path="[\\"nested\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v5.push(v4)}v6={"TAG":e[2],"_0":v5}}catch(e0){try{let v7=i["type"],v8=i["nested"],v12=[];if(v7!=="Z"){e[3](v7)}if(!Array.isArray(v8)){e[4](v8)}for(let v9=0;v9<v8.length;++v9){let v11;try{v11=r0(v8[v9])}catch(v10){if(v10&&v10.s===s){v10.path="[\\"nested\\"]"+\'["\'+v9+\'"]\'+v10.path}throw v10}v12.push(v11)}v6={"TAG":e[5],"_0":v12}}catch(e1){e[6]([e0,e1,])}}}return v6};return r0(i)}`,
`i=>{let r0=i=>{let v6=i;if(!i||i.constructor!==Object){if(i!=="B"){if(i!=="C"){if(i!=="D"){if(i!=="E"){if(i!=="F"){if(i!=="G"){if(i!=="H"){if(i!=="I"){if(i!=="J"){if(i!=="K"){if(i!=="L"){if(i!=="M"){if(i!=="N"){if(i!=="O"){if(i!=="P"){if(i!=="Q"){if(i!=="R"){if(i!=="S"){if(i!=="T"){if(i!=="U"){if(i!=="V"){if(i!=="W"){if(i!=="X"){if(i!=="Y"){e[7](i)}}}}}}}}}}}}}}}}}}}}}}}}}else{try{let v0=i["type"],v1=i["nested"];if(v0!=="A"){e[0](v0)}if(!Array.isArray(v1)){e[1](v1)}let v5=[];for(let v2=0;v2<v1.length;++v2){let v4;try{v4=r0(v1[v2])}catch(v3){if(v3&&v3.s===s){v3.path="[\\"nested\\"]"+\'["\'+v2+\'"]\'+v3.path}throw v3}v5.push(v4)}v6={"TAG":e[2],"_0":v5}}catch(e0){try{let v7=i["type"],v8=i["nested"];if(v7!=="Z"){e[3](v7)}if(!Array.isArray(v8)){e[4](v8)}let v12=[];for(let v9=0;v9<v8.length;++v9){let v11;try{v11=r0(v8[v9])}catch(v10){if(v10&&v10.s===s){v10.path="[\\"nested\\"]"+\'["\'+v9+\'"]\'+v10.path}throw v10}v12.push(v11)}v6={"TAG":e[5],"_0":v12}}catch(e1){e[6]([e0,e1,])}}}return v6};return r0(i)}`,
)
})

Expand Down
15 changes: 7 additions & 8 deletions src/S_Core.bs.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1555,18 +1555,17 @@ function processInputItems(b, ctx, input, schema, path) {
if (typeFilter !== undefined && (isLiteral || b.g.o & 1)) {
b.c = b.c + typeFilterCode(b, typeFilter, schema$1, itemInput, path$1);
}
var bb = scope(b);
if (isObject && schema$1.d) {
var bb = scope(b);
processInputItems(bb, ctx, itemInput, schema$1, path$1);
b.c = prevCode + b.c + allocateScope(bb);
} else {
var itemOutput = schema$1.b(b, itemInput, schema$1, path$1);
var itemOutput = schema$1.b(bb, itemInput, schema$1, path$1);
addItemOutput(b, ctx, item, itemOutput);
if (isLiteral) {
b.c = b.c + prevCode;
} else {
b.c = prevCode + b.c;
}
}
if (isLiteral) {
b.c = b.c + allocateScope(bb) + prevCode;
} else {
b.c = prevCode + b.c + allocateScope(bb);
}
}
if (!(isObject && ctx.s.t.unknownKeys === "Strict" && b.g.o & 1)) {
Expand Down
17 changes: 8 additions & 9 deletions src/S_Core.res
Original file line number Diff line number Diff line change
Expand Up @@ -2361,20 +2361,19 @@ module Object = {
| _ => ()
}

let bb = b->B.scope
if isObject && schema.definer->Obj.magic {
let bb = b->B.scope
bb->processInputItems(~ctx, ~input=itemInput, ~schema, ~path)
b.code = prevCode ++ b.code ++ bb->B.allocateScope
} else {
let itemOutput = b->B.parse(~schema, ~input=itemInput, ~path)
let itemOutput = bb->B.parse(~schema, ~input=itemInput, ~path)
b->BuildCtx.addItemOutput(~ctx, item, itemOutput)
}

// Parse literal fields first, because they are most often used as discriminants
if isLiteral {
b.code = b.code ++ prevCode
} else {
b.code = prevCode ++ b.code
}
// Parse literal fields first, because they are most often used as discriminants
if isLiteral {
b.code = b.code ++ bb->B.allocateScope ++ prevCode
} else {
b.code = prevCode ++ b.code ++ bb->B.allocateScope
}
}

Expand Down

2 comments on commit 64b5b1d

@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: 64b5b1d Previous: 90ab99c Ratio
Parse string 168056161 ops/sec (±0.36%) 811431552 ops/sec (±0.15%) 4.83
Reverse convert string 290645994 ops/sec (±0.14%)
Advanced object schema factory 452448 ops/sec (±0.83%) 472293 ops/sec (±0.54%) 1.04
Parse advanced object 52063556 ops/sec (±0.23%) 56799894 ops/sec (±0.57%) 1.09
Assert advanced object - compile 163767903 ops/sec (±0.18%)
Assert advanced object 166007071 ops/sec (±0.28%) 171920805 ops/sec (±0.20%) 1.04
Create and parse advanced object 92618 ops/sec (±0.52%) 95083 ops/sec (±0.21%) 1.03
Create and parse advanced object - with S.schema 101230 ops/sec (±0.35%)
Parse advanced strict object 24356520 ops/sec (±0.15%) 25471962 ops/sec (±0.17%) 1.05
Assert advanced strict object 28792865 ops/sec (±0.24%) 30460141 ops/sec (±0.20%) 1.06
Reverse convert advanced object 60146396 ops/sec (±0.23%)
Reverse convert advanced object - with S.schema 57830460 ops/sec (±0.37%)
Reverse convert advanced object - compile 438724562 ops/sec (±22.20%)

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: 64b5b1d Previous: 90ab99c Ratio
Parse string 168056161 ops/sec (±0.36%) 811431552 ops/sec (±0.15%) 4.83

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

Please sign in to comment.