Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/object methods #10

Merged
merged 3 commits into from
Dec 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,73 @@ if (result.ok) {
// `other` is preserved in `result.value`.
}
```

## Methods

### `merge`

Combine the keys of this schema, and the keys of another schema into a new schema containing both sets of keys. The keys
of the second schema will replace the keys of the first schema if there is any overlap. The combined schema will also
inherit the behaviour of the second schema for any unknown keys.

```typescript
import * as p from '@vbudovski/paseri';

const schema1 = p.object({
foo: p.string(),
bar: p.number(),
}).strict();
const schema2 = p.object({
bar: p.string(),
baz: p.number(),
}).passthrough();
const combinedSchema = schema1.merge(schema2);
/*
The resulting schema will be:
p.object({
foo: p.string(),
bar: p.string(),
baz: p.number(),
}).passthrough();
*/
```

### `pick`

Analogous to TypeScript's built-in `Pick` utility type. Creates a schema that contains only the selected keys.

```typescript
import * as p from '@vbudovski/paseri';

const schema = p.object({
foo: p.string(),
bar: p.number(),
});
const schemaPicked = schema.pick('foo');
/*
The resulting schema will be:
p.object({
foo: p.string(),
});
*/
```

### `omit`

Analogous to TypeScript's built-in `Omit` utility type. Creates a schema that contains all except the selected keys.

```typescript
import * as p from '@vbudovski/paseri';

const schema = p.object({
foo: p.string(),
bar: p.number(),
});
const schemaOmitted = schema.omit('foo');
/*
The resulting schema will be:
p.object({
bar: p.number(),
});
*/
```
94 changes: 94 additions & 0 deletions paseri-lib/src/schemas/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,97 @@ test('Immutable', async (t) => {
expect(modified).not.toBe(original);
});
});

test('Merge (without overlap)', () => {
const schema = p.object({ foo: p.number() });
const schemaOther = p.object({ bar: p.string() });
const schemaMerged = schema.merge(schemaOther);

fc.assert(
fc.property(fc.record({ foo: fc.float(), bar: fc.string() }), (data) => {
const result = schemaMerged.safeParse(data);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<{ foo: number; bar: string }>;
expect(result.value).toEqual(data);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Merge (with overlap)', () => {
const schema = p.object({ foo: p.number() });
const schemaOther = p.object({ foo: p.string() });
const schemaMerged = schema.merge(schemaOther);

fc.assert(
fc.property(fc.record({ foo: fc.string() }), (data) => {
const result = schemaMerged.safeParse(data);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<{ foo: string }>;
expect(result.value).toEqual(data);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Merge (mode)', async (t) => {
await t.step('strip', () => {
const schema = p.object({ foo: p.string() }).strict();
const schemaOther = p.object({ foo: p.number() }).strip();
const schemaMerged = schema.merge(schemaOther);

const result = schemaMerged.parse({ foo: 123, bar: 'hello' });
expect(result).toEqual({ foo: 123 });
});

await t.step('strict', () => {
const schema = p.object({ foo: p.string() }).strip();
const schemaOther = p.object({ foo: p.number() }).strict();
const schemaMerged = schema.merge(schemaOther);

expect(() => {
schemaMerged.parse({ foo: 123, bar: 'hello' });
}).toThrow('Failed to parse. See `e.messages()` for details.');
});

await t.step('passthrough', () => {
const schema = p.object({ foo: p.string() }).strict();
const schemaOther = p.object({ foo: p.number() }).passthrough();
const schemaMerged = schema.merge(schemaOther);

const result = schemaMerged.parse({ foo: 123, bar: 'hello' });
expect(result).toEqual({ foo: 123, bar: 'hello' });
});
});

test('Pick', () => {
const schema = p.object({ foo: p.string(), bar: p.number() });
const schemaPicked = schema.pick('foo');

const data = { foo: 'hello' };
const result = schemaPicked.safeParse(data);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<{ foo: string }>;
expect(result.value).toEqual(data);
} else {
expect(result.ok).toBeTruthy();
}
});

test('Omit', () => {
const schema = p.object({ foo: p.string(), bar: p.number() });
const schemaOmitted = schema.omit('foo');

const data = { bar: 123 };
const result = schemaOmitted.safeParse(data);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<{ bar: number }>;
expect(result.value).toEqual(data);
} else {
expect(result.ok).toBeTruthy();
}
});
33 changes: 32 additions & 1 deletion paseri-lib/src/schemas/object.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { NonEmptyObject } from 'type-fest';
import type { IsEqual, Merge, NonEmptyObject, TupleToUnion } from 'type-fest';
import type { Infer } from '../infer.ts';
import { type LeafNode, type TreeNode, issueCodes } from '../issue.ts';
import { addIssue } from '../issue.ts';
Expand Down Expand Up @@ -163,6 +163,37 @@ class ObjectSchema<ShapeType extends ValidShapeType<ShapeType>> extends Schema<I

return cloned;
}
merge<ShapeTypeOther extends ValidShapeType<ShapeTypeOther>>(
other: ObjectSchema<ShapeTypeOther>,
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Merge?
): ObjectSchema<Merge<ShapeType, ShapeTypeOther>> {
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Merge?
const merged = new ObjectSchema<Merge<ShapeType, ShapeTypeOther>>(
Object.fromEntries([...this._shape.entries(), ...other._shape.entries()]),
);
merged._mode = other._mode;

return merged;
}
pick<Keys extends [keyof ShapeType, ...(keyof ShapeType)[]]>(
...keys: Keys
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Pick?
): ObjectSchema<Pick<ShapeType, TupleToUnion<Keys>>> {
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Pick?
return new ObjectSchema<Pick<ShapeType, TupleToUnion<Keys>>>(
Object.fromEntries(this._shape.entries().filter(([key]) => keys.includes(key as keyof ShapeType))),
);
}
omit<Keys extends [keyof ShapeType, ...(keyof ShapeType)[]]>(
// Ensure at least one key remains in schema.
...keys: IsEqual<TupleToUnion<Keys>, keyof ShapeType> extends true ? never : Keys
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Omit?
): ObjectSchema<Omit<ShapeType, TupleToUnion<Keys>>> {
// @ts-expect-error FIXME: How do we get the shape validation to play nicely with Omit?
return new ObjectSchema<Omit<ShapeType, TupleToUnion<Keys>>>(
Object.fromEntries(this._shape.entries().filter(([key]) => !keys.includes(key as keyof ShapeType))),
);
}
}

function object<ShapeType extends ValidShapeType<ShapeType>>(
Expand Down
Loading