Skip to content

Commit

Permalink
Merge pull request #16 from vbudovski/feature/more-string-validators
Browse files Browse the repository at this point in the history
Feature/more string validators
  • Loading branch information
vbudovski authored Jan 3, 2025
2 parents d556bd9 + ad9799a commit 678d955
Show file tree
Hide file tree
Showing 13 changed files with 1,029 additions and 2 deletions.
118 changes: 118 additions & 0 deletions paseri-docs/src/content/docs/reference/Schema/Primitives/string.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,121 @@ A valid [Nano ID](https://github.com/ai/nanoid).
```typescript
p.string().nanoid();
```

### `includes`

Contains the `searchString`.

```typescript
p.string().includes('foo');
```

### `startsWith`

Starts with the `searchString`.

```typescript
p.string().startsWith('foo');
```

### `endsWith`

Ends with the `searchString`.

```typescript
p.string().endsWith('foo');
```

### `date`

A valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) date string `YYYY-MM-DD`.

```typescript
p.string().date();
```

### `time`

A valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) time string `hh:mm:ss[.s+]`.

```typescript
p.string().time();
```

You can require a fixed precision by setting the `precision` option.

```typescript
p.string().time({ precision: 3 });
// 01:02:03.123 ✅
// 01:02:03.1234 ❌
```

### `datetime`

A valid [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) UTC datetime string `YYYY-MM-DDThh:mm:ss[.s+]Z`.

```typescript
p.string().datetime();
```

You can require a fixed precision by setting the `precision` option.

```typescript
p.string().datetime({ precision: 3 });
// 2020-01-02T01:02:03.123Z ✅
// 2020-01-02T01:02:03.1234Z ❌
```

Non-UTC offsets are accepted by setting the `offset` option.

```typescript
p.string().datetime({ offset: true });
// 2020-01-02T01:02:03.123Z ✅
// 2020-01-02T01:02:03.123+02:30 ✅
// 2020-01-02T01:02:03.123-0430 ✅
// 2020-01-02T01:02:03.123 ❌
```

Offset-less (naïve) values are accepted by setting the `local` option.

```typescript
p.string().datetime({ local: true });
// 2020-01-02T01:02:03.123Z ✅
// 2020-01-02T01:02:03.123 ✅
```

### `ip`

