Skip to content

Commit

Permalink
Merge pull request #4 from vbudovski/feature/set-validator
Browse files Browse the repository at this point in the history
Feature/set validator
  • Loading branch information
vbudovski authored Jul 20, 2024
2 parents 88f2ee8 + f040e7d commit d50c996
Show file tree
Hide file tree
Showing 8 changed files with 426 additions and 5 deletions.
43 changes: 43 additions & 0 deletions paseri-docs/src/content/docs/reference/Collections/set.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: "Set"
sidebar:
order: 25
---

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

const schema = p.set(p.number());
const data = new Set([1, 2, 3]);

const result = schema.safeParse(data);
if (result.ok) {
// result.value typed as `Set<number>`.
}
```

## Validators

### `min`

Consists of at least `size` elements.

```typescript
p.set(p.number()).min(3);
```

### `max`

Consists of at most `size` elements.

```typescript
p.set(p.number()).max(3);
```

### `length`

Consists of exactly `size` elements.

```typescript
p.set(p.number()).size(3);
```
26 changes: 26 additions & 0 deletions paseri-lib/bench/set/type.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { z } from 'zod';
import * as p from '../../src/index.ts';

const { bench } = Deno;

const paseriSchema = p.set(p.number());
const zodSchema = z.set(z.number());

const dataValid = new Set<number>([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
const dataInvalid = null;

bench('Paseri', { group: 'Type valid' }, () => {
paseriSchema.safeParse(dataValid);
});

bench('Zod', { group: 'Type valid' }, () => {
zodSchema.safeParse(dataValid);
});

bench('Paseri', { group: 'Type invalid' }, () => {
paseriSchema.safeParse(dataInvalid);
});

bench('Zod', { group: 'Type invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
1 change: 1 addition & 0 deletions paseri-lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export {
number,
object,
record,
set,
string,
symbol,
tuple,
Expand Down
8 changes: 5 additions & 3 deletions paseri-lib/src/infer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ type Infer<SchemaType> = Simplify<
? InferMapped<SchemaType>
: SchemaType extends Readonly<Record<string | number | symbol, AnySchemaType>>
? InferMapped<SchemaType>
: SchemaType extends Schema<infer OutputType>
? OutputType
: never
: SchemaType extends Set<Schema<infer OutputType>>
? Set<OutputType>
: SchemaType extends Schema<infer OutputType>
? OutputType
: never
>;

export type { Infer };
4 changes: 2 additions & 2 deletions paseri-lib/src/schemas/array.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ test('Valid type', () => {
});

test('Invalid type', () => {
const schema = p.string();
const schema = p.array(p.number());

fc.assert(
fc.property(
fc.anything().filter((value) => Array.isArray(value)),
fc.anything().filter((value) => !Array.isArray(value)),
(data) => {
const result = schema.safeParse(data);
if (!result.ok) {
Expand Down
1 change: 1 addition & 0 deletions paseri-lib/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { null_ } from './null.ts';
export { number } from './number.ts';
export { object } from './object.ts';
export { record } from './record.ts';
export { set } from './set.ts';
export { string } from './string.ts';
export { symbol } from './symbol.ts';
export { tuple } from './tuple.ts';
Expand Down
256 changes: 256 additions & 0 deletions paseri-lib/src/schemas/set.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { expect } from '@std/expect';
import { expectTypeOf } from 'expect-type';
import fc from 'fast-check';
import * as p from '../index.ts';
import type { TreeNode } from '../issue.ts';

const { test } = Deno;

test('Valid type', () => {
const schema = p.set(p.number());

fc.assert(
fc.property(fc.array(fc.float()), (data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number>>;
expect(result.value).toEqual(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Invalid type', () => {
const schema = p.set(p.number());

fc.assert(
fc.property(fc.anything(), (data) => {
const result = schema.safeParse(data);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'invalid_type' });
} else {
expect(result.ok).toBeFalsy();
}
}),
);
});

test('Valid min', () => {
const schema = p.set(p.number()).min(3);

fc.assert(
fc.property(
fc.array(fc.float(), { minLength: 3 }).filter((value) => new Set(value).size >= 3),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number>>;
expect(result.value).toBe(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
},
),
);
});

test('Invalid min', () => {
const schema = p.set(p.number()).min(3);

fc.assert(
fc.property(
fc.array(fc.float(), { maxLength: 2 }).filter((value) => new Set(value).size <= 2),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' });
} else {
expect(result.ok).toBeFalsy();
}
},
),
);
});

test('Valid max', () => {
const schema = p.set(p.number()).max(3);

fc.assert(
fc.property(
fc.array(fc.float(), { maxLength: 3 }).filter((value) => new Set(value).size <= 3),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number>>;
expect(result.value).toBe(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
},
),
);
});

test('Invalid max', () => {
const schema = p.set(p.number()).max(3);

fc.assert(
fc.property(
fc.array(fc.float(), { minLength: 4 }).filter((value) => new Set(value).size >= 4),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' });
} else {
expect(result.ok).toBeFalsy();
}
},
),
);
});

test('Valid size', () => {
const schema = p.set(p.number()).size(3);

fc.assert(
fc.property(
fc.array(fc.float(), { minLength: 3, maxLength: 3 }).filter((value) => new Set(value).size === 3),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number>>;
expect(result.value).toBe(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
},
),
);
});

test('Invalid size (too long)', () => {
const schema = p.set(p.number()).size(3);

fc.assert(
fc.property(
fc.array(fc.float(), { minLength: 4 }).filter((value) => new Set(value).size >= 4),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'too_long' });
} else {
expect(result.ok).toBeFalsy();
}
},
),
);
});

test('Invalid size (too short)', () => {
const schema = p.set(p.number()).size(3);

fc.assert(
fc.property(
fc.array(fc.float(), { maxLength: 2 }).filter((value) => new Set(value).size <= 2),
(data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (!result.ok) {
expect(result.issue).toEqual({ type: 'leaf', code: 'too_short' });
} else {
expect(result.ok).toBeFalsy();
}
},
),
);
});

test('Invalid elements', () => {
const schema = p.set(p.number());
const data = new Set([1, 'foo', 2, 'bar']);

const result = schema.safeParse(data);
if (!result.ok) {
const expectedResult: TreeNode = {
type: 'join',
left: { type: 'nest', key: 1, child: { type: 'leaf', code: 'invalid_type' } },
right: { type: 'nest', key: 3, child: { type: 'leaf', code: 'invalid_type' } },
};
expect(result.issue).toEqual(expectedResult);
} else {
expect(result.ok).toBeFalsy();
}
});

test('Optional', () => {
const schema = p.set(p.number()).optional();

fc.assert(
fc.property(fc.option(fc.array(fc.float()), { nil: undefined }), (data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number> | undefined>;
expect(result.value).toEqual(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Nullable', () => {
const schema = p.set(p.number()).nullable();

fc.assert(
fc.property(fc.option(fc.array(fc.float()), { nil: null }), (data) => {
const dataAsSet = new Set(data);

const result = schema.safeParse(dataAsSet);
if (result.ok) {
expectTypeOf(result.value).toEqualTypeOf<Set<number> | null>;
expect(result.value).toEqual(dataAsSet);
} else {
expect(result.ok).toBeTruthy();
}
}),
);
});

test('Immutable', async (t) => {
await t.step('min', () => {
const original = p.set(p.string());
const modified = original.min(3);
expect(modified).not.toEqual(original);
});

await t.step('max', () => {
const original = p.set(p.string());
const modified = original.max(3);
expect(modified).not.toEqual(original);
});

await t.step('size', () => {
const original = p.set(p.string());
const modified = original.size(3);
expect(modified).not.toEqual(original);
});
});
Loading

0 comments on commit d50c996

Please sign in to comment.