Skip to content

Commit

Permalink
Improve serializing by using experimental reverse schema under the ho…
Browse files Browse the repository at this point in the history
…od (#89)

* Change rawTagged field alias

* Add S.reverse function - wip, always returns self for now

* Rename internal functions

* Use makePrimitiveSchema helper

* Fix multiple reverse call

* Correct reverse for null

* Reverse schema for option and test improvements

* Reverse child schema for dict and array schema

* Support reverse for S.jsonString

* Support reverse schema for transform

* Add reverse support for object/tuple

* Add reverse support for variant

* Support reverse schema almost for everything

* Rename rawTagged to tagged

* User reversed schema for serializing

* Remove serialize builder

* Rename internal functions

* Improve option serializing

* Fix lint

* Serialize union by type

* Prepare repo for a release

* Improve serializing of option schemas

* Fix tests

* Improve test coverage and remove dead code

* Update documentation
  • Loading branch information
DZakh authored Sep 10, 2024
1 parent 617ba9f commit 9fde7c6
Show file tree
Hide file tree
Showing 46 changed files with 2,884 additions and 2,056 deletions.
39 changes: 0 additions & 39 deletions CHANGELOG_NEXT.md

This file was deleted.

19 changes: 15 additions & 4 deletions IDEAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@ let trimContract: S.contract<string => string> = S.contract(s => {

- Use internal transform for trim

- Move S.inline to a separate codegen module
## v9

## v8
- Add S.reverse
TODO:

- S.recursive
- S.schema
- memo

- async serializing support
- S.create / S.validate
- S.parseToJsonString
- Rename S.inline to S.toRescriptCode
- Add serializeToJsonString to js api

## v10

- Make S.serializeToJsonString super fast
- Add S.bigint
Expand All @@ -33,9 +46,7 @@ let trimContract: S.contract<string => string> = S.contract(s => {
- Codegen type
- Codegen schema using type
- Don't recreate the object, when nothing should be transformed - stop reallocate objects without transformations
- S.validateWith
- Make `error.reason` tree-shakeable
- Add serializeToJsonString to js api
- S.toJSON/S.castToJson ???
- s.optional for object
- S.produce
Expand Down
4 changes: 2 additions & 2 deletions docs/js-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,8 @@ An union represents a logical OR relationship. You can apply this concept to you

The schema function `union` creates an OR relationship between any number of schemas that you pass as the first argument in the form of an array. On validation, the schema returns the result of the first schema that was successfully validated.

> 🧠 Schemas are not guaranteed to be validated in the order they are passed to `S.union`. They are grouped by the input data type to optimise performance and improve error message. Schemas with unknown data typed validated the last.
```ts
// TypeScript type for reference:
// type Union = string | number;
Expand All @@ -429,8 +431,6 @@ stringOrNumberSchema.parseOrThrow("foo"); // passes
stringOrNumberSchema.parseOrThrow(14); // passes
```

If a bad input can be uniquely assigned to one of the schemas based on the data type, the result of that schema is returned. Otherwise, a general issue is returned that contains the issues of each schema as subissues.

### Discriminated unions

```typescript
Expand Down
8 changes: 6 additions & 2 deletions docs/rescript-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,12 @@ Circle({radius: 1})->S.serializeWith(schema)

`array<S.t<'value>> => S.t<'value>`

An union represents a logical OR relationship. You can apply this concept to your schemas with `S.union`. This is the best API to use for variants and polymorphic variants.

On validation, the `S.union` schema returns the result of the first item that was successfully validated.

> 🧠 Schemas are not guaranteed to be validated in the order they are passed to `S.union`. They are grouped by the input data type to optimise performance and improve error message. Schemas with unknown data typed validated the last.
```rescript
// TypeScript type for reference:
// type Shape =
Expand Down Expand Up @@ -751,8 +757,6 @@ Square({x: 2.})->S.serializeWith(shapeSchema)
// })
```

The `union` will test the input against each of the schemas in order and return the first value that validates successfully.

#### Enums

Also, you can describe enums using `S.union` together with `S.literal`.
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 @@ -77,6 +77,6 @@ test("Compiled serialize code snapshot", t => {
t->U.assertCompiledCode(
~schema=filmSchema,
~op=#Serialize,
`i=>{let v0=i["tags"],v1,v2=i["rating"],v3,v4=i["deprecatedAgeRestriction"],v5;if(v0!==void 0){v1=e[0](v0)}try{if(v2!=="G"){e[1](v2)}if(v2!=="G"){e[2](v2)}v3=v2}catch(e0){try{if(v2!=="PG"){e[3](v2)}if(v2!=="PG"){e[4](v2)}v3=v2}catch(e1){try{if(v2!=="PG13"){e[5](v2)}if(v2!=="PG13"){e[6](v2)}v3=v2}catch(e2){try{if(v2!=="R"){e[7](v2)}if(v2!=="R"){e[8](v2)}v3=v2}catch(e3){e[9]([e0,e1,e2,e3,])}}}}if(v4!==void 0){v5=e[10](v4)}return {"Id":i["id"],"Title":i["title"],"Tags":v1,"Rating":v3,"Age":v5,}}`,
`i=>{let v0=i["tags"],v3=i["rating"];if(v3!=="G"){if(v3!=="PG"){if(v3!=="PG13"){if(v3!=="R"){e[0](v3)}}}}return {"Id":i["id"],"Title":i["title"],"Tags":v0,"Rating":v3,"Age":i["deprecatedAgeRestriction"],}}`,
)
})
6 changes: 1 addition & 5 deletions packages/tests/src/core/S_Option_getOrWith_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,5 @@ test("Compiled async parse code snapshot", t => {
test("Compiled serialize code snapshot", t => {
let schema = S.bool->S.option->S.Option.getOrWith(() => false)

t->U.assertCompiledCode(
~schema,
~op=#Serialize,
`i=>{let v0;if(i!==void 0){v0=e[0](i)}return v0}`,
)
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})
6 changes: 1 addition & 5 deletions packages/tests/src/core/S_Option_getOr_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,5 @@ test("Compiled async parse code snapshot", t => {
test("Compiled serialize code snapshot", t => {
let schema = S.bool->S.option->S.Option.getOr(false)

t->U.assertCompiledCode(
~schema,
~op=#Serialize,
`i=>{let v0;if(i!==void 0){v0=e[0](i)}return v0}`,
)
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})
25 changes: 22 additions & 3 deletions packages/tests/src/core/S_array_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,41 @@ module CommonWithNested = {

test("Compiled serialize code snapshot", t => {
let schema = S.array(S.string)
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)

let schema = S.array(S.option(S.string))
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Compiled serialize code snapshot with transform", t => {
let schema = S.array(S.option(S.string))
let schema = S.array(S.null(S.string))

// TODO: Simplify
t->U.assertCompiledCode(
~schema,
~op=#Serialize,
`i=>{let v5=[];for(let v0=0;v0<i.length;++v0){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=e[0](v2)}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5.push(v4)}return v5}`,
`i=>{let v5=[];for(let v0=0;v0<i.length;++v0){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=v2}else{v3=null}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5.push(v4)}return v5}`,
)
})

test("Reverse to self", t => {
let schema = factory()
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = factory()
t->U.assertReverseParsesBack(schema, value)
})
}

test("Reverse child schema", t => {
let schema = S.array(S.null(S.string))
t->U.assertEqualSchemas(
schema->S.\"~experimantalReverse",
S.array(S.option(S.string))->S.toUnknown,
)
})

test("Successfully parses matrix", t => {
let schema = S.array(S.array(S.string))

Expand Down
10 changes: 10 additions & 0 deletions packages/tests/src/core/S_bool_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ module Common = {

t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Reverse schema to self", t => {
let schema = factory()
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = factory()
t->U.assertReverseParsesBack(schema, true)
})
}

test("Parses bool when JSON is true", t => {
Expand Down
11 changes: 11 additions & 0 deletions packages/tests/src/core/S_custom_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,17 @@ test("Correctly serializes custom schema", t => {
t->Assert.deepEqual(None->S.serializeToUnknownWith(schema), Ok(%raw(`null`)), ())
})

test("Reverses custom schema to unknown", t => {
let schema = nullableSchema(S.string)

t->U.assertEqualSchemas(schema->S.\"~experimantalReverse", S.unknown)
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = nullableSchema(S.string)
t->U.assertReverseParsesBack(schema, Some("abc"))
})

test("Fails to serialize with user error", t => {
let schema = S.custom("Test", s => {
serializer: _ => s.fail("User error"),
Expand Down
24 changes: 22 additions & 2 deletions packages/tests/src/core/S_dict_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,41 @@ module CommonWithNested = {

test("Compiled serialize code snapshot", t => {
let schema = S.dict(S.string)
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)

let schema = S.dict(S.option(S.string))
t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Compiled serialize code snapshot with transform", t => {
let schema = S.dict(S.option(S.string))
let schema = S.dict(S.null(S.string))

t->U.assertCompiledCode(
~schema,
~op=#Serialize,
`i=>{let v5={};for(let v0 in i){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=e[0](v2)}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5[v0]=v4}return v5}`,
`i=>{let v5={};for(let v0 in i){let v2=i[v0],v4;try{let v3;if(v2!==void 0){v3=v2}else{v3=null}v4=v3}catch(v1){if(v1&&v1.s===s){v1.path=""+\'["\'+v0+\'"]\'+v1.path}throw v1}v5[v0]=v4}return v5}`,
)
})

test("Reverse to self", t => {
let schema = factory()
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = factory()
t->U.assertReverseParsesBack(schema, value)
})
}

test("Reverse child schema", t => {
let schema = S.dict(S.null(S.string))
t->U.assertEqualSchemas(
schema->S.\"~experimantalReverse",
S.dict(S.option(S.string))->S.toUnknown,
)
})

test("Successfully parses dict with int keys", t => {
let schema = S.dict(S.string)

Expand Down
10 changes: 10 additions & 0 deletions packages/tests/src/core/S_float_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ module Common = {

t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Reverse schema to self", t => {
let schema = factory()
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = factory()
t->U.assertReverseParsesBack(schema, 123.3)
})
}

test("Successfully parses number with a fractional part", t => {
Expand Down
10 changes: 10 additions & 0 deletions packages/tests/src/core/S_int_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ module Common = {

t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Reverse schema to self", t => {
let schema = factory()
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = factory()
t->U.assertReverseParsesBack(schema, 123)
})
}

test("Fails to parse int when JSON is a number bigger than +2^31", t => {
Expand Down
10 changes: 10 additions & 0 deletions packages/tests/src/core/S_jsonString_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,13 @@ test("Compiled serialize code snapshot with space", t => {

t->U.assertCompiledCode(~schema, ~op=#Serialize, `i=>{return JSON.stringify(i,null,2)}`)
})

test("Reverse schema to the original schema", t => {
let schema = S.jsonString(S.bool)
t->U.assertEqualSchemas(schema->S.\"~experimantalReverse", S.bool->S.toUnknown)
})

test("Succesfully uses reversed schema for parsing back to initial value", t => {
let schema = S.jsonString(S.bool)
t->U.assertReverseParsesBack(schema, true)
})
25 changes: 23 additions & 2 deletions packages/tests/src/core/S_json_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,33 @@ test("Compiled parse code snapshot", t => {

test("Compiled parse code snapshot with validate=false", t => {
let schema = S.json(~validate=false)

t->U.assertCompiledCodeIsNoop(~schema, ~op=#Parse)
})

test("Compiled serialize code snapshot", t => {
let schema = S.json(~validate=true)

t->U.assertCompiledCodeIsNoop(~schema, ~op=#Serialize)
})

test("Reverse schema to S.json(~validate=false) with validate=true", t => {
let schema = S.json(~validate=true)
t->U.assertEqualSchemas(schema->S.\"~experimantalReverse", S.json(~validate=false)->S.toUnknown)
})

test("Succesfully uses reversed schema with validate=true for parsing back to initial value", t => {
let schema = S.json(~validate=true)
t->U.assertReverseParsesBack(schema, %raw(`{"foo":"bar"}`))
})

test("Reverse schema to self with validate=false", t => {
let schema = S.json(~validate=false)
t->Assert.is(schema->S.\"~experimantalReverse", schema->S.toUnknown, ())
})

test(
"Succesfully uses reversed schema with validate=false for parsing back to initial value",
t => {
let schema = S.json(~validate=false)
t->U.assertReverseParsesBack(schema, %raw(`{"foo":"bar"}`))
},
)
Loading

1 comment on commit 9fde7c6

@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: 9fde7c6 Previous: 83f51d8 Ratio
Parse string 818507445 ops/sec (±0.14%) 815414023 ops/sec (±0.49%) 1.00
Serialize string 819767154 ops/sec (±0.07%) 817783951 ops/sec (±0.11%) 1.00
Advanced object schema factory 475041 ops/sec (±0.73%) 483577 ops/sec (±0.73%) 1.02
Parse advanced object 57280049 ops/sec (±0.31%) 57257765 ops/sec (±0.43%) 1.00
Assert advanced object 173343548 ops/sec (±0.18%) 173214107 ops/sec (±0.13%) 1.00
Create and parse advanced object 94089 ops/sec (±1.15%) 94633 ops/sec (±0.16%) 1.01
Parse advanced strict object 25323138 ops/sec (±0.51%) 25373330 ops/sec (±0.31%) 1.00
Assert advanced strict object 30935350 ops/sec (±0.37%) 30917166 ops/sec (±0.16%) 1.00
Serialize advanced object 63300565 ops/sec (±3.85%) 76333117 ops/sec (±0.06%) 1.21

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

Please sign in to comment.