A valid [IPv4](https://en.wikipedia.org/wiki/IPv4) or [IPv6](https://en.wikipedia.org/wiki/IPv6) address.

```typescript
p.string().ip();
// 127.0.0.1 ✅
// ::1 ✅
```

The protocol version can be restricted.

```typescript
p.string().ip({ version: 4 });
// 127.0.0.1 ✅
// ::1 ❌
```

### `cidr`

A valid IP address range in [CIDR](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) notation.

```typescript
p.string().cidr();
// 127.0.0.0/8 ✅
// ::1/128 ✅
```

The protocol version can be restricted.

```typescript
p.string().cidr({ version: 4 });
// 127.0.0.0/8 ✅
// ::1/128 ❌
```
26 changes: 26 additions & 0 deletions paseri-lib/bench/string/cidr.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.string().cidr();
const zodSchema = z.string().cidr();

const dataValid = '10.0.0.0/22';
const dataInvalid = '127.0.0.1';

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

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

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

bench('Zod', { group: 'CIDR invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
26 changes: 26 additions & 0 deletions paseri-lib/bench/string/date.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.string().date();
const zodSchema = z.string().date();

const dataValid = '2020-01-01';
const dataInvalid = '2024-01-32';

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

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

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

bench('Zod', { group: 'Date invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
26 changes: 26 additions & 0 deletions paseri-lib/bench/string/datetime.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.string().datetime();
const zodSchema = z.string().datetime();

const dataValid = '2020-01-01T01:02:03.45678Z';
const dataInvalid = '2024-01-32T00:00:00';

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

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

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

bench('Zod', { group: 'Datetime invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
36 changes: 36 additions & 0 deletions paseri-lib/bench/string/endsWith.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as v from '@badrap/valita';
import { z } from 'zod';
import * as p from '../../src/index.ts';

const { bench } = Deno;

const paseriSchema = p.string().endsWith('foo');
const zodSchema = z.string().endsWith('foo');
const valitaSchema = v.string().assert((value) => value.endsWith('foo'));

const dataValid = 'Hello, world!foo';
const dataInvalid = 'Hello, world!';

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

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

bench('Valita', { group: 'Ends with valid' }, () => {
valitaSchema.try(dataValid);
});

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

bench('Zod', { group: 'Ends with invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});

bench('Valita', { group: 'Ends with invalid' }, () => {
valitaSchema.try(dataInvalid);
});
36 changes: 36 additions & 0 deletions paseri-lib/bench/string/includes.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as v from '@badrap/valita';
import { z } from 'zod';
import * as p from '../../src/index.ts';

const { bench } = Deno;

const paseriSchema = p.string().includes('foo');
const zodSchema = z.string().includes('foo');
const valitaSchema = v.string().assert((value) => value.includes('foo'));

const dataValid = 'Hello,fooworld!';
const dataInvalid = 'Hello, world!';

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

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

bench('Valita', { group: 'Includes valid' }, () => {
valitaSchema.try(dataValid);
});

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

bench('Zod', { group: 'Includes invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});

bench('Valita', { group: 'Includes invalid' }, () => {
valitaSchema.try(dataInvalid);
});
26 changes: 26 additions & 0 deletions paseri-lib/bench/string/ip.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.string().ip();
const zodSchema = z.string().ip();

const dataValid = '192.168.1.254';
const dataInvalid = '999';

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

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

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

bench('Zod', { group: 'IP invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
36 changes: 36 additions & 0 deletions paseri-lib/bench/string/startsWith.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as v from '@badrap/valita';
import { z } from 'zod';
import * as p from '../../src/index.ts';

const { bench } = Deno;

const paseriSchema = p.string().startsWith('foo');
const zodSchema = z.string().startsWith('foo');
const valitaSchema = v.string().assert((value) => value.startsWith('foo'));

const dataValid = 'fooHello, world!';
const dataInvalid = 'Hello, world!';

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

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

bench('Valita', { group: 'Starts with valid' }, () => {
valitaSchema.try(dataValid);
});

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

bench('Zod', { group: 'Starts with invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});

bench('Valita', { group: 'Starts with invalid' }, () => {
valitaSchema.try(dataInvalid);
});
26 changes: 26 additions & 0 deletions paseri-lib/bench/string/time.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.string().time();
const zodSchema = z.string().time();

const dataValid = '00:00:00';
const dataInvalid = '99:99:99';

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

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

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

bench('Zod', { group: 'Time invalid' }, () => {
zodSchema.safeParse(dataInvalid);
});
8 changes: 8 additions & 0 deletions paseri-lib/src/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const issueCodes = {
INVALID_EMOJI: 'invalid_emoji' as Tagged<'invalid_emoji', 'IssueCode'>,
INVALID_UUID: 'invalid_uuid' as Tagged<'invalid_uuid', 'IssueCode'>,
INVALID_NANOID: 'invalid_nanoid' as Tagged<'invalid_nanoid', 'IssueCode'>,
DOES_NOT_INCLUDE: 'does_not_include' as Tagged<'does_not_include', 'IssueCode'>,
DOES_NOT_START_WITH: 'does_not_start_with' as Tagged<'does_not_start_with', 'IssueCode'>,
DOES_NOT_END_WITH: 'does_not_end_with' as Tagged<'does_not_end_with', 'IssueCode'>,
INVALID_DATE_STRING: 'invalid_date_string' as Tagged<'invalid_date_string', 'IssueCode'>,
INVALID_TIME_STRING: 'invalid_time_string' as Tagged<'invalid_time_string', 'IssueCode'>,
INVALID_DATE_TIME_STRING: 'invalid_date_time_string' as Tagged<'invalid_date_time_string', 'IssueCode'>,
INVALID_IP_ADDRESS: 'invalid_ip_address' as Tagged<'invalid_ip_address', 'IssueCode'>,
INVALID_IP_ADDRESS_RANGE: 'invalid_ip_address_range' as Tagged<'invalid_ip_address_range', 'IssueCode'>,
// BigInt/Number.
TOO_SMALL: 'too_small' as Tagged<'too_small', 'IssueCode'>,
TOO_LARGE: 'too_large' as Tagged<'too_large', 'IssueCode'>,
Expand Down
8 changes: 8 additions & 0 deletions paseri-lib/src/locales/en-GB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ const en_GB = {
invalid_emoji: 'Invalid emoji.',
invalid_uuid: 'Invalid UUID.',
invalid_nanoid: 'Invalid Nano ID.',
does_not_include: 'Does not include search string.',
does_not_start_with: 'Does not start with search string.',
does_not_end_with: 'Does not end with search string.',
invalid_date_string: 'Invalid date string.',
invalid_time_string: 'Invalid time string.',
invalid_date_time_string: 'Invalid datetime string.',
invalid_ip_address: 'Invalid IP address.',
invalid_ip_address_range: 'Invalid IP address range.',
too_small: 'Too small.',
too_large: 'Too large.',
invalid_integer: 'Number must be an integer.',
Expand Down
Loading

0 comments on commit 678d955

Please sign in to comment.