Skip to content

Commit

Permalink
feat: add two new types
Browse files Browse the repository at this point in the history
  • Loading branch information
ForbesLindesay committed Mar 16, 2022
1 parent e9d5a13 commit 795b1e4
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 24 deletions.
12 changes: 6 additions & 6 deletions .size-snapshot.json
@@ -1,13 +1,13 @@
{
"index.js": {
"bundled": 15675,
"minified": 10974,
"gzipped": 2910
"bundled": 16672,
"minified": 11618,
"gzipped": 3110
},
"index.mjs": {
"bundled": 14060,
"minified": 10079,
"gzipped": 2839,
"bundled": 14990,
"minified": 10732,
"gzipped": 3047,
"treeshaked": {
"rollup": {
"code": 17,
Expand Down
116 changes: 98 additions & 18 deletions README.md
Expand Up @@ -10,6 +10,7 @@ Validators and parsers for common types not covered by the base [funtypes](https
This package includes all these schemas:

- 🚀 Base64String
- 🚀 ChainCodecs
- 🚀 ConstrainLength
- 🚀 DateString
- 🚀 DateTime
Expand All @@ -24,6 +25,7 @@ This package includes all these schemas:
- 🚀 ParsedDateTimeString
- 🚀 ParsedFloatString
- 🚀 ParsedIntegerString
- 🚀 ParsedJsonString
- 🚀 ParsedUrlString
- 🚀 Url
- 🚀 UrlString
Expand Down Expand Up @@ -133,6 +135,30 @@ deepEqual(
);
```

### ChainCodecs

Chain multiple codecs together to combine parsers, for example you can parse a Base64 encoded JSON object:

```ts
import {deepEqual} from 'assert';

import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = s.ChainCodecs(
s.ParsedBase64String(),
s.ParsedJsonString(t.Object({value: s.Integer()})),
);

// ✅ Valid:
deepEqual(assertMySchema.parse('eyJ2YWx1ZSI6NDJ9'), {value: 42});

// ✅ Valid:
deepEqual(assertMySchema.serialize({value: 42}), 'eyJ2YWx1ZSI6NDJ9');
```

You can pass as many codecs as you like as parameters to `ChainCodecs`. They will be applied in order when parsing and in reverse order when serializing.

### ConstrainLength

Constrain the length of a base type that has a `length` property, such as a `String` or an `Array`.
Expand Down Expand Up @@ -183,7 +209,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
dateOfBirth: t.DateString({max: `2022-03-15`}),
dateOfBirth: s.DateString({max: `2022-03-15`}),
});
```

Expand Down Expand Up @@ -232,7 +258,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.ParsedDateString(),
timestamp: s.ParsedDateString(),
});

deepEqual(
Expand Down Expand Up @@ -261,7 +287,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.ParsedDateString(),
timestamp: s.ParsedDateString(),
});

// The next line throws an error because there is a time
Expand All @@ -280,7 +306,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.DateTime({min: new Date(`2022-03-15`)}),
timestamp: s.DateTime({min: new Date(`2022-03-15`)}),
});
```

Expand Down Expand Up @@ -325,7 +351,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.DateTimeString({min: new Date(`2022-03-15`), strict: true}),
timestamp: s.DateTimeString({min: new Date(`2022-03-15`), strict: true}),
});
```

Expand Down Expand Up @@ -390,7 +416,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.ParsedDateTimeString({
timestamp: s.ParsedDateTimeString({
min: new Date(`2022-03-15`),
strict: true,
}),
Expand Down Expand Up @@ -422,7 +448,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
timestamp: t.ParsedDateTimeString(),
timestamp: s.ParsedDateTimeString(),
});

// The next line throws an error because the
Expand All @@ -444,7 +470,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
weight: t.Float({min: 0, max: 1}),
weight: s.Float({min: 0, max: 1}),
});
```

Expand Down Expand Up @@ -484,7 +510,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
weight: t.FloatString({min: `0`, max: `1`}),
weight: s.FloatString({min: `0`, max: `1`}),
});
```

Expand Down Expand Up @@ -531,7 +557,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
weight: t.ParsedFloatString({min: 0, max: 1}),
weight: s.ParsedFloatString({min: 0, max: 1}),
});

deepEqual(
Expand Down Expand Up @@ -560,7 +586,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
level: t.ParsedIntegerString({min: 1, max: 6}),
level: s.ParsedIntegerString({min: 1, max: 6}),
});

// The next line throws an error because the value is outside the requested range:
Expand All @@ -581,7 +607,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
level: t.Integer({min: 1, max: 6}),
level: s.Integer({min: 1, max: 6}),
});
```

Expand Down Expand Up @@ -621,7 +647,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
id: t.Integer({
id: s.Integer({
min: `-9999999999999999999999999`,
max: `9999999999999999999999999`,
}),
Expand Down Expand Up @@ -671,7 +697,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
level: t.ParsedIntegerString({min: 1, max: 6}),
level: s.ParsedIntegerString({min: 1, max: 6}),
});

deepEqual(
Expand Down Expand Up @@ -700,9 +726,63 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
level: t.ParsedIntegerString({min: 1, max: 6}),
level: s.ParsedIntegerString({min: 1, max: 6}),
});

// The next line throws an error because the value is not an integer:
MySchema.serialize({
level: 3.14,
});
```

### ParsedJsonString

Transparently parse/serialize to/from JSON. A codec can optionally be provided to handle the parsed value.

✅ Valid:

