Skip to content

Commit

Permalink
Add S.merge
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Oct 12, 2023
1 parent 8455570 commit eb3cc15
Show file tree
Hide file tree
Showing 11 changed files with 367 additions and 2 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG_NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- Added table of contents and splitted documentation into multiple documents
- Improved tree-shaking
- Added `S.Error.reason`
- Added alpha version of `S.merge` helper (JS/TS api only)
- Added `S.name`/`S.setName` for JS/TS api
- Documented `S.classify`/`S.name`/`S.setName`

## Opt-in ppx support

Expand Down
3 changes: 1 addition & 2 deletions IDEAS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,13 @@ let trimContract: S.contract<string => string> = S.contract(s => {

## v5.1

- Error.reason
- ppx
- stop reallocate objects without transformations
- S.matcher
- S.validateWith
- S.toJSON/S.castToJson ???
- nestedField for object
- spread for object (intersection)
- s.spread for object
- S.produce
- S.mutator

Expand Down
39 changes: 39 additions & 0 deletions docs/js-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
- [Advanced object struct](#advanced-object-struct)
- [`Object.strict`](#objectstrict)
- [`Object.strip`](#objectstrip)
- [`merge`](#merge)
- [Arrays](#arrays)
- [Tuples](#tuples)
- [Advanced tuple struct](#advanced-tuple-struct)
Expand All @@ -36,6 +37,8 @@
- [`parseAsync`](#parseasync)
- [`serialize`](#serialize)
- [`serializeOrThrow`](#serializeorthrow)
- [`name`](#name)
- [`setName`](#setname)
- [Error handling](#error-handling)
- [Comparison](#comparison)

Expand Down Expand Up @@ -296,6 +299,20 @@ S.parseOrThrow(personStruct, {

You can use the `S.Object.strip` function to reset an object struct to the default behavior (stripping unrecognized keys).

### `merge`

You can add additional fields to an object schema with the `merge` function.

```ts
const baseTeacherStruct = S.object({ students: S.array(S.string) });
const hasIDStruct = S.object({ id: S.string });

const teacherStruct = S.merge(baseTeacherStruct, hasIDStruct);
type Teacher = S.Output<typeof teacherStruct>; // => { students: string[], id: string }
```

> 🧠 The function will throw if the structs share keys. The returned schema also inherits the "unknownKeys" policy (strip/strict) of B.
## Arrays

```ts
Expand Down Expand Up @@ -529,6 +546,28 @@ S.serializeOrThrow(userStruct, user); // => Input

The exception-based version of `S.serialize`.

### **`name`**

```ts
S.name(S.literal({ abc: 123 }));
// `Literal({"abc": 123})`
```

Used internally for readable error messages.

> 🧠 Subject to change
### **`setName`**

```ts
const struct = S.setName(S.literal({ abc: 123 }, "Abc"));

S.name(struct);
// `Abc`
```

You can customise a struct name using `S.setName`.

## Error handling

**rescript-struct** provides a subclass of Error called `S.Error`. It contains detailed information about the validation problem.
Expand Down
40 changes: 40 additions & 0 deletions docs/rescript-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
- [`serializeToJsonStringWith`](#serializetojsonstringwith)
- [`serializeOrRaiseWith`](#serializeorraisewith)
- [`serializeToUnknownOrRaiseWith`](#serializetounknownorraisewith)
- [`classify`](#classify)
- [`name`](#name)
- [`setName`](#setname)
- [Error handling](#error-handling)
- [`Error.make`](#errormake)
- [`Error.raise`](#errorraise)
Expand Down Expand Up @@ -1166,6 +1169,43 @@ try {

The exception-based version of `serializeToUnknownWith`.

### **`classify`**

`(S.t<'value>) => S.tagged`

```rescript
S.string->S.tagged
// String
```

This can be useful for building other tools like [`rescript-json-schema`](https://github.com/DZakh/rescript-json-schema).

### **`name`**

`(S.t<'value>) => string`

```rescript
S.literal({"abc": 123})->S.name
// `Literal({"abc": 123})`
```

Used internally for readable error messages.

> 🧠 Subject to change
### **`setName`**

`(S.t<'value>, string) => string`

```rescript
let struct = S.literal({"abc": 123})->S.setName("Abc")
struct->S.name
// `Abc`
```

You can customise a struct name using `S.setName`.

## Error handling

**rescript-struct** returns a result type with error `S.error` containing detailed information about the validation problems.
Expand Down
152 changes: 152 additions & 0 deletions packages/tests/src/core/S_JsApi_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,158 @@ test("Resets object strict mode with strip method", (t) => {
>(true);
});

test("Successfully parses intersected objects", (t) => {
const struct = S.merge(
S.object({
foo: S.string,
bar: S.boolean,
}),
S.object({
baz: S.string,
})
);

expectType<
TypeEqual<
typeof struct,
S.Struct<
{
foo: string;
bar: boolean;
} & {
baz: string;
},
Record<string, unknown>
>
>
>(true);

const result = S.parse(struct, {
foo: "bar",
bar: true,
});
if (result.success) {
t.fail("Should fail");
return;
}
t.is(
result.error.message,
`Failed parsing at ["baz"]. Reason: Expected String, received undefined`
);

const value = S.parseOrThrow(struct, {
foo: "bar",
baz: "baz",
bar: true,
});
t.deepEqual(value, {
foo: "bar",
baz: "baz",
bar: true,
});
});

test("Successfully parses intersected objects with transform", (t) => {
const struct = S.merge(
S.transform(
S.object({
foo: S.string,
bar: S.boolean,
}),
(obj) => ({
abc: obj.foo,
})
),
S.object({
baz: S.string,
})
);

expectType<
TypeEqual<
typeof struct,
S.Struct<
{
abc: string;
} & {
baz: string;
},
Record<string, unknown>
>
>
>(true);

const result = S.parse(struct, {
foo: "bar",
bar: true,
});
if (result.success) {
t.fail("Should fail");
return;
}
t.is(
result.error.message,
`Failed parsing at ["baz"]. Reason: Expected String, received undefined`
);

const value = S.parseOrThrow(struct, {
foo: "bar",
baz: "baz",
bar: true,
});
t.deepEqual(value, {
abc: "bar",
baz: "baz",
});
});

test("Fails to serialize merge. Not supported yet", (t) => {
const struct = S.merge(
S.object({
foo: S.string,
bar: S.boolean,
}),
S.object({
baz: S.string,
})
);

const result = S.serialize(struct, {
foo: "bar",
bar: true,
baz: "string",
});
if (result.success) {
t.fail("Should fail");
return;
}
t.is(
result.error.message,
`Failed serializing at root. Reason: The S.merge serializing is not supported yet`
);
});

test("Name of merge struct", (t) => {
const struct = S.merge(
S.object({
foo: S.string,
bar: S.boolean,
}),
S.object({
baz: S.string,
})
);

t.is(
S.name(struct),
`Object({"foo": String, "bar": Bool}) & Object({"baz": String})`
);
});

test("setName", (t) => {
t.is(S.name(S.setName(S.unknown, "BlaBla")), `BlaBla`);
});

test("Successfully parses and returns result", (t) => {
const struct = S.string;
const value = S.parse(struct, "123");
Expand Down
4 changes: 4 additions & 0 deletions packages/tests/src/core/S_name_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ test("Name of Literal struct", t => {
t->Assert.deepEqual(S.literal(123)->S.name, "Literal(123)", ())
})

test("Name of Literal object struct", t => {
t->Assert.deepEqual(S.literal({"abc": 123})->S.name, `Literal({"abc": 123})`, ())
})

test("Name of Array struct", t => {
t->Assert.deepEqual(S.array(S.string)->S.name, "Array(String)", ())
})
Expand Down
11 changes: 11 additions & 0 deletions src/S.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ export const Object: {
) => Struct<Output, Input>;
};

export function merge<O1, O2>(
struct1: Struct<O1, Record<string, unknown>>,
struct2: Struct<O2, Record<string, unknown>>
): Struct<O1 & O2, Record<string, unknown>>;

export const String: {
min: <Input>(
struct: Struct<string, Input>,
Expand Down Expand Up @@ -281,6 +286,12 @@ export function custom<Output, Input = unknown>(
serializer: (value: Output, s: EffectCtx<unknown, unknown>) => Input
): Struct<Output, Input>;

export function name(struct: Struct<unknown, unknown>): string;
export function setName<Output, Input>(
struct: Struct<Output, Input>,
name: string
): Struct<Output, Input>;

export function asyncParserRefine<Output, Input>(
struct: Struct<Output, Input>,
refiner: (value: Output, s: EffectCtx<Output, Input>) => Promise<void>
Expand Down
3 changes: 3 additions & 0 deletions src/S.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const record = S.dict;
export const jsonString = S.jsonString;
export const union = S.union;
export const object = S.js_object;
export const merge = S.js_merge;
export const Object = S.$$Object;
export const String = S.$$String;
export const Number = S.Float;
Expand All @@ -33,3 +34,5 @@ export const parseOrThrow = S.js_parseOrThrow;
export const parseAsync = S.js_parseAsync;
export const serialize = S.js_serialize;
export const serializeOrThrow = S.js_serializeOrThrow;
export const name = S.js_name;
export const setName = S.setName;
Loading

2 comments on commit eb3cc15

@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: eb3cc15 Previous: 80ac4f1 Ratio
Parse string 687922551 ops/sec (±2.07%) 706149725 ops/sec (±0.41%) 1.03
Serialize string 707464957 ops/sec (±0.12%) 693539015 ops/sec (±2.10%) 0.98
Advanced object struct factory 284697 ops/sec (±0.47%) 286556 ops/sec (±0.58%) 1.01
Parse advanced object 22530626 ops/sec (±0.17%) 22341504 ops/sec (±0.24%) 0.99
Create and parse advanced object 25199 ops/sec (±0.62%) 58042 ops/sec (±1.08%) 2.30
Parse advanced strict object 12008484 ops/sec (±0.62%) 13141235 ops/sec (±0.73%) 1.09
Serialize advanced object 687644746 ops/sec (±0.75%) 32617508 ops/sec (±0.33%) 0.0474336613341913

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: eb3cc15 Previous: 80ac4f1 Ratio
Create and parse advanced object 25199 ops/sec (±0.62%) 58042 ops/sec (±1.08%) 2.30

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

Please sign in to comment.