```ts
import {deepEqual} from 'assert';

import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = s.ParsedJsonString(
t.Object({
level: s.ParsedIntegerString(),
}),
);

deepEqual(MySchema.parse(`{"level": "3"}`), {
level: 3,
});

deepEqual(
MySchema.serialize({
level: 3,
}),
`{"level":"3"}`,
);
```

🚨 Invalid:

```ts
import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = s.ParsedJsonString(
t.Object({
level: s.ParsedIntegerString(),
}),
);

// The next line throws an error because the string is not valid JSON:
MySchema.parse(`{level: '3'}`);

// The next line throws an error because the value is not an integer:
MySchema.parse(`{"level": "3.14"}`);

// The next line throws an error because the value is not an integer:
MySchema.serialize({
level: 3.14,
Expand All @@ -722,7 +802,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
href: t.Url({allowedProtocols: new Set([`http:`, `https:`])}),
href: s.Url({allowedProtocols: new Set([`http:`, `https:`])}),
});
```

Expand Down Expand Up @@ -763,7 +843,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
href: t.UrlString({allowedProtocols: new Set([`http:`, `https:`])}),
href: s.UrlString({allowedProtocols: new Set([`http:`, `https:`])}),
});
```

Expand Down Expand Up @@ -806,7 +886,7 @@ import * as t from 'funtypes';
import * as s from 'funtypes-schemas';

const MySchema = t.Object({
href: t.ParsedUrlString(),
href: s.ParsedUrlString(),
});

deepEqual(
Expand Down
66 changes: 66 additions & 0 deletions src/__tests__/index.test.ts
Expand Up @@ -28,6 +28,27 @@ test(`Base64String`, () => {
);
});

test(`ChainCodecs`, () => {
const example = s.ChainCodecs(
s.ParsedBase64String(),
s.ParsedIntegerString(),
);
expectOk(example.safeParse(`NDI=`)).toBe(42);
expectOk(example.safeSerialize(42)).toBe(`NDI=`);

expectFail(
example.safeParse(Buffer.from(`1.5`).toString(`base64`)),
).toMatchInlineSnapshot(
`"Expected an integer string between \\"-9007199254740991\\" and \\"9007199254740991\\" but got \\"1.5\\""`,
);
expectFail(example.safeSerialize(1.5)).toMatchInlineSnapshot(
`"Expected an integer between -9007199254740991 and 9007199254740991 but got 1.5"`,
);
expectFail(example.safeParse('hello world')).toMatchInlineSnapshot(
`"Expected a base64 encoded string but got \\"hello world\\""`,
);
});

test(`ConstrainLength`, () => {
expect(() =>
s.ConstrainLength(t.String, {min: -1, max: 3}),
Expand Down Expand Up @@ -912,6 +933,51 @@ test(`ParsedIntegerString`, () => {
);
});

test(`ParsedJsonString`, () => {
const example = s.ParsedJsonString(
t.Object({value: s.ParsedIntegerString()}),
);
expectOk(example.safeParse(JSON.stringify({value: '10'})))
.toMatchInlineSnapshot(`
Object {
"value": 10,
}
`);
expectOk(example.safeSerialize({value: 10})).toMatchInlineSnapshot(
`"{\\"value\\":\\"10\\"}"`,
);

expectFail(
example.safeParse(JSON.stringify({value: '1.5'})),
).toMatchInlineSnapshot(
`"Expected an integer string between \\"-9007199254740991\\" and \\"9007199254740991\\" but got \\"1.5\\""`,
);
expectFail(example.safeSerialize({value: 1.5})).toMatchInlineSnapshot(
`"Expected an integer between -9007199254740991 and 9007199254740991 but got 1.5"`,
);
expectFail(example.safeParse('hello world')).toMatchInlineSnapshot(
`"Invalid JSON: Unexpected token h in JSON at position 0"`,
);
});

test(`ParsedJsonString -> Unknown`, () => {
const example = s.ParsedJsonString();

expectOk(example.safeParse(JSON.stringify({value: '10'})))
.toMatchInlineSnapshot(`
Object {
"value": "10",
}
`);
expectOk(example.safeSerialize({value: '10'})).toMatchInlineSnapshot(
`"{\\"value\\":\\"10\\"}"`,
);

expectFail(example.safeParse('hello world')).toMatchInlineSnapshot(
`"Invalid JSON: Unexpected token h in JSON at position 0"`,
);
});

test(`ParsedUrlString`, () => {
expect(() =>
s.ParsedUrlString({allowedProtocols: new Set(['http', 'https'])}),
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Expand Up @@ -9,6 +9,7 @@ export type {IntegerStringOptions} from './types/IntegerString';
export type {UrlOptions} from './types/Url';

export {default as Base64String} from './types/Base64String';
export {default as ChainCodecs} from './types/ChainCodecs';
export {default as ConstrainLength} from './types/ConstrainLength';
export {default as DateString} from './types/DateString';
export {default as DateTime} from './types/DateTime';
Expand All @@ -26,4 +27,5 @@ export {default as ParsedDateString} from './types/ParsedDateString';
export {default as ParsedDateTimeString} from './types/ParsedDateTimeString';
export {default as ParsedFloatString} from './types/ParsedFloatString';
export {default as ParsedIntegerString} from './types/ParsedIntegerString';
export {default as ParsedJsonString} from './types/ParsedJsonString';
export {default as ParsedUrlString} from './types/ParsedUrlString';

0 comments on commit 795b1e4

Please sign in to comment.