(n: number, fn: (idx: number) => T): T[] {
2 | return Array.from({ length: n }, (_, idx) => fn(idx));
3 | }
4 |
5 | function random(min: number, max: number): number {
6 | return Math.floor(Math.random() * (max - min + 1)) + min;
7 | }
8 |
9 | export function generateRandomUser() {
10 | return {
11 | id: random(1, 100000),
12 | email: "wan2land@gmail.com",
13 | name: "wan2land",
14 | articles: repeatArray(20, (i) => ({
15 | id: i,
16 | title: `title ${i}`,
17 | content: `content ${i}`,
18 | comments: repeatArray(3, (i) => ({
19 | id: i,
20 | contents: `contents ${i}`,
21 | createdAt: {
22 | timestamp: 1671926400000,
23 | offset: 0,
24 | },
25 | })),
26 | updatedAt: {
27 | timestamp: 1671926400000,
28 | offset: 0,
29 | },
30 | createdAt: {
31 | timestamp: 1671926400000,
32 | offset: 0,
33 | },
34 | })),
35 | comments: repeatArray(3, (i) => ({
36 | id: i,
37 | contents: `contents ${i}`,
38 | createdAt: {
39 | timestamp: 1671926400000,
40 | offset: 0,
41 | },
42 | })),
43 | location: "Seoul",
44 | createdAt: {
45 | timestamp: 1671926400000,
46 | offset: 0,
47 | },
48 | };
49 | }
50 |
--------------------------------------------------------------------------------
/.benchmark/stubs/validate_ajv.ts:
--------------------------------------------------------------------------------
1 | import Ajv from "https://esm.sh/ajv@8";
2 |
3 | const DateType = {
4 | type: "object",
5 | properties: {
6 | timestamp: { type: "integer" },
7 | offset: { type: "integer" },
8 | },
9 | required: ["timestamp", "offset"],
10 | };
11 |
12 | const CommentType = {
13 | type: "object",
14 | properties: {
15 | id: {
16 | anyOf: [
17 | { type: "integer" },
18 | { type: "string" },
19 | ],
20 | },
21 | contents: { type: "string" },
22 | createdAt: DateType,
23 | },
24 | required: ["id", "contents", "createdAt"],
25 | };
26 |
27 | const ArticleType = {
28 | type: "object",
29 | properties: {
30 | id: { type: "integer" },
31 | title: { type: "string" },
32 | content: { type: "string" },
33 | comments: {
34 | type: "array",
35 | items: CommentType,
36 | },
37 | updatedAt: DateType,
38 | createdAt: DateType,
39 | },
40 | required: ["id", "title", "content", "comments", "updatedAt", "createdAt"],
41 | };
42 |
43 | const UserType = {
44 | type: "object",
45 | properties: {
46 | id: {
47 | anyOf: [
48 | { type: "integer" },
49 | { type: "string" },
50 | ],
51 | },
52 | email: { type: "string" },
53 | name: { type: "string" },
54 | articles: {
55 | type: "array",
56 | items: ArticleType,
57 | },
58 | comments: {
59 | type: "array",
60 | items: CommentType,
61 | },
62 | location: { type: "string" },
63 | createdAt: DateType,
64 | },
65 | required: [
66 | "id",
67 | "email",
68 | "name",
69 | "articles",
70 | "comments",
71 | "location",
72 | "createdAt",
73 | ],
74 | };
75 |
76 | const ajv1 = new Ajv();
77 | const validate = ajv1.compile(UserType);
78 | export function isUser(input: unknown) {
79 | return validate(input);
80 | }
81 |
82 | const ajv2 = new Ajv();
83 | export function generateAndIsUser(input: unknown) {
84 | ajv2.removeSchema(); // clear cache
85 | const validate = ajv2.compile(UserType);
86 | return validate(input);
87 | }
88 |
--------------------------------------------------------------------------------
/.benchmark/stubs/validate_safen.ts:
--------------------------------------------------------------------------------
1 | import { v } from "../../mod.ts";
2 |
3 | const DateType = {
4 | timestamp: Number,
5 | offset: Number,
6 | };
7 |
8 | const CommentType = {
9 | id: v.union([Number, String]),
10 | contents: String,
11 | createdAt: DateType,
12 | };
13 |
14 | const ArticleType = {
15 | id: v.union([Number, String]),
16 | title: String,
17 | content: String,
18 | comments: v.array(CommentType),
19 | updatedAt: DateType,
20 | createdAt: DateType,
21 | };
22 |
23 | const UserType = {
24 | id: v.union([Number, String]),
25 | email: v.decorate(String, (d) => d.email()),
26 | name: String,
27 | articles: v.array(ArticleType),
28 | comments: v.array(CommentType),
29 | location: String,
30 | createdAt: DateType,
31 | };
32 |
33 | const validate = v(UserType);
34 | export function isUser(user: unknown) {
35 | return validate(user);
36 | }
37 |
38 | export function generateAndIsUser(user: unknown) {
39 | return v(UserType)(user);
40 | }
41 |
--------------------------------------------------------------------------------
/.benchmark/stubs/validate_typebox.ts:
--------------------------------------------------------------------------------
1 | import { Type } from "npm:@sinclair/typebox";
2 | import { TypeCompiler } from "npm:@sinclair/typebox/compiler";
3 |
4 | const DateType = Type.Object({
5 | timestamp: Type.Integer(),
6 | offset: Type.Integer(),
7 | });
8 |
9 | const CommentType = Type.Object({
10 | id: Type.Union([Type.Integer(), Type.String()]),
11 | contents: Type.String(),
12 | createdAt: DateType,
13 | });
14 |
15 | const ArticleType = Type.Object({
16 | id: Type.Union([Type.Integer(), Type.String()]),
17 | title: Type.String(),
18 | content: Type.String(),
19 | comments: Type.Array(CommentType),
20 | updatedAt: DateType,
21 | createdAt: DateType,
22 | });
23 |
24 | const UserType = Type.Object({
25 | id: Type.Union([Type.Integer(), Type.String()]),
26 | email: Type.String(), // email?
27 | name: Type.String(),
28 | articles: Type.Array(ArticleType),
29 | comments: Type.Array(CommentType),
30 | location: Type.String(),
31 | createdAt: DateType,
32 | });
33 |
34 | const compiled = TypeCompiler.Compile(UserType);
35 | export function isUser(input: unknown) {
36 | return compiled.Check(input);
37 | }
38 |
39 | export function generateAndIsUser(input: unknown) {
40 | const compiled = TypeCompiler.Compile(UserType);
41 | return compiled.Check(input);
42 | }
43 |
--------------------------------------------------------------------------------
/.benchmark/stubs/validate_zod.ts:
--------------------------------------------------------------------------------
1 | import { z } from "https://deno.land/x/zod@v3.16.1/mod.ts";
2 |
3 | const DateType = z.object({
4 | timestamp: z.number().int(),
5 | offset: z.number().int(),
6 | });
7 |
8 | const CommentType = z.object({
9 | id: z.number().int().or(z.string()),
10 | contents: z.string(),
11 | createdAt: DateType,
12 | });
13 |
14 | const ArticleType = z.object({
15 | id: z.number().int().or(z.string()),
16 | title: z.string(),
17 | content: z.string(),
18 | comments: z.array(CommentType),
19 | updatedAt: DateType,
20 | createdAt: DateType,
21 | });
22 |
23 | const UserType = z.object({
24 | id: z.number().int().or(z.string()),
25 | email: z.string(),
26 | name: z.string(),
27 | articles: z.array(ArticleType),
28 | comments: z.array(CommentType),
29 | location: z.string(),
30 | createdAt: DateType,
31 | });
32 |
33 | export function isUser(input: unknown) {
34 | return UserType.safeParse(input).success;
35 | }
36 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | matrix:
14 | deno-version: [v1.x]
15 | steps:
16 | - name: Git Checkout Deno Module
17 | uses: actions/checkout@v2
18 | - name: Use Deno Version ${{ matrix.deno-version }}
19 | uses: denoland/setup-deno@v1
20 | with:
21 | deno-version: ${{ matrix.deno-version }}
22 | - name: Format
23 | run: deno fmt --check
24 | - name: Lint
25 | run: deno lint
26 | - name: Unit Test
27 | run: deno test --coverage=coverage
28 | - name: Create coverage report
29 | run: deno coverage ./coverage --lcov > coverage.lcov
30 | - name: Collect coverage
31 | uses: codecov/codecov-action@v1.0.10
32 | with:
33 | file: ./coverage.lcov
34 | - name: Build Module
35 | run: deno task build:npm
36 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .npm
2 |
3 | deno.lock
4 |
5 | examples/**/package-lock.json
6 | examples/**/node_modules
7 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "denoland.vscode-deno"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true,
3 | "deno.lint": true,
4 | "editor.formatOnSave": true,
5 | "editor.defaultFormatter": "denoland.vscode-deno",
6 | "cSpell.words": [
7 | "alphanum",
8 | "creditcard",
9 | "hexcolor",
10 | "macaddress",
11 | "safen",
12 | "typebox"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Safen
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Safen is a high-performance validation and sanitization library with easy type
15 | inference. Its syntax is similar to TypeScript interface, making it easy to
16 | create validation rules.
17 |
18 | https://user-images.githubusercontent.com/4086535/203831205-8b3481cb-bb8d-4f3c-9876-e41adb6855fd.mp4
19 |
20 | ## Installation
21 |
22 | **Node**
23 |
24 | ```bash
25 | npm install safen
26 | ```
27 |
28 | **Deno**
29 |
30 | ```ts
31 | import {
32 | s, // create sanitize,
33 | v, // create validate,
34 | } from "https://deno.land/x/safen/mod.ts";
35 | ```
36 |
37 | ## Basic Usage
38 |
39 | **Create Validate Fn**
40 |
41 | ```ts
42 | import { v } from "https://deno.land/x/safen/mod.ts";
43 |
44 | const validate = v(String); // now, validate: (data: unknown) => data is string
45 |
46 | const input = {} as unknown;
47 | if (validate(input)) {
48 | // now input is string!
49 | }
50 | ```
51 |
52 | **Create Sanitize Fn**
53 |
54 | ```ts
55 | import { s } from "https://deno.land/x/safen/mod.ts";
56 |
57 | const sanitize = s(String); // now, sanitize: (data: unknown) => string
58 |
59 | const input = {} as unknown; // some unknown value
60 |
61 | sanitize("something" as unknown); // return "something"
62 | sanitize(null as unknown); // throw InvalidValueError
63 | ```
64 |
65 | ## Types
66 |
67 | ```ts
68 | // Primitive Types
69 | const validate = v(String); // (data: unknown) => data is string
70 | const validate = v(Number); // (data: unknown) => data is number
71 | const validate = v(Boolean); // (data: unknown) => data is boolean
72 | const validate = v(BigInt); // (data: unknown) => data is bigint
73 | const validate = v(Symbol); // (data: unknown) => data is symbol
74 |
75 | // Literal Types
76 | const validate = v("foo"); // (data: unknown) => data is "foo"
77 | const validate = v(1024); // (data: unknown) => data is 1024
78 | const validate = v(true); // (data: unknown) => data is true
79 | const validate = v(2048n); // (data: unknown) => data is 2048n
80 | const validate = v(null); // (data: unknown) => data is null
81 | const validate = v(undefined); // (data: unknown) => data is undefined
82 |
83 | // Special
84 | const validate = v(v.any()); // (data: unknown) => data is any
85 | const validate = v(Array); // (data: unknown) => data is any[]
86 |
87 | // Object
88 | const Point = { x: Number, y: Number };
89 | const validate = v({ p1: Point, p2: Point }); // (data: unknown) => data is { p1: { x: number, y: number }, p2: { x: number, y: number } }
90 |
91 | // Union
92 | const validate = v(v.union([String, Number])); // (data: unknown) => data is string | number
93 |
94 | // Array
95 | const validate = v([String]); // (data: unknown) => data is string[]
96 | const validate = v([v.union([String, Number])]); // (data: unknown) => data is (string | number)[]
97 | ```
98 |
99 | ## Decorator
100 |
101 | Decorators do not affect type inference, but do affect additional validation and
102 | data transformation.
103 |
104 | **Step1. Basic Sanitize**
105 |
106 | ```ts
107 | const sanitize = s(s.union([
108 | String,
109 | null,
110 | ]));
111 |
112 | sanitize("hello world!"); // return "hello world!"
113 | sanitize(" hello world! "); // return " hello world! "
114 | sanitize(" "); // return " "
115 | sanitize(null); // return null
116 | ```
117 |
118 | **Step2. Add trim decorator**
119 |
120 | ```ts
121 | const sanitize = s(s.union([
122 | s.decorate(String, (d) => d.trim()),
123 | null,
124 | ]));
125 |
126 | sanitize("hello world!"); // return "hello world!"
127 | sanitize(" hello world! "); // return "hello world!"
128 | sanitize(" "); // return ""
129 | sanitize(null); // return null
130 | ```
131 |
132 | **Step3. Add emptyToNull decorator**
133 |
134 | ```ts
135 | const sanitize = s(
136 | s.decorate(
137 | s.union([
138 | s.decorate(String, (d) => d.trim()),
139 | null,
140 | ]),
141 | (d) => d.emptyToNull(),
142 | ),
143 | );
144 |
145 | sanitize("hello world!"); // return "hello world!"
146 | sanitize(" hello world! "); // return "hello world!"
147 | sanitize(" "); // return null
148 | sanitize(null); // return null
149 | ```
150 |
151 | ### Defined Decorators
152 |
153 | | Decorator | Validate | Transform | Type | Description |
154 | | ------------------------- | -------- | --------- | ------------------ | ----------------------------------------------------------------------------------- |
155 | | `alpha` | ✅ | | `string` | contains only letters([a-zA-Z]). |
156 | | `alphanum` | ✅ | | `string` | contains only letters and numbers([a-zA-Z0-9]) |
157 | | `ascii` | ✅ | | `string` | contains only ascii characters. |
158 | | `base64` | ✅ | | `string` | Base64. |
159 | | `between(min, max)` | ✅ | | `string`, `number` | value is between `{min}` and `{max}`. (ex) `between("aaa","zzz")`, `between(1,100)` |
160 | | `ceil` | | ✅ | `number` | Math.ceil. (ref. `floor`, `round`) |
161 | | `creditcard` | ✅ | | `string` | valid Credit Card number. cf. `0000-0000-0000-0000` |
162 | | `dateformat` | ✅ | | `string` | valid Date string(RFC2822, ISO8601). cf. `2018-12-25`, `12/25/2018`, `Dec 25, 2018` |
163 | | `email` | ✅ | | `string` | valid E-mail string. |
164 | | `emptyToNull` | | ✅ | `string or null` | empty string(`""`) to null |
165 | | `floor` | | ✅ | `number` | Math.floor. (ref. `ceil`, `round`) |
166 | | `hexcolor` | ✅ | | `string` | valid Hex Color string. cf. `#ffffff` |
167 | | `ip(version = null)` | ✅ | | `string` | valid UUID.
version is one of `null`(both, default), `v4`, and `v6`. |
168 | | `json` | ✅ | | `string` | valid JSON. |
169 | | `length(size)` | ✅ | | `string`, `any[]` | length is `{size}`. |
170 | | `lengthBetween(min, max)` | ✅ | | `string`, `any[]` | length is between `{min}` and `{max}`. |
171 | | `lengthMax(max)` | ✅ | | `string`, `any[]` | length is less than `{max}`. |
172 | | `lengthMin(min)` | ✅ | | `string`, `any[]` | length is greater than `{min}`. |
173 | | `lowercase` | ✅ | | `string` | lowercase. |
174 | | `macaddress` | ✅ | | `string` | valid Mac Address. |
175 | | `max(max)` | ✅ | | `string`, `number` | value is less than `{min}`. |
176 | | `min(min)` | ✅ | | `string`, `number` | value is greater than `{max}`. |
177 | | `port` | ✅ | | `number` | valid PORT(0-65535). |
178 | | `re` | ✅ | | `string` | match RegExp. |
179 | | `round` | | ✅ | `number` | Math.round. (ref. `ceil`, `floor`) |
180 | | `stringify` | | ✅ | `string` | cast to string |
181 | | `toLower` | | ✅ | `string` | change to lower case. |
182 | | `toUpper` | | ✅ | `string` | change to upper case. |
183 | | `trim` | | ✅ | `string` | trim. |
184 | | `uppercase` | ✅ | | `string` | uppercase. |
185 | | `url` | ✅ | | `string` | valid URL. |
186 | | `uuid(version = null)` | ✅ | | `string` | valid UUID.
version is one of `null`(default), `v3`, `v4`, and `v5`. |
187 |
188 | ## Custom Decorator
189 |
190 | ```mermaid
191 | graph LR;
192 | A[input] -->|type = unknown| B{cast};
193 | B -->|type = T| C{validate};
194 | C -->|true| D{transform};
195 | C -->|false| E[error];
196 | D --> F[output];
197 | ```
198 |
199 | ```ts
200 | interface Decorator {
201 | name: string;
202 | cast?(v: unknown): T;
203 | validate?(v: T): boolean;
204 | transform?(v: T): T;
205 | }
206 | ```
207 |
208 | The `cast` function is invoked at the beginning of the data processing pipeline,
209 | before the `validate` and `transform` functions. The purpose of the `cast`
210 | function is to ensure that the data is in the right type before being processed
211 | further.
212 |
213 | This is an example of a cast-only function:
214 |
215 | ```ts
216 | const decorator: Decorator = {
217 | name: "json_string",
218 | cast: (v) => JSON.stringify(v),
219 | };
220 | ```
221 |
222 | Once the data has been casted, the `validate` function is called to verify the
223 | content and format of the data. This function ensures that the data is valid and
224 | meets the specified criteria before being processed further.
225 |
226 | The `transform` function, on the other hand, is invoked only after the
227 | validation function returns a `true` result. The `transform` function then
228 | processes the data according to the specified rules and criteria.
229 |
230 | Therefore, the `cast`, `validate`, and `transform` functions work together to
231 | ensure that the data is in the right format, is valid, and is properly
232 | processed.
233 |
234 | ## Benchmark
235 |
236 | Please see [benchmark results](.benchmark).
237 |
238 | ## Old Version Docs
239 |
240 | - [1.x](https://github.com/denostack/safen/tree/1.x)
241 | - [2.x](https://github.com/denostack/safen/tree/1.x)
242 |
--------------------------------------------------------------------------------
/ast/ast.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 |
3 | import { Decorator } from "../decorator/decorator.ts";
4 |
5 | export enum Kind {
6 | Primitive = 1,
7 | Literal = 2,
8 |
9 | Array = 11,
10 | Object = 12,
11 |
12 | Union = 21,
13 |
14 | Decorator = 31,
15 | }
16 |
17 | export enum PrimitiveType {
18 | Any = 0,
19 | Null = 1,
20 | Undefined = 2,
21 | String = 3,
22 | Number = 4,
23 | Boolean = 5,
24 | BigInt = 6,
25 | Symbol = 7,
26 | }
27 |
28 | export type Ast =
29 | | AstSugarPrimitive
30 | | AstSugarLiteral
31 | | AstSugarArray
32 | | AstSugarAnyArray
33 | | AstSugarObject
34 | | AstStrict;
35 |
36 | export type AstSugarPrimitive =
37 | | null
38 | | undefined
39 | | StringConstructor
40 | | NumberConstructor
41 | | BooleanConstructor
42 | | BigIntConstructor
43 | | SymbolConstructor;
44 |
45 | export type AstSugarLiteral = string | number | boolean | bigint;
46 | export type AstSugarArray = [Ast];
47 | export type AstSugarAnyArray = ArrayConstructor; // map to [array, [primitive, any]];
48 | export interface AstSugarObject {
49 | [key: string]: Ast;
50 | }
51 |
52 | // Strict
53 | export type AstStrict =
54 | | AstPrimitive
55 | | AstLiteral
56 | | AstArray
57 | | AstObject
58 | | AstUnion
59 | | AstDecorator;
60 |
61 | export type AstPrimitive = [kind: Kind.Primitive, type: PrimitiveType];
62 | export type AstLiteral = [
63 | kind: Kind.Literal,
64 | type: string | number | boolean | bigint,
65 | ];
66 | export type AstArray = [kind: Kind.Array, of: T];
67 | export type AstObject = [
68 | kind: Kind.Object,
69 | obj: { [key: string]: T },
70 | ];
71 | export type AstUnion = [kind: Kind.Union, types: T[]];
72 | export type AstDecorator = [
73 | kind: Kind.Decorator,
74 | of: T,
75 | decorators: Decorator[],
76 | ];
77 |
--------------------------------------------------------------------------------
/ast/desugar.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "assert/mod.ts";
2 | import { desugar } from "./desugar.ts";
3 | import { Kind, PrimitiveType } from "./ast.ts";
4 |
5 | Deno.test("ast/desugar, desugar primitive", () => {
6 | assertEquals(desugar(null), [Kind.Primitive, PrimitiveType.Null]);
7 | assertEquals(desugar(undefined), [Kind.Primitive, PrimitiveType.Undefined]);
8 | assertEquals(desugar(String), [Kind.Primitive, PrimitiveType.String]);
9 | assertEquals(desugar(Number), [Kind.Primitive, PrimitiveType.Number]);
10 | assertEquals(desugar(Boolean), [Kind.Primitive, PrimitiveType.Boolean]);
11 | assertEquals(desugar(BigInt), [Kind.Primitive, PrimitiveType.BigInt]);
12 | assertEquals(desugar(Symbol), [Kind.Primitive, PrimitiveType.Symbol]);
13 | });
14 |
15 | Deno.test("ast/desugar, desugar literal", () => {
16 | assertEquals(desugar("foo"), [Kind.Literal, "foo"]);
17 | assertEquals(desugar(30), [Kind.Literal, 30]);
18 | assertEquals(desugar(true), [Kind.Literal, true]);
19 | assertEquals(desugar(false), [Kind.Literal, false]);
20 | assertEquals(desugar(10n), [Kind.Literal, 10n]);
21 | });
22 |
23 | Deno.test("ast/desugar, desugar array", () => {
24 | assertEquals(
25 | desugar(Array),
26 | [Kind.Array, [Kind.Primitive, PrimitiveType.Any]],
27 | );
28 | assertEquals(
29 | desugar([String]),
30 | [Kind.Array, [Kind.Primitive, PrimitiveType.String]],
31 | );
32 | assertEquals(
33 | desugar([Kind.Array, String]),
34 | [Kind.Array, [Kind.Primitive, PrimitiveType.String]],
35 | );
36 | });
37 |
38 | Deno.test("ast/desugar, desugar object", () => {
39 | assertEquals(
40 | desugar({
41 | name: String,
42 | age: Number,
43 | }),
44 | [Kind.Object, {
45 | name: [Kind.Primitive, PrimitiveType.String],
46 | age: [Kind.Primitive, PrimitiveType.Number],
47 | }],
48 | );
49 | assertEquals(
50 | desugar([Kind.Object, {
51 | name: String,
52 | age: Number,
53 | }]),
54 | [Kind.Object, {
55 | name: [Kind.Primitive, PrimitiveType.String],
56 | age: [Kind.Primitive, PrimitiveType.Number],
57 | }],
58 | );
59 | });
60 |
--------------------------------------------------------------------------------
/ast/desugar.ts:
--------------------------------------------------------------------------------
1 | import { Ast, AstStrict, Kind, PrimitiveType } from "./ast.ts";
2 |
3 | export function desugar(ast: Ast): AstStrict {
4 | switch (ast) {
5 | case null:
6 | return [Kind.Primitive, PrimitiveType.Null];
7 | case undefined:
8 | return [Kind.Primitive, PrimitiveType.Undefined];
9 | case String:
10 | return [Kind.Primitive, PrimitiveType.String];
11 | case Number:
12 | return [Kind.Primitive, PrimitiveType.Number];
13 | case Boolean:
14 | return [Kind.Primitive, PrimitiveType.Boolean];
15 | case BigInt:
16 | return [Kind.Primitive, PrimitiveType.BigInt];
17 | case Symbol:
18 | return [Kind.Primitive, PrimitiveType.Symbol];
19 | case Array:
20 | return [Kind.Array, [Kind.Primitive, PrimitiveType.Any]];
21 | }
22 | switch (typeof ast) {
23 | case "string":
24 | return [Kind.Literal, ast];
25 | case "number":
26 | return [Kind.Literal, ast];
27 | case "boolean":
28 | return [Kind.Literal, ast];
29 | case "bigint":
30 | return [Kind.Literal, ast];
31 | }
32 | if (Array.isArray(ast)) {
33 | if (ast.length === 1) {
34 | return [Kind.Array, desugar(ast[0])];
35 | }
36 | switch (ast[0]) {
37 | case Kind.Array:
38 | return [Kind.Array, desugar(ast[1])];
39 | case Kind.Object:
40 | return desugar(ast[1]);
41 | case Kind.Union:
42 | return [Kind.Union, ast[1].map(desugar)];
43 | case Kind.Decorator:
44 | return [Kind.Decorator, desugar(ast[1]), ast[2]];
45 | }
46 | return ast;
47 | }
48 | if (typeof ast === "object") {
49 | const obj: Record = {};
50 | for (const key in ast) {
51 | obj[key] = desugar(ast[key]);
52 | }
53 | return [Kind.Object, obj];
54 | }
55 | throw new Error("..");
56 | }
57 |
--------------------------------------------------------------------------------
/ast/estimate_type.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "assert/mod.ts";
2 | import type { Equal, Expect } from "@type-challenges/utils";
3 | import { type EstimateType } from "./estimate_type.ts";
4 | import { Kind } from "./ast.ts";
5 |
6 | type TestPrimitiveTypes = [
7 | Expect, string>>,
8 | Expect, number>>,
9 | Expect, boolean>>,
10 | Expect, bigint>>,
11 | Expect, symbol>>,
12 | Expect, null>>,
13 | Expect, undefined>>,
14 | ];
15 |
16 | type TestScalarValueTypes = [
17 | Expect, "something">>,
18 | Expect, 30>>,
19 | Expect, true>>,
20 | Expect, false>>,
21 | Expect, 1n>>,
22 | ];
23 |
24 | type Point = { x: typeof Number; y: typeof Number };
25 | type TestArray = [
26 | Expect<
27 | Equal<
28 | EstimateType,
29 | // deno-lint-ignore no-explicit-any
30 | any[]
31 | >
32 | >,
33 | Expect<
34 | Equal<
35 | EstimateType<[Kind.Array, typeof String]>,
36 | string[]
37 | >
38 | >,
39 | ];
40 | type TestObject = [
41 | Expect, { foo: string }>>,
42 | Expect<
43 | Equal<
44 | EstimateType<{ start: Point; end: Point }>,
45 | { start: { x: number; y: number }; end: { x: number; y: number } }
46 | >
47 | >,
48 | ];
49 |
50 | type TestUnion = [
51 | Expect<
52 | Equal<
53 | EstimateType<[Kind.Union, (typeof String | typeof Number)[]]>,
54 | string | number
55 | >
56 | >,
57 | Expect<
58 | Equal<
59 | EstimateType<
60 | { hello: [Kind.Union, (typeof String | typeof Number | Point)[]] }
61 | >,
62 | { hello: string | number | { x: number; y: number } }
63 | >
64 | >,
65 | ];
66 |
67 | type TestDecorator = [
68 | // with scalar
69 | Expect<
70 | Equal, string>
71 | >,
72 | // with object
73 | Expect<
74 | Equal<
75 | EstimateType<[Kind.Decorator, { foo: typeof String }, []]>,
76 | { foo: string }
77 | >
78 | >,
79 | // with array
80 | Expect<
81 | Equal<
82 | EstimateType<[Kind.Decorator, [Kind.Array, typeof String], []]>,
83 | string[]
84 | >
85 | >,
86 | // with or
87 | Expect<
88 | Equal<
89 | EstimateType<
90 | [
91 | Kind.Decorator,
92 | { hello: [Kind.Union, (typeof String | typeof Number | Point)[]] },
93 | [],
94 | ]
95 | >,
96 | { hello: string | number | { x: number; y: number } }
97 | >
98 | >,
99 | ];
100 |
101 | Deno.test("ast/estimate_type", () => {
102 | assertEquals(true, true);
103 | });
104 |
--------------------------------------------------------------------------------
/ast/estimate_type.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 |
3 | import {
4 | Ast,
5 | AstArray,
6 | AstDecorator,
7 | AstLiteral,
8 | AstObject,
9 | AstPrimitive,
10 | AstSugarArray,
11 | AstSugarLiteral,
12 | AstSugarObject,
13 | AstUnion,
14 | PrimitiveType,
15 | } from "./ast.ts";
16 |
17 | export interface PrimitiveTypeMap {
18 | [PrimitiveType.Any]: any;
19 | [PrimitiveType.Null]: null;
20 | [PrimitiveType.Undefined]: undefined;
21 | [PrimitiveType.String]: string;
22 | [PrimitiveType.Number]: number;
23 | [PrimitiveType.Boolean]: boolean;
24 | [PrimitiveType.BigInt]: bigint;
25 | [PrimitiveType.Symbol]: symbol;
26 | }
27 |
28 | export type EstimateType = T extends StringConstructor ? string
29 | : T extends NumberConstructor ? number
30 | : T extends BooleanConstructor ? boolean
31 | : T extends BigIntConstructor ? bigint
32 | : T extends SymbolConstructor ? symbol
33 | : T extends ArrayConstructor ? any[] // anyarray
34 | : T extends infer U extends (null | undefined) ? U
35 | : T extends infer U extends AstSugarLiteral ? U
36 | : T extends AstSugarObject ? { [K in keyof T]: EstimateType }
37 | : T extends AstSugarArray ? EstimateType[]
38 | : T extends AstPrimitive ? PrimitiveTypeMap[T[1]]
39 | : T extends AstLiteral ? T[1]
40 | : T extends AstArray ? EstimateType[]
41 | : T extends AstObject ? { [K in keyof T[1]]: EstimateType }
42 | : T extends AstUnion ? EstimateType
43 | : T extends AstDecorator ? EstimateType
44 | : never;
45 |
--------------------------------------------------------------------------------
/ast/utils.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 | import {
3 | Ast,
4 | AstArray,
5 | AstDecorator,
6 | AstPrimitive,
7 | AstUnion,
8 | Kind,
9 | PrimitiveType,
10 | } from "./ast.ts";
11 | import { EstimateType } from "./estimate_type.ts";
12 |
13 | export function any(): AstPrimitive {
14 | return [Kind.Primitive, PrimitiveType.Any];
15 | }
16 |
17 | export function or(types: T[]): AstUnion {
18 | return [Kind.Union, types];
19 | }
20 |
21 | export function union(types: T[]): AstUnion {
22 | return [Kind.Union, types];
23 | }
24 |
25 | export function array(of: T): AstArray {
26 | return [Kind.Array, of];
27 | }
28 |
29 | export function decorate(
30 | of: T,
31 | decorator: Decorator>,
32 | ): AstDecorator;
33 | export function decorate(
34 | of: T,
35 | decorators: Decorator>[],
36 | ): AstDecorator;
37 | export function decorate(
38 | of: T,
39 | decorator: Decorator> | Decorator<
40 | EstimateType
41 | >[],
42 | ): AstDecorator {
43 | return [
44 | Kind.Decorator,
45 | of,
46 | Array.isArray(decorator) ? decorator : [decorator],
47 | ];
48 | }
49 |
50 | export function optional(
51 | of: T,
52 | ): AstUnion {
53 | return [Kind.Union, [undefined, of]];
54 | }
55 |
--------------------------------------------------------------------------------
/decorator/decorator.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals } from "assert/mod.ts";
2 |
3 | import { Decorator } from "./decorator.ts";
4 |
5 | type IsSubset = T extends F ? true : false;
6 |
7 | function is() {
8 | return true;
9 | }
10 |
11 | function isNot() {
12 | return true;
13 | }
14 |
15 | Deno.test("decorator/decorator, type check", () => {
16 | const v1 = { name: "custom" };
17 |
18 | assertEquals(is>>(), true);
19 |
20 | const v2 = () => {};
21 | assertEquals(isNot>>(), true);
22 |
23 | const v3 = "string";
24 | assertEquals(isNot>>(), true);
25 | });
26 |
--------------------------------------------------------------------------------
/decorator/decorator.ts:
--------------------------------------------------------------------------------
1 | export interface Decorator {
2 | name: string;
3 | cast?(v: unknown): T;
4 | validate?(v: T): boolean;
5 | transform?(v: T): T;
6 | apply?: never;
7 | call?: never;
8 | bind?: never;
9 | }
10 |
--------------------------------------------------------------------------------
/decorators.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertThrows } from "assert/mod.ts";
2 | import { d } from "./decorators.ts";
3 | import { decorate, union } from "./ast/utils.ts";
4 | import { createSanitize } from "./validator/create_sanitize.ts";
5 | import { InvalidValueError } from "./validator/invalid_value_error.ts";
6 |
7 | const {
8 | alpha,
9 | alphanum,
10 | ascii,
11 | base64,
12 | between,
13 | ceil,
14 | creditcard,
15 | dateformat,
16 | email,
17 | emptyToNull,
18 | floor,
19 | hexcolor,
20 | ip,
21 | json,
22 | length,
23 | lengthBetween,
24 | lengthMax,
25 | lengthMin,
26 | lowercase,
27 | macaddress,
28 | max,
29 | min,
30 | port,
31 | re,
32 | round,
33 | stringify,
34 | toLower,
35 | toUpper,
36 | trim,
37 | uppercase,
38 | url,
39 | uuid,
40 | } = d;
41 |
42 | Deno.test("decorators, alpha", () => {
43 | const s = createSanitize(decorate(String, alpha()));
44 |
45 | assertEquals(s("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz");
46 |
47 | const e = assertThrows(
48 | () => {
49 | s("1");
50 | },
51 | InvalidValueError,
52 | "This is an invalid value from decorator.",
53 | );
54 | assertEquals(e.reason, "#alpha");
55 | });
56 |
57 | Deno.test("decorators, alphanum", () => {
58 | const s = createSanitize(decorate(String, alphanum()));
59 |
60 | assertEquals(s("abcdefghijklmnopqrstuvwxyz1"), "abcdefghijklmnopqrstuvwxyz1");
61 | assertEquals(s("1"), "1");
62 |
63 | const e = assertThrows(
64 | () => {
65 | s("äbc1");
66 | },
67 | InvalidValueError,
68 | "This is an invalid value from decorator.",
69 | );
70 | assertEquals(e.reason, "#alphanum");
71 | });
72 |
73 | Deno.test("decorators, ascii", () => {
74 | const s = createSanitize(decorate(String, ascii()));
75 |
76 | assertEquals(s("abcdefghijklmnopqrstuvwxyz"), "abcdefghijklmnopqrstuvwxyz");
77 | assertEquals(s("0123456789"), "0123456789");
78 | assertEquals(s("!@#$%^&*()"), "!@#$%^&*()");
79 |
80 | const e = assertThrows(
81 | () => {
82 | s("äbc");
83 | },
84 | InvalidValueError,
85 | "This is an invalid value from decorator.",
86 | );
87 | assertEquals(e.reason, "#ascii");
88 | });
89 |
90 | Deno.test("decorators, base64", () => {
91 | const s = createSanitize(decorate(String, base64()));
92 |
93 | assertEquals(s("Zg=="), "Zg==");
94 |
95 | const e = assertThrows(
96 | () => {
97 | s("Zg=");
98 | },
99 | InvalidValueError,
100 | "This is an invalid value from decorator.",
101 | );
102 | assertEquals(e.reason, "#base64");
103 | });
104 |
105 | Deno.test("decorators, between", () => {
106 | const s1 = createSanitize(decorate(Number, between(2, 3)));
107 | assertEquals(s1(2), 2);
108 | assertEquals(s1(3), 3);
109 |
110 | {
111 | const e = assertThrows(
112 | () => {
113 | s1(1);
114 | },
115 | InvalidValueError,
116 | "This is an invalid value from decorator.",
117 | );
118 | assertEquals(e.reason, "#between");
119 | assertThrows(
120 | () => {
121 | s1(4);
122 | },
123 | InvalidValueError,
124 | "This is an invalid value from decorator.",
125 | );
126 | }
127 | });
128 |
129 | Deno.test("decorators, ceil", () => {
130 | const s1 = createSanitize(decorate(Number, ceil()));
131 |
132 | assertEquals(s1(2.1), 3);
133 | assertEquals(s1(2.9), 3);
134 |
135 | assertEquals(s1(-2.1), -2);
136 | assertEquals(s1(-2.9), -2);
137 | });
138 |
139 | Deno.test("decorators, creditcard", () => {
140 | const s = createSanitize(decorate(String, creditcard()));
141 |
142 | assertEquals(s("4716-2210-5188-5662"), "4716-2210-5188-5662");
143 | assertEquals(s("4929 7226 5379 7141"), "4929 7226 5379 7141");
144 |
145 | const e = assertThrows(
146 | () => {
147 | s("5398228707871528");
148 | },
149 | InvalidValueError,
150 | "This is an invalid value from decorator.",
151 | );
152 | assertEquals(e.reason, "#creditcard");
153 | });
154 |
155 | Deno.test("decorators, dateformat", () => {
156 | const s = createSanitize(decorate(String, dateformat()));
157 |
158 | assertEquals(s("2018-12-25"), "2018-12-25");
159 | assertEquals(s("12/25/2018"), "12/25/2018");
160 | assertEquals(s("Dec 25, 2018"), "Dec 25, 2018");
161 |
162 | const e = assertThrows(
163 | () => {
164 | s("1539043200000");
165 | },
166 | InvalidValueError,
167 | "This is an invalid value from decorator.",
168 | );
169 | assertEquals(e.reason, "#dateformat");
170 | });
171 |
172 | Deno.test("decorators, email", () => {
173 | const s = createSanitize(decorate(String, email()));
174 |
175 | assertEquals(s("wan2land+en@gmail.com"), "wan2land+en@gmail.com");
176 | const e = assertThrows(
177 | () => {
178 | s("unknown");
179 | },
180 | InvalidValueError,
181 | "This is an invalid value from decorator.",
182 | );
183 | assertEquals(e.reason, "#email");
184 | });
185 |
186 | Deno.test("decorators, emptyToNull", () => {
187 | const s = createSanitize(decorate(union([String, null]), emptyToNull()));
188 |
189 | assertEquals(s("empty"), "empty");
190 | assertEquals(s(""), null);
191 | assertEquals(s(null), null);
192 | });
193 |
194 | Deno.test("decorators, floor", () => {
195 | const s1 = createSanitize(decorate(Number, floor()));
196 |
197 | assertEquals(s1(2.1), 2);
198 | assertEquals(s1(2.9), 2);
199 |
200 | assertEquals(s1(-2.1), -3);
201 | assertEquals(s1(-2.9), -3);
202 | });
203 |
204 | Deno.test("decorators, hexcolor", () => {
205 | const s = createSanitize(decorate(String, hexcolor()));
206 |
207 | assertEquals(s("#CCCCCC"), "#CCCCCC");
208 | const e = assertThrows(
209 | () => {
210 | s("#ff");
211 | },
212 | InvalidValueError,
213 | "This is an invalid value from decorator.",
214 | );
215 | assertEquals(e.reason, "#hexcolor");
216 | });
217 |
218 | Deno.test("decorators, ip", () => {
219 | const s1 = createSanitize(decorate(String, ip()));
220 |
221 | assertEquals(s1("127.0.0.1"), "127.0.0.1");
222 | assertEquals(s1("2001:db8:0000:1:1:1:1:1"), "2001:db8:0000:1:1:1:1:1");
223 | {
224 | const e = assertThrows(
225 | () => {
226 | s1("256.0.0.0");
227 | },
228 | InvalidValueError,
229 | "This is an invalid value from decorator.",
230 | );
231 | assertEquals(e.reason, "#ip");
232 | }
233 |
234 | const s2 = createSanitize(decorate(String, ip("v4")));
235 |
236 | assertEquals(s2("127.0.0.1"), "127.0.0.1");
237 | {
238 | const e = assertThrows(
239 | () => {
240 | s2("256.0.0.0");
241 | },
242 | InvalidValueError,
243 | "This is an invalid value from decorator.",
244 | );
245 | assertEquals(e.reason, "#ip");
246 | assertThrows(
247 | () => {
248 | s2("2001:db8:0000:1:1:1:1:1");
249 | },
250 | InvalidValueError,
251 | "This is an invalid value from decorator.",
252 | );
253 | }
254 |
255 | const s3 = createSanitize(decorate(String, ip("v6")));
256 |
257 | assertEquals(s3("2001:db8:0000:1:1:1:1:1"), "2001:db8:0000:1:1:1:1:1");
258 | {
259 | const e = assertThrows(
260 | () => {
261 | s3("256.0.0.0");
262 | },
263 | InvalidValueError,
264 | "This is an invalid value from decorator.",
265 | );
266 | assertEquals(e.reason, "#ip");
267 | assertThrows(
268 | () => {
269 | s3("127.0.0.1");
270 | },
271 | InvalidValueError,
272 | "This is an invalid value from decorator.",
273 | );
274 | }
275 | });
276 |
277 | Deno.test("decorators, json", () => {
278 | const s = createSanitize(decorate(String, json()));
279 |
280 | assertEquals(s("{}"), "{}");
281 | const e = assertThrows(
282 | () => {
283 | s("a");
284 | },
285 | InvalidValueError,
286 | "This is an invalid value from decorator.",
287 | );
288 | assertEquals(e.reason, "#json");
289 | });
290 |
291 | Deno.test("decorators, lengthBetween", () => {
292 | const s1 = createSanitize(decorate(String, lengthBetween(2, 3)));
293 | assertEquals(s1("ab"), "ab");
294 | assertEquals(s1("abc"), "abc");
295 |
296 | {
297 | const e = assertThrows(
298 | () => {
299 | s1("a");
300 | },
301 | InvalidValueError,
302 | "This is an invalid value from decorator.",
303 | );
304 | assertEquals(e.reason, "#lengthBetween");
305 | assertThrows(
306 | () => {
307 | s1("abcd");
308 | },
309 | InvalidValueError,
310 | "This is an invalid value from decorator.",
311 | );
312 | }
313 |
314 | const s2 = createSanitize(decorate(Array, lengthBetween(2, 3)));
315 | assertEquals(s2([1, 2]), [1, 2]);
316 | assertEquals(s2([1, 2, 3]), [1, 2, 3]);
317 |
318 | {
319 | const e = assertThrows(
320 | () => {
321 | s2([1]);
322 | },
323 | InvalidValueError,
324 | "This is an invalid value from decorator.",
325 | );
326 | assertEquals(e.reason, "#lengthBetween");
327 | assertThrows(
328 | () => {
329 | s2([1, 2, 3, 4]);
330 | },
331 | InvalidValueError,
332 | "This is an invalid value from decorator.",
333 | );
334 | }
335 | });
336 |
337 | Deno.test("decorators, lengthMax", () => {
338 | const s = createSanitize(decorate(String, lengthMax(3)));
339 | assertEquals(s("a"), "a");
340 | assertEquals(s("ab"), "ab");
341 | assertEquals(s("abc"), "abc");
342 |
343 | const e = assertThrows(
344 | () => {
345 | s("abcd");
346 | },
347 | InvalidValueError,
348 | "This is an invalid value from decorator.",
349 | );
350 | assertEquals(e.reason, "#lengthMax");
351 | });
352 |
353 | Deno.test("decorators, lengthMin", () => {
354 | const s = createSanitize(decorate(String, lengthMin(2)));
355 | assertEquals(s("ab"), "ab");
356 | assertEquals(s("abc"), "abc");
357 |
358 | const e = assertThrows(
359 | () => {
360 | s("a");
361 | },
362 | InvalidValueError,
363 | "This is an invalid value from decorator.",
364 | );
365 | assertEquals(e.reason, "#lengthMin");
366 | });
367 |
368 | Deno.test("decorators, length", () => {
369 | const s = createSanitize(decorate(String, length(2)));
370 | assertEquals(s("ab"), "ab");
371 |
372 | const e = assertThrows(
373 | () => {
374 | s("a");
375 | },
376 | InvalidValueError,
377 | "This is an invalid value from decorator.",
378 | );
379 | assertEquals(e.reason, "#length");
380 | assertThrows(
381 | () => {
382 | s("abcd");
383 | },
384 | InvalidValueError,
385 | "This is an invalid value from decorator.",
386 | );
387 | });
388 |
389 | Deno.test("decorators, lowercase", () => {
390 | const s = createSanitize(decorate(String, lowercase()));
391 |
392 | assertEquals(s("abcd"), "abcd");
393 |
394 | const e = assertThrows(
395 | () => {
396 | s("ABCD");
397 | },
398 | InvalidValueError,
399 | "This is an invalid value from decorator.",
400 | );
401 | assertEquals(e.reason, "#lowercase");
402 | });
403 |
404 | Deno.test("decorators, macaddress", () => {
405 | const s = createSanitize(decorate(String, macaddress()));
406 |
407 | assertEquals(s("ab:ab:ab:ab:ab:ab"), "ab:ab:ab:ab:ab:ab");
408 |
409 | const e = assertThrows(
410 | () => {
411 | s("01:02:03:04:05");
412 | },
413 | InvalidValueError,
414 | "This is an invalid value from decorator.",
415 | );
416 | assertEquals(e.reason, "#macaddress");
417 | });
418 |
419 | Deno.test("decorators, max", () => {
420 | const s = createSanitize(decorate(Number, max(2)));
421 |
422 | assertEquals(s(2), 2);
423 |
424 | const e = assertThrows(
425 | () => {
426 | s(3);
427 | },
428 | InvalidValueError,
429 | "This is an invalid value from decorator.",
430 | );
431 | assertEquals(e.reason, "#max");
432 | });
433 |
434 | Deno.test("decorators, min", () => {
435 | const s = createSanitize(decorate(Number, min(2)));
436 |
437 | assertEquals(s(2), 2);
438 |
439 | const e = assertThrows(
440 | () => {
441 | s(1);
442 | },
443 | InvalidValueError,
444 | "This is an invalid value from decorator.",
445 | );
446 | assertEquals(e.reason, "#min");
447 | });
448 |
449 | Deno.test("decorators, port", () => {
450 | const s = createSanitize(decorate(Number, port()));
451 |
452 | assertEquals(s(0), 0);
453 | assertEquals(s(1), 1);
454 | assertEquals(s(65534), 65534);
455 | assertEquals(s(65535), 65535);
456 |
457 | const e = assertThrows(
458 | () => {
459 | s(65536);
460 | },
461 | InvalidValueError,
462 | "This is an invalid value from decorator.",
463 | );
464 | assertEquals(e.reason, "#port");
465 | });
466 |
467 | Deno.test("decorators, re", () => {
468 | const s = createSanitize(decorate(String, re(/^abc?$/i)));
469 |
470 | assertEquals(s("abc"), "abc");
471 |
472 | const e = assertThrows(
473 | () => {
474 | s("github");
475 | },
476 | InvalidValueError,
477 | "This is an invalid value from decorator.",
478 | );
479 | assertEquals(e.reason, "#re");
480 | });
481 |
482 | Deno.test("decorators, round", () => {
483 | const s1 = createSanitize(decorate(Number, round()));
484 |
485 | assertEquals(s1(2.1), 2);
486 | assertEquals(s1(2.9), 3);
487 |
488 | assertEquals(s1(-2.1), -2);
489 | assertEquals(s1(-2.9), -3);
490 | });
491 |
492 | Deno.test("decorators, stringify", () => {
493 | const s = createSanitize(decorate(String, stringify()));
494 |
495 | assertEquals(s("abcdef"), "abcdef");
496 | assertEquals(s(3030), "3030");
497 | assertEquals(s(123n), "123");
498 | assertEquals(s(true), "true");
499 | assertEquals(s(false), "false");
500 |
501 | // object, JSON.stringify
502 | assertEquals(s({ foo: "wow" }), '{"foo":"wow"}');
503 | assertEquals(s([{ foo: "wow" }]), '[{"foo":"wow"}]');
504 | });
505 |
506 | Deno.test("decorators, toLower", () => {
507 | const s = createSanitize(decorate(String, toLower()));
508 |
509 | assertEquals(s("aBcDeF"), "abcdef");
510 | });
511 |
512 | Deno.test("decorators, toUpper", () => {
513 | const s = createSanitize(decorate(String, toUpper()));
514 |
515 | assertEquals(s("aBcDeF"), "ABCDEF");
516 | });
517 |
518 | Deno.test("decorators, trim", () => {
519 | const s = createSanitize(decorate(String, trim()));
520 |
521 | assertEquals(s(" abcd\n\n\r\t"), "abcd");
522 | });
523 |
524 | Deno.test("decorators, uppercase", () => {
525 | const s = createSanitize(decorate(String, uppercase()));
526 |
527 | assertEquals(s("ABCD"), "ABCD");
528 |
529 | const e = assertThrows(
530 | () => {
531 | s("abcd");
532 | },
533 | InvalidValueError,
534 | "This is an invalid value from decorator.",
535 | );
536 | assertEquals(e.reason, "#uppercase");
537 | });
538 |
539 | Deno.test("decorators, url", () => {
540 | const s = createSanitize(decorate(String, url()));
541 |
542 | assertEquals(
543 | s("http://github.com/corgidisco"),
544 | "http://github.com/corgidisco",
545 | );
546 | assertEquals(s("https://github.com"), "https://github.com");
547 |
548 | const e = assertThrows(
549 | () => {
550 | s("github");
551 | },
552 | InvalidValueError,
553 | "This is an invalid value from decorator.",
554 | );
555 | assertEquals(e.reason, "#url");
556 | });
557 |
558 | Deno.test("decorators, uuid", () => {
559 | const s = createSanitize(decorate(String, uuid()));
560 |
561 | assertEquals(
562 | s("A987FBC9-4BED-3078-CF07-9141BA07C9F3"),
563 | "A987FBC9-4BED-3078-CF07-9141BA07C9F3",
564 | );
565 | const e = assertThrows(
566 | () => {
567 | s("xxxA987FBC9-4BED-3078-CF07-9141BA07C9F3");
568 | },
569 | InvalidValueError,
570 | "This is an invalid value from decorator.",
571 | );
572 | assertEquals(e.reason, "#uuid");
573 | });
574 |
--------------------------------------------------------------------------------
/decorators.ts:
--------------------------------------------------------------------------------
1 | import { alpha } from "./decorators/alpha.ts";
2 | import { alphanum } from "./decorators/alphanum.ts";
3 | import { ascii } from "./decorators/ascii.ts";
4 | import { base64 } from "./decorators/base64.ts";
5 | import { between } from "./decorators/between.ts";
6 | import { ceil } from "./decorators/ceil.ts";
7 | import { creditcard } from "./decorators/creditcard.ts";
8 | import { dateformat } from "./decorators/dateformat.ts";
9 | import { email } from "./decorators/email.ts";
10 | import { emptyToNull } from "./decorators/empty_to_null.ts";
11 | import { floor } from "./decorators/floor.ts";
12 | import { hexcolor } from "./decorators/hexcolor.ts";
13 | import { ip } from "./decorators/ip.ts";
14 | import { json } from "./decorators/json.ts";
15 | import { lengthBetween } from "./decorators/length_between.ts";
16 | import { lengthMax } from "./decorators/length_max.ts";
17 | import { lengthMin } from "./decorators/length_min.ts";
18 | import { length } from "./decorators/length.ts";
19 | import { lowercase } from "./decorators/lowercase.ts";
20 | import { macaddress } from "./decorators/macaddress.ts";
21 | import { min } from "./decorators/min.ts";
22 | import { max } from "./decorators/max.ts";
23 | import { port } from "./decorators/port.ts";
24 | import { re } from "./decorators/re.ts";
25 | import { round } from "./decorators/round.ts";
26 | import { stringify } from "./decorators/stringify.ts";
27 | import { toLower } from "./decorators/to_lower.ts";
28 | import { toUpper } from "./decorators/to_upper.ts";
29 | import { trim } from "./decorators/trim.ts";
30 | import { uppercase } from "./decorators/uppercase.ts";
31 | import { url } from "./decorators/url.ts";
32 | import { uuid } from "./decorators/uuid.ts";
33 |
34 | export const d = {
35 | alpha,
36 | alphanum,
37 | ascii,
38 | base64,
39 | between,
40 | ceil,
41 | creditcard,
42 | dateformat,
43 | email,
44 | emptyToNull,
45 | floor,
46 | hexcolor,
47 | ip,
48 | json,
49 | lengthBetween,
50 | lengthMax,
51 | lengthMin,
52 | length,
53 | lowercase,
54 | macaddress,
55 | min,
56 | max,
57 | port,
58 | re,
59 | round,
60 | stringify,
61 | toLower,
62 | toUpper,
63 | trim,
64 | uppercase,
65 | url,
66 | uuid,
67 | };
68 |
69 | export type PredefinedDecorators = typeof d;
70 |
--------------------------------------------------------------------------------
/decorators/alpha.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const re = /^[a-z]+$/i;
4 | const decorator: Decorator = {
5 | name: "alpha",
6 | validate(v) {
7 | return re.test(v);
8 | },
9 | };
10 |
11 | export function alpha(): Decorator {
12 | return decorator;
13 | }
14 |
--------------------------------------------------------------------------------
/decorators/alphanum.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const re = /^[a-z0-9]+$/i;
4 | const decorator: Decorator = {
5 | name: "alphanum",
6 | validate(v) {
7 | return re.test(v);
8 | },
9 | };
10 |
11 | export function alphanum(): Decorator {
12 | return decorator;
13 | }
14 |
--------------------------------------------------------------------------------
/decorators/ascii.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | // deno-lint-ignore no-control-regex
4 | const re = /^[\x00-\x7F]+$/;
5 | const decorator: Decorator = {
6 | name: "ascii",
7 | validate(v) {
8 | return re.test(v);
9 | },
10 | };
11 |
12 | export function ascii(): Decorator {
13 | return decorator;
14 | }
15 |
--------------------------------------------------------------------------------
/decorators/base64.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "base64",
5 | validate(v) {
6 | const l = v.length;
7 | if (!l || l % 4 !== 0 || /[^A-Z0-9+\\/=]/i.test(v)) return false;
8 | const index = v.indexOf("=");
9 | return index === -1 || index === l - 1 ||
10 | (index === l - 2 && v[l - 1] === "=");
11 | },
12 | };
13 |
14 | export function base64(): Decorator {
15 | return decorator;
16 | }
17 |
--------------------------------------------------------------------------------
/decorators/between.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function between(min: number, max: number): Decorator;
4 | export function between(min: string, max: string): Decorator;
5 | export function between(
6 | min: number | string,
7 | max: number | string,
8 | ): Decorator {
9 | return {
10 | name: "between",
11 | validate(v) {
12 | return v >= min && v <= max;
13 | },
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/decorators/ceil.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "ceil",
5 | transform(v) {
6 | return Math.ceil(v);
7 | },
8 | };
9 | export function ceil(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/creditcard.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const re =
4 | /^(?:4\d{12}(?:\d{3})?|5[1-5]\d{14}|(222[1-9]|22[3-9]\d|2[3-6]\d{2}|27[01]\d|2720)\d{12}|6(?:011|5\d\d)\d{12}|3[47]\d{13}|3(?:0[0-5]|[68]\d)\d{11}|(?:2131|1800|35\d{3})\d{11}|6[27]\d{14})$/;
5 |
6 | const decorator: Decorator = {
7 | name: "creditcard",
8 | validate(v) {
9 | v = v.replace(/\D+/g, "");
10 | if (!re.test(v)) return false;
11 | let sum = 0;
12 | let check = false;
13 | for (let i = v.length - 1; i >= 0; i--) {
14 | let tmp = parseInt(v.charAt(i), 10);
15 | if (check) {
16 | tmp *= 2;
17 | if (tmp >= 10) sum += (tmp % 10) + 1;
18 | else sum += tmp;
19 | } else {
20 | sum += tmp;
21 | }
22 | check = !check;
23 | }
24 | return !!((sum % 10) === 0 ? v : false);
25 | },
26 | };
27 |
28 | export function creditcard(): Decorator {
29 | return decorator;
30 | }
31 |
--------------------------------------------------------------------------------
/decorators/dateformat.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "dateformat",
5 | validate(v) {
6 | return !Number.isNaN(Date.parse(v));
7 | },
8 | };
9 |
10 | export function dateformat(): Decorator {
11 | return decorator;
12 | }
13 |
--------------------------------------------------------------------------------
/decorators/email.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | /**
4 | * RFC 5322
5 | * @ref https://emailregex.com/
6 | */
7 | const re =
8 | /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
9 | const decorator: Decorator = {
10 | name: "email",
11 | validate(v: string) {
12 | return re.test(v);
13 | },
14 | };
15 |
16 | export function email(): Decorator {
17 | return decorator;
18 | }
19 |
--------------------------------------------------------------------------------
/decorators/empty_to_null.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "emptyToNull",
5 | transform: (v) => v ? v : null,
6 | };
7 | export function emptyToNull(): Decorator {
8 | return decorator;
9 | }
10 |
--------------------------------------------------------------------------------
/decorators/floor.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "floor",
5 | transform(v) {
6 | return Math.floor(v);
7 | },
8 | };
9 | export function floor(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/hexcolor.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "hexcolor",
5 | validate(v) {
6 | return /^#?([0-9A-F]{3}|[0-9A-F]{6})$/i.test(v);
7 | },
8 | };
9 |
10 | export function hexcolor(): Decorator {
11 | return decorator;
12 | }
13 |
--------------------------------------------------------------------------------
/decorators/ip.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export const re4 =
4 | /^((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)$/;
5 | export const re6 =
6 | /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d)\.){3,3}(25[0-5]|(2[0-4]|1{0,1}\d){0,1}\d))$/;
7 |
8 | export function ip(version?: "v4" | "v6"): Decorator {
9 | if (version === "v4") {
10 | return {
11 | name: "ip",
12 | validate(v) {
13 | return re4.test(v);
14 | },
15 | };
16 | }
17 | if (version === "v6") {
18 | return {
19 | name: "ip",
20 | validate(v) {
21 | return re6.test(v);
22 | },
23 | };
24 | }
25 | return {
26 | name: "ip",
27 | validate(v) {
28 | return re4.test(v) || re6.test(v);
29 | },
30 | };
31 | }
32 |
--------------------------------------------------------------------------------
/decorators/json.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "json",
5 | validate(v) {
6 | try {
7 | JSON.parse(v);
8 | return true;
9 | } catch {
10 | return false;
11 | }
12 | },
13 | };
14 |
15 | export function json(): Decorator {
16 | return decorator;
17 | }
18 |
--------------------------------------------------------------------------------
/decorators/length.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function length(n: number): Decorator {
4 | return {
5 | name: "length",
6 | validate(v) {
7 | return v.length === n;
8 | },
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/decorators/length_between.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function lengthBetween(
4 | min: number,
5 | max: number,
6 | ): Decorator {
7 | return {
8 | name: "lengthBetween",
9 | validate(v) {
10 | return typeof v.length === "number" &&
11 | v.length >= min &&
12 | v.length <= max;
13 | },
14 | };
15 | }
16 |
--------------------------------------------------------------------------------
/decorators/length_max.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function lengthMax(max: number): Decorator {
4 | return {
5 | name: "lengthMax",
6 | validate(v) {
7 | return typeof v.length === "number" &&
8 | v.length <= max;
9 | },
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/length_min.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function lengthMin(min: number): Decorator {
4 | return {
5 | name: "lengthMin",
6 | validate(v) {
7 | return typeof v.length === "number" &&
8 | v.length >= min;
9 | },
10 | };
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/lowercase.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function lowercase(): Decorator {
4 | return {
5 | name: "lowercase",
6 | validate(v) {
7 | return v.toLowerCase() === v;
8 | },
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/decorators/macaddress.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const re = /^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$/;
4 | const decorator: Decorator = {
5 | name: "macaddress",
6 | validate(v) {
7 | return re.test(v);
8 | },
9 | };
10 |
11 | export function macaddress(): Decorator {
12 | return decorator;
13 | }
14 |
--------------------------------------------------------------------------------
/decorators/max.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function max(max: number): Decorator;
4 | export function max(max: string): Decorator;
5 | export function max(max: number | string): Decorator {
6 | return {
7 | name: "max",
8 | validate(v) {
9 | return v <= max;
10 | },
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/decorators/min.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function min(min: number): Decorator;
4 | export function min(min: string): Decorator;
5 | export function min(min: number | string): Decorator {
6 | return {
7 | name: "min",
8 | validate(v) {
9 | return v >= min;
10 | },
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/decorators/port.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "port",
5 | validate(v) {
6 | return v >= 0 && v <= 65535;
7 | },
8 | };
9 | export function port(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/re.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export function re(re: RegExp): Decorator {
4 | return {
5 | name: "re",
6 | validate(v) {
7 | return re.test(v);
8 | },
9 | };
10 | }
11 |
--------------------------------------------------------------------------------
/decorators/round.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "round",
5 | transform(v) {
6 | return Math.round(v);
7 | },
8 | };
9 | export function round(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/stringify.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "stringify",
5 | cast: (v) =>
6 | v == null ? "" : typeof v === "object" ? JSON.stringify(v) : `${v}`,
7 | };
8 |
9 | export function stringify(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/to_lower.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "toLower",
5 | transform(v) {
6 | return v.toLowerCase();
7 | },
8 | };
9 | export function toLower(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/to_upper.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "toUpper",
5 | transform(v) {
6 | return v.toUpperCase();
7 | },
8 | };
9 | export function toUpper(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/trim.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "trim",
5 | transform: (v) => v.trim(),
6 | };
7 | export function trim(): Decorator {
8 | return decorator;
9 | }
10 |
--------------------------------------------------------------------------------
/decorators/uppercase.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "uppercase",
5 | validate(v) {
6 | return v.toUpperCase() === v;
7 | },
8 | };
9 | export function uppercase(): Decorator {
10 | return decorator;
11 | }
12 |
--------------------------------------------------------------------------------
/decorators/url.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | const decorator: Decorator = {
4 | name: "url",
5 | validate(v) {
6 | try {
7 | new URL(v);
8 | return true;
9 | } catch {
10 | return false;
11 | }
12 | },
13 | };
14 | export function url(): Decorator {
15 | return decorator;
16 | }
17 |
--------------------------------------------------------------------------------
/decorators/uuid.ts:
--------------------------------------------------------------------------------
1 | import { Decorator } from "../decorator/decorator.ts";
2 |
3 | export const re =
4 | /^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
5 | export const re3 =
6 | /^[0-9A-F]{8}-[0-9A-F]{4}-3[0-9A-F]{3}-[0-9A-F]{4}-[0-9A-F]{12}$/i;
7 | export const re4 =
8 | /^[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
9 | export const re5 =
10 | /^[0-9A-F]{8}-[0-9A-F]{4}-5[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i;
11 |
12 | // ref. https://github.com/chriso/validator.js/blob/master/src/lib/isUUID.js
13 | export function uuid(version?: "v3" | "v4" | "v5"): Decorator {
14 | if (version === "v3") {
15 | return {
16 | name: "uuid(v3)",
17 | validate(v) {
18 | return re3.test(v);
19 | },
20 | };
21 | }
22 | if (version === "v4") {
23 | return {
24 | name: "uuid(v4)",
25 | validate(v) {
26 | return re4.test(v);
27 | },
28 | };
29 | }
30 | if (version === "v5") {
31 | return {
32 | name: "uuid(v5)",
33 | validate(v) {
34 | return re5.test(v);
35 | },
36 | };
37 | }
38 | return {
39 | name: "uuid",
40 | validate(v) {
41 | return re.test(v);
42 | },
43 | };
44 | }
45 |
--------------------------------------------------------------------------------
/deno.json:
--------------------------------------------------------------------------------
1 | {
2 | "imports": {
3 | "dnt/": "https://deno.land/x/dnt@0.40.0/",
4 | "assert/": "https://deno.land/std@0.220.1/assert/",
5 | "@type-challenges/utils": "npm:@type-challenges/utils@0.1.1/index.d.ts"
6 | },
7 | "tasks": {
8 | "test": "deno task test:unit && deno task test:lint && deno task test:format && deno task test:types",
9 | "test:unit": "deno test -A --unstable",
10 | "test:lint": "deno lint --ignore=.npm",
11 | "test:format": "deno fmt --check --ignore=.npm",
12 | "test:types": "find . -name '*.ts' -not -path './.npm/*' | xargs deno check",
13 | "build:npm": "deno run -A scripts/build_npm.ts"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export { any, array, decorate, optional, or, union } from "./ast/utils.ts";
2 | export type {
3 | Ast,
4 | AstArray,
5 | AstDecorator,
6 | AstLiteral,
7 | AstObject,
8 | AstPrimitive,
9 | AstStrict,
10 | AstSugarAnyArray,
11 | AstSugarArray,
12 | AstSugarLiteral,
13 | AstSugarObject,
14 | AstSugarPrimitive,
15 | AstUnion,
16 | Kind,
17 | PrimitiveType,
18 | } from "./ast/ast.ts";
19 | export type { EstimateType } from "./ast/estimate_type.ts";
20 | export type { Decorator } from "./decorator/decorator.ts";
21 |
22 | export { createSanitize } from "./validator/create_sanitize.ts";
23 | export { createValidate } from "./validator/create_validate.ts";
24 | export { InvalidValueError } from "./validator/invalid_value_error.ts";
25 |
26 | export { d, type PredefinedDecorators } from "./decorators.ts";
27 | export { type DecoratorFactory, s, v } from "./short.ts";
28 |
--------------------------------------------------------------------------------
/parser/syntax_error.ts:
--------------------------------------------------------------------------------
1 | function padStart(text: string, length: number): string {
2 | if (text.length > length) {
3 | return text;
4 | }
5 | length = length - text.length;
6 | return " ".repeat(length).slice(0, length) + text;
7 | }
8 |
9 | export class SyntaxError extends Error {
10 | public readonly code = "SYNTAX_ERROR";
11 |
12 | public constructor(
13 | source: string,
14 | expected: string,
15 | received: string,
16 | public position: number,
17 | public line: number,
18 | public column: number,
19 | ) {
20 | super(
21 | `Syntax Error: ${
22 | expected ? `expected ${expected}, ` : ""
23 | }unexpected token "${received}" (${line}:${column})
24 | ${line}: ${source.split("\n")[line - 1]}
25 | ${padStart("^", column + 2 + line.toString().length)}`,
26 | );
27 | this.name = "SyntaxError";
28 | Object.setPrototypeOf(this, SyntaxError.prototype);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/scripts/build_npm.ts:
--------------------------------------------------------------------------------
1 | import { build, emptyDir } from "dnt/mod.ts";
2 |
3 | const cmd = new Deno.Command(Deno.execPath(), {
4 | args: ["git", "describe", "--tags"],
5 | stdout: "piped",
6 | });
7 | const { stdout } = await cmd.output();
8 | const version = new TextDecoder().decode(stdout).trim();
9 |
10 | await emptyDir("./.npm");
11 |
12 | await build({
13 | entryPoints: ["./mod.ts"],
14 | outDir: "./.npm",
15 | shims: {
16 | deno: false,
17 | },
18 | test: false,
19 | compilerOptions: {
20 | lib: ["ES2021", "DOM"],
21 | },
22 | package: {
23 | name: "safen",
24 | version,
25 | description: "Super Fast Object Validator for Javascript(& Typescript).",
26 | keywords: [
27 | "validation",
28 | "validator",
29 | "validate",
30 | "sanitizer",
31 | "sanitize",
32 | "assert",
33 | "check",
34 | "type",
35 | "schema",
36 | "jsonschema",
37 | "joi",
38 | "ajv",
39 | "typescript",
40 | ],
41 | license: "MIT",
42 | repository: {
43 | type: "git",
44 | url: "git+https://github.com/denostack/safen.git",
45 | },
46 | bugs: {
47 | url: "https://github.com/denostack/safen/issues",
48 | },
49 | },
50 | });
51 |
52 | // post build steps
53 | Deno.copyFileSync("README.md", ".npm/README.md");
54 |
--------------------------------------------------------------------------------
/short.test.ts:
--------------------------------------------------------------------------------
1 | import { assert, assertFalse } from "assert/mod.ts";
2 | import { v } from "./short.ts";
3 |
4 | Deno.test("short, validation, decorate", () => {
5 | const validate = v({
6 | decorate: v.decorate(String, (d) => [d.email()]),
7 | });
8 |
9 | assert(validate({
10 | decorate: "wan2land@gmail.com",
11 | }));
12 | assertFalse(validate({
13 | decorate: "wan2land",
14 | }));
15 | });
16 |
17 | Deno.test("short, validation, union", () => {
18 | const validate = v({
19 | union: v.union([String, Number]),
20 | });
21 |
22 | assert(validate({
23 | union: "string",
24 | }));
25 | assert(validate({
26 | union: 30,
27 | }));
28 | assertFalse(validate({
29 | union: false,
30 | }));
31 | });
32 |
33 | Deno.test("short, validation, array", () => {
34 | const validate = v({
35 | array: v.array(String),
36 | });
37 |
38 | assert(validate({
39 | array: ["string"],
40 | }));
41 | assertFalse(validate({
42 | array: {},
43 | }));
44 | });
45 |
46 | Deno.test("short, validation, any", () => {
47 | const validate = v({
48 | any: v.any(),
49 | });
50 |
51 | assert(validate({
52 | any: "string",
53 | }));
54 | assert(validate({
55 | any: null,
56 | }));
57 | assert(validate({
58 | any: undefined,
59 | }));
60 | assert(validate({}));
61 | });
62 |
63 | Deno.test("short, validation, optional", () => {
64 | const validate = v({
65 | optional: v.optional(String),
66 | });
67 |
68 | assert(validate({
69 | optional: "string",
70 | }));
71 | assert(validate({
72 | optional: undefined,
73 | }));
74 | assert(validate({}));
75 |
76 | assertFalse(validate({
77 | optional: 30,
78 | }));
79 | });
80 |
--------------------------------------------------------------------------------
/short.ts:
--------------------------------------------------------------------------------
1 | import { Ast } from "./ast/ast.ts";
2 | import { any, array, decorate, optional, union } from "./ast/utils.ts";
3 | import { EstimateType } from "./ast/estimate_type.ts";
4 | import { createValidate } from "./validator/create_validate.ts";
5 | import { createSanitize } from "./validator/create_sanitize.ts";
6 | import { Decorator } from "./decorator/decorator.ts";
7 | import { d, PredefinedDecorators } from "./decorators.ts";
8 |
9 | export type DecoratorFactory = (
10 | d: PredefinedDecorators,
11 | ) => Decorator> | Decorator>[];
12 |
13 | const helpers = {
14 | any() {
15 | return any();
16 | },
17 | union(types: T[]) {
18 | return union(types);
19 | },
20 | array(of: T) {
21 | return array(of);
22 | },
23 | decorate(of: T, by: DecoratorFactory) {
24 | return decorate(of, by(d) as Decorator>);
25 | },
26 | optional(of: T) {
27 | return optional(of);
28 | },
29 | };
30 |
31 | export const v = Object.assign(function (ast: T) {
32 | return createValidate(ast);
33 | }, helpers);
34 |
35 | export const s = Object.assign(function (ast: T) {
36 | return createSanitize(ast);
37 | }, helpers);
38 |
--------------------------------------------------------------------------------
/validator/create_sanitize.test.ts:
--------------------------------------------------------------------------------
1 | import { assertEquals, assertThrows } from "assert/mod.ts";
2 | import { between } from "../decorators/between.ts";
3 | import { email } from "../decorators/email.ts";
4 | import { ip } from "../decorators/ip.ts";
5 | import { lengthBetween } from "../decorators/length_between.ts";
6 | import { trim } from "../decorators/trim.ts";
7 | import { any, array, decorate, optional, or, union } from "../ast/utils.ts";
8 | import { createSanitize } from "./create_sanitize.ts";
9 | import { InvalidValueError } from "./invalid_value_error.ts";
10 | import { emptyToNull } from "../decorators/empty_to_null.ts";
11 |
12 | Deno.test("validator/create_sanitize, createSanitize string", () => {
13 | const s = createSanitize(String);
14 |
15 | assertEquals(s("1"), "1");
16 | assertEquals(s(""), "");
17 |
18 | const e = assertThrows(
19 | () => s(30),
20 | InvalidValueError,
21 | "It must be a string.",
22 | );
23 | assertEquals(e.path, "");
24 | assertEquals(e.reason, "string");
25 | });
26 |
27 | Deno.test("validator/create_sanitize, createSanitize number", () => {
28 | const s = createSanitize(Number);
29 |
30 | assertEquals(s(30), 30);
31 | assertEquals(s(3.5), 3.5);
32 |
33 | const e = assertThrows(
34 | () => s(true),
35 | InvalidValueError,
36 | "It must be a number.",
37 | );
38 | assertEquals(e.path, "");
39 | assertEquals(e.reason, "number");
40 | });
41 |
42 | Deno.test("validator/create_sanitize, createSanitize boolean", () => {
43 | const s = createSanitize(Boolean);
44 |
45 | assertEquals(s(true), true);
46 | assertEquals(s(false), false);
47 |
48 | const e = assertThrows(
49 | () => s(30),
50 | InvalidValueError,
51 | "It must be a boolean.",
52 | );
53 | assertEquals(e.path, "");
54 | assertEquals(e.reason, "boolean");
55 | });
56 |
57 | Deno.test("validator/create_sanitize, createSanitize bigint", () => {
58 | const s = createSanitize(BigInt);
59 |
60 | assertEquals(s(1n), 1n);
61 |
62 | const e = assertThrows(
63 | () => s(30),
64 | InvalidValueError,
65 | "It must be a bigint.",
66 | );
67 | assertEquals(e.path, "");
68 | assertEquals(e.reason, "bigint");
69 | });
70 |
71 | Deno.test("validator/create_sanitize, createSanitize symbol", () => {
72 | const s = createSanitize(Symbol);
73 |
74 | const sym = Symbol(30);
75 | assertEquals(s(sym), sym);
76 |
77 | const e = assertThrows(
78 | () => s(30),
79 | InvalidValueError,
80 | "It must be a symbol.",
81 | );
82 | assertEquals(e.path, "");
83 | assertEquals(e.reason, "symbol");
84 | });
85 |
86 | Deno.test("validator/create_sanitize, createSanitize string value", () => {
87 | const s = createSanitize("something");
88 |
89 | assertEquals(s("something"), "something");
90 |
91 | const e = assertThrows(
92 | () => s(30),
93 | InvalidValueError,
94 | 'It must be a "something".',
95 | );
96 | assertEquals(e.path, "");
97 | assertEquals(e.reason, '"something"');
98 | });
99 |
100 | Deno.test("validator/create_sanitize, createSanitize number value", () => {
101 | const s = createSanitize(1);
102 |
103 | assertEquals(s(1), 1);
104 |
105 | const e = assertThrows(
106 | () => s(2),
107 | InvalidValueError,
108 | "It must be a 1.",
109 | );
110 | assertEquals(e.path, "");
111 | assertEquals(e.reason, "1");
112 | });
113 |
114 | Deno.test("validator/create_sanitize, createSanitize boolean value", () => {
115 | const s = createSanitize(true);
116 |
117 | assertEquals(s(true), true);
118 |
119 | const e = assertThrows(
120 | () => s(false),
121 | InvalidValueError,
122 | "It must be a true.",
123 | );
124 | assertEquals(e.path, "");
125 | assertEquals(e.reason, "true");
126 | });
127 |
128 | Deno.test("validator/create_sanitize, createSanitize bigint value", () => {
129 | const s = createSanitize(1n);
130 |
131 | assertEquals(s(1n), 1n);
132 |
133 | const e = assertThrows(
134 | () => s(1),
135 | InvalidValueError,
136 | "It must be a 1n.",
137 | );
138 | assertEquals(e.path, "");
139 | assertEquals(e.reason, "1n");
140 | });
141 |
142 | Deno.test("validator/create_sanitize, createSanitize null", () => {
143 | const s = createSanitize(null);
144 |
145 | assertEquals(s(null), null);
146 |
147 | const e = assertThrows(
148 | () => s(undefined),
149 | InvalidValueError,
150 | "It must be a null.",
151 | );
152 | assertEquals(e.path, "");
153 | assertEquals(e.reason, "null");
154 | });
155 |
156 | Deno.test("validator/create_sanitize, createSanitize undefined", () => {
157 | const s = createSanitize(undefined);
158 |
159 | assertEquals(s(undefined), undefined);
160 |
161 | const e = assertThrows(
162 | () => s(null),
163 | InvalidValueError,
164 | "It must be a undefined.",
165 | );
166 | assertEquals(e.path, "");
167 | assertEquals(e.reason, "undefined");
168 | });
169 |
170 | Deno.test("validator/create_sanitize, createSanitize any", () => {
171 | const s = createSanitize(any());
172 |
173 | assertEquals(s(undefined), undefined);
174 | assertEquals(s(1), 1);
175 | assertEquals(s("1"), "1");
176 | assertEquals(s(true), true);
177 | assertEquals(s({}), {});
178 | });
179 |
180 | Deno.test("validator/create_sanitize, createSanitize object", () => {
181 | const Point = {
182 | x: Number,
183 | y: Number,
184 | };
185 | const s = createSanitize({
186 | start: Point,
187 | end: Point,
188 | empty: {},
189 | });
190 |
191 | assertEquals(s({ start: { x: 1, y: 2 }, end: { x: 3, y: 4 }, empty: {} }), {
192 | start: { x: 1, y: 2 },
193 | end: { x: 3, y: 4 },
194 | empty: {},
195 | });
196 |
197 | {
198 | const e = assertThrows(
199 | () => s(null),
200 | InvalidValueError,
201 | "It must be a object.",
202 | );
203 | assertEquals(e.path, "");
204 | assertEquals(e.reason, "object");
205 | }
206 | {
207 | const e = assertThrows(
208 | () =>
209 | s({
210 | start: { x: 1, y: 2 },
211 | end: { x: 3 },
212 | }),
213 | InvalidValueError,
214 | "It must be a number.",
215 | );
216 | assertEquals(e.path, "end.y");
217 | assertEquals(e.reason, "number");
218 | }
219 | });
220 |
221 | Deno.test("validator/create_sanitize, createSanitize union", () => {
222 | const s = createSanitize({
223 | id: union([String, { "#": String }, Number, BigInt, { _: Number }]),
224 | });
225 |
226 | assertEquals(s({ id: "1" }), { id: "1" });
227 | assertEquals(s({ id: { "#": "0x1" } }), { id: { "#": "0x1" } });
228 | assertEquals(s({ id: 1 }), { id: 1 });
229 | assertEquals(s({ id: 1n }), { id: 1n });
230 | assertEquals(s({ id: { _: 10 } }), { id: { _: 10 } });
231 |
232 | {
233 | const e = assertThrows(
234 | () => s({ id: true }),
235 | InvalidValueError,
236 | "It must be one of the types.",
237 | );
238 | assertEquals(e.path, "id");
239 | assertEquals(e.reason, "union");
240 | }
241 | });
242 |
243 | Deno.test("validator/create_sanitize, createSanitize array of any", () => {
244 | const s = createSanitize(Array);
245 |
246 | assertEquals(s([]), []);
247 | assertEquals(s(["1", 1, 1n]), ["1", 1, 1n]);
248 |
249 | assertEquals(s(["1"]), ["1"]);
250 | assertEquals(s([1]), [1]);
251 | assertEquals(s([1n]), [1n]);
252 | assertEquals(s([true]), [true]);
253 | assertEquals(s([null]), [null]);
254 | assertEquals(s([undefined]), [undefined]);
255 |
256 | const e = assertThrows(
257 | () => s({ id: true }),
258 | InvalidValueError,
259 | "It must be a array.",
260 | );
261 | assertEquals(e.path, "");
262 | assertEquals(e.reason, "array");
263 | });
264 |
265 | Deno.test("validator/create_sanitize, createSanitize sugar array", () => {
266 | const s = createSanitize({ ids: [Number] });
267 |
268 | assertEquals(s({ ids: [] }), { ids: [] });
269 | assertEquals(s({ ids: [1, 2, 3] }), { ids: [1, 2, 3] });
270 |
271 | const e = assertThrows(
272 | () => s({ ids: [1, 2, "3"] }),
273 | InvalidValueError,
274 | "It must be a number.",
275 | );
276 | assertEquals(e.path, "ids[2]");
277 | assertEquals(e.reason, "number");
278 | });
279 |
280 | Deno.test("validator/create_sanitize, createSanitize array of primitive", () => {
281 | const s = createSanitize({ ids: array(Number) });
282 |
283 | assertEquals(s({ ids: [] }), { ids: [] });
284 | assertEquals(s({ ids: [1, 2, 3] }), { ids: [1, 2, 3] });
285 |
286 | const e = assertThrows(
287 | () => s({ ids: [1, 2, "3"] }),
288 | InvalidValueError,
289 | "It must be a number.",
290 | );
291 | assertEquals(e.path, "ids[2]");
292 | assertEquals(e.reason, "number");
293 | });
294 |
295 | Deno.test("validator/create_sanitize, createSanitize array of union", () => {
296 | const s = createSanitize(array(or([String, Number, BigInt])));
297 |
298 | assertEquals(s([]), []);
299 | assertEquals(s(["1", 1, 1n]), ["1", 1, 1n]);
300 |
301 | assertEquals(s(["1"]), ["1"]);
302 | assertEquals(s([1]), [1]);
303 | assertEquals(s([1n]), [1n]);
304 |
305 | const e = assertThrows(
306 | () => s([1, 2, true]),
307 | InvalidValueError,
308 | "It must be one of the types.",
309 | );
310 | assertEquals(e.path, "[2]");
311 | assertEquals(e.reason, "union");
312 | });
313 |
314 | Deno.test("validator/create_sanitize, createSanitize decorate", () => {
315 | const s = createSanitize(decorate(String, [trim(), ip("v4")]));
316 |
317 | assertEquals(s("127.0.0.1"), "127.0.0.1");
318 | assertEquals(s(" 127.0.0.1 "), "127.0.0.1");
319 |
320 | const e = assertThrows(
321 | () => s("128.0.0.1.1"),
322 | InvalidValueError,
323 | "This is an invalid value from decorator.",
324 | );
325 | assertEquals(e.path, "");
326 | assertEquals(e.reason, "#ip");
327 | });
328 |
329 | Deno.test("validator/create_sanitize, createSanitize decorate complex", () => {
330 | const s = createSanitize(
331 | decorate(
332 | union([decorate(String, trim()), null]),
333 | emptyToNull(),
334 | ),
335 | );
336 |
337 | assertEquals(s(" 127.0.0.1 "), "127.0.0.1");
338 | assertEquals(s(" "), null);
339 | assertEquals(s(null), null);
340 | });
341 |
342 | Deno.test("validator/create_sanitize, createSanitize complex", () => {
343 | const typeLat = decorate(Number, between(-90, 90));
344 | const typeLng = decorate(Number, between(-180, 180));
345 | const s = createSanitize({
346 | id: Number,
347 | email: decorate(String, [trim(), email()]),
348 | name: optional(String),
349 | password: decorate(String, lengthBetween(8, 20)),
350 | areas: [{
351 | lat: typeLat,
352 | lng: typeLng,
353 | }],
354 | env: {
355 | ip: decorate(String, ip("v4")),
356 | os: {
357 | name: or([
358 | "window" as const,
359 | "osx" as const,
360 | "android" as const,
361 | "iphone" as const,
362 | ]),
363 | version: String,
364 | },
365 | browser: {
366 | name: or([
367 | "chrome" as const,
368 | "firefox" as const,
369 | "edge" as const,
370 | "ie" as const,
371 | ]),
372 | version: String,
373 | },
374 | },
375 | });
376 |
377 | assertEquals(
378 | s({
379 | id: 30,
380 | email: " wan2land@gmail.com ",
381 | name: "wan2land",
382 | password: "12345678",
383 | areas: [
384 | { lat: 0, lng: 0 },
385 | ],
386 | env: {
387 | ip: "127.0.0.1",
388 | os: {
389 | name: "osx",
390 | version: "10.13.1",
391 | },
392 | browser: {
393 | name: "chrome",
394 | version: "62.0.3202.94",
395 | },
396 | },
397 | }),
398 | {
399 | id: 30,
400 | email: "wan2land@gmail.com", // trimmed!
401 | name: "wan2land",
402 | password: "12345678",
403 | areas: [
404 | { lat: 0, lng: 0 },
405 | ],
406 | env: {
407 | ip: "127.0.0.1",
408 | os: {
409 | name: "osx",
410 | version: "10.13.1",
411 | },
412 | browser: {
413 | name: "chrome",
414 | version: "62.0.3202.94",
415 | },
416 | },
417 | },
418 | );
419 | });
420 |
--------------------------------------------------------------------------------
/validator/create_sanitize.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Ast,
3 | AstLiteral,
4 | AstPrimitive,
5 | AstStrict,
6 | Kind,
7 | PrimitiveType,
8 | } from "../ast/ast.ts";
9 | import { desugar } from "../ast/desugar.ts";
10 | import { EstimateType } from "../ast/estimate_type.ts";
11 | import { Decorator } from "../decorator/decorator.ts";
12 | import {
13 | condLiteral,
14 | condPrimitive,
15 | stringifyLiteral,
16 | } from "./create_validate.ts";
17 | import { InvalidValueError } from "./invalid_value_error.ts";
18 |
19 | const astToIndex = new Map();
20 | let fns: string[] = [];
21 | const decoratorToIdx = new Map, number>();
22 | let decorators: Decorator[] = [];
23 |
24 | type InternalError =
25 | | InternalUnionError
26 | | InternalTypeError
27 | | InternalDecoratorError;
28 |
29 | interface InternalUnionError {
30 | type: "union";
31 | path: string;
32 | }
33 |
34 | interface InternalTypeError {
35 | type: "type";
36 | reason: string;
37 | path: string;
38 | }
39 |
40 | interface InternalDecoratorError {
41 | type: "decorator";
42 | reason: string;
43 | path: string;
44 | }
45 |
46 | function throwUnionError(path = "p") {
47 | return `throw{type:"union",path:${path}}`;
48 | }
49 |
50 | function throwTypeError(type: string, path = "p") {
51 | return `throw{type:"type",reason:${JSON.stringify(type)},path:${path}}`;
52 | }
53 |
54 | function throwDecoratorError(reason: string, path = "p") {
55 | return `throw{type:"decorator",reason:${
56 | JSON.stringify(reason)
57 | },path:${path}}`;
58 | }
59 |
60 | function invalidPrimitive(ast: AstPrimitive, value = "v", path = "p"): string {
61 | switch (ast[1]) {
62 | case PrimitiveType.Any: {
63 | return "";
64 | }
65 | case PrimitiveType.Null: {
66 | return `if(${value}!==null)${throwTypeError("null", path)};`;
67 | }
68 | case PrimitiveType.Undefined: {
69 | return `if(typeof ${value}!=="undefined")${
70 | throwTypeError("undefined", path)
71 | };`;
72 | }
73 | case PrimitiveType.String: {
74 | return `if(typeof ${value}!=="string")${throwTypeError("string", path)};`;
75 | }
76 | case PrimitiveType.Number: {
77 | return `if(typeof ${value}!=="number")${throwTypeError("number", path)};`;
78 | }
79 | case PrimitiveType.Boolean: {
80 | return `if(typeof ${value}!=="boolean")${
81 | throwTypeError("boolean", path)
82 | };`;
83 | }
84 | case PrimitiveType.BigInt: {
85 | return `if(typeof ${value}!=="bigint")${throwTypeError("bigint", path)};`;
86 | }
87 | case PrimitiveType.Symbol: {
88 | return `if(typeof ${value}!=="symbol")${throwTypeError("symbol", path)};`;
89 | }
90 | }
91 | throw new Error("unsupported primitive type");
92 | }
93 |
94 | function invalidLiteral(ast: AstLiteral, value: string, path = "p"): string {
95 | const literalValue = stringifyLiteral(ast[1]);
96 | return `if(${value}!==${literalValue})${throwTypeError(literalValue, path)};`;
97 | }
98 |
99 | function invalidAst(ast: AstStrict, value: string, path = "p"): string {
100 | switch (ast[0]) {
101 | case Kind.Primitive: {
102 | return invalidPrimitive(ast, value, path);
103 | }
104 | case Kind.Literal: {
105 | return invalidLiteral(ast, value, path);
106 | }
107 | }
108 | if (!astToIndex.has(ast)) {
109 | traverse(ast);
110 | }
111 | const idx = astToIndex.get(ast)!;
112 | return `${value}=_${idx}(${value},${path});`;
113 | }
114 |
115 | function traverse(ast: AstStrict) {
116 | const idx = fns.length;
117 | const name = `_${idx}`;
118 |
119 | astToIndex.set(ast, idx);
120 | fns.push("");
121 |
122 | switch (ast[0]) {
123 | case Kind.Primitive: {
124 | fns[idx] = `function ${name}(v,p){${invalidPrimitive(ast, "v")}return v}`;
125 | return;
126 | }
127 | case Kind.Literal: {
128 | fns[idx] = `function ${name}(v,p){${invalidLiteral(ast, "v")}return v}`;
129 | return;
130 | }
131 | case Kind.Array: {
132 | let result = `function ${name}(v,p){`;
133 | result += `if(!Array.isArray(v))${throwTypeError("array")};`;
134 | result += `for(let i=0;i] => {
187 | if (!decoratorToIdx.has(decorator)) {
188 | decoratorToIdx.set(decorator, decorators.length);
189 | decorators.push(decorator);
190 | }
191 | return [decoratorToIdx.get(decorator)!, decorator];
192 | });
193 | for (const [dId, decorator] of pairs) {
194 | if (decorator.cast) {
195 | result += `v=_d[${dId}].cast(v);`;
196 | }
197 | }
198 | result += invalidAst(ast[1], "v");
199 | for (const [dId, decorator] of pairs) {
200 | if (decorator.validate) {
201 | result += `if(!_d[${dId}].validate(v))${
202 | throwDecoratorError(decorator.name)
203 | };`;
204 | }
205 | if (decorator.transform) {
206 | result += `v=_d[${dId}].transform(v);`;
207 | }
208 | }
209 | result += `return v}`;
210 | fns[idx] = result;
211 | return;
212 | }
213 | }
214 | throw new Error("Invalid ast");
215 | }
216 |
217 | export function createSanitizeSource(ast: AstStrict) {
218 | fns = [];
219 | decorators = [];
220 | astToIndex.clear();
221 | decoratorToIdx.clear();
222 | traverse(ast);
223 | return {
224 | source: fns.join("\n"),
225 | decorators,
226 | };
227 | }
228 |
229 | function mapCreateError(error: InternalError) {
230 | if (error.type === "decorator") {
231 | const path = error.path.replace(/^\.+/, "");
232 | return new InvalidValueError(
233 | "This is an invalid value from decorator.",
234 | `#${error.reason}`,
235 | path,
236 | );
237 | }
238 | if (error.type === "type") {
239 | const path = error.path.replace(/^\.+/, "");
240 | return new InvalidValueError(
241 | `It must be a ${error.reason}.`,
242 | `${error.reason}`,
243 | path,
244 | );
245 | }
246 | if (error.type === "union") {
247 | const path = error.path.replace(/^\.+/, "");
248 | return new InvalidValueError(
249 | `It must be one of the types.`,
250 | "union",
251 | path,
252 | );
253 | }
254 | throw new Error("Invalid error type");
255 | }
256 |
257 | export function createSanitize(
258 | ast: T,
259 | ): (data: unknown) => EstimateType {
260 | const { source, decorators } = createSanitizeSource(desugar(ast));
261 | return new Function(
262 | "_d",
263 | "_e",
264 | `${source}\nreturn function(d){try{return _0(d,'')}catch(e){throw _e(e)}}`,
265 | )(decorators, mapCreateError);
266 | }
267 |
--------------------------------------------------------------------------------
/validator/create_validate.test.ts:
--------------------------------------------------------------------------------
1 | import { between } from "../decorators/between.ts";
2 | import { email } from "../decorators/email.ts";
3 | import { lengthBetween } from "../decorators/length_between.ts";
4 | import { assert, assertFalse } from "assert/mod.ts";
5 | import { any, array, decorate, optional, or, union } from "../ast/utils.ts";
6 | import { emptyToNull } from "../decorators/empty_to_null.ts";
7 | import { ip } from "../decorators/ip.ts";
8 | import { trim } from "../decorators/trim.ts";
9 | import { createValidate } from "./create_validate.ts";
10 |
11 | Deno.test("validator/create_validate, createValidate string", () => {
12 | const v = createValidate(String);
13 |
14 | assert(v("1"));
15 | assert(v(""));
16 |
17 | assertFalse(v(1));
18 | assertFalse(v(1n));
19 | assertFalse(v(true));
20 | assertFalse(v(null));
21 | assertFalse(v(undefined));
22 | });
23 |
24 | Deno.test("validator/create_validate, createValidate number", () => {
25 | const v = createValidate(Number);
26 |
27 | assert(v(30));
28 | assert(v(3.5));
29 |
30 | assertFalse(v("1"));
31 | assertFalse(v(1n));
32 | assertFalse(v(true));
33 | assertFalse(v(null));
34 | assertFalse(v(undefined));
35 | });
36 |
37 | Deno.test("validator/create_validate, createValidate boolean", () => {
38 | const v = createValidate(Boolean);
39 |
40 | assert(v(true));
41 | assert(v(false));
42 |
43 | assertFalse(v("1"));
44 | assertFalse(v(1));
45 | assertFalse(v(1n));
46 | assertFalse(v(null));
47 | assertFalse(v(undefined));
48 | });
49 |
50 | Deno.test("validator/create_validate, createValidate bigint", () => {
51 | const v = createValidate(BigInt);
52 |
53 | assert(v(1n));
54 |
55 | assertFalse(v("1"));
56 | assertFalse(v(1));
57 | assertFalse(v(true));
58 | assertFalse(v(null));
59 | assertFalse(v(undefined));
60 | });
61 |
62 | Deno.test("validator/create_validate, createValidate symbol", () => {
63 | const v = createValidate(Symbol);
64 |
65 | assert(v(Symbol(1)));
66 |
67 | assertFalse(v("1"));
68 | assertFalse(v(1));
69 | assertFalse(v(true));
70 | assertFalse(v(null));
71 | assertFalse(v(undefined));
72 | });
73 |
74 | Deno.test("validator/create_validate, createValidate string literal", () => {
75 | const v = createValidate("something");
76 |
77 | assert(v("something"));
78 | assertFalse(v("unknown"));
79 | });
80 |
81 | Deno.test("validator/create_validate, createValidate number literal", () => {
82 | const v = createValidate(1);
83 |
84 | assert(v(1));
85 | assertFalse(v(0));
86 | });
87 |
88 | Deno.test("validator/create_validate, createValidate boolean literal", () => {
89 | {
90 | const v = createValidate(true);
91 |
92 | assert(v(true));
93 | assertFalse(v(false));
94 | }
95 | {
96 | const v = createValidate(false);
97 |
98 | assert(v(false));
99 | assertFalse(v(true));
100 | }
101 | });
102 |
103 | Deno.test("validator/create_validate, createValidate bigint literal", () => {
104 | const v = createValidate(1n);
105 |
106 | assert(v(1n));
107 | assertFalse(v(0n));
108 | });
109 |
110 | Deno.test("validator/create_validate, createValidate null", () => {
111 | const v = createValidate(null);
112 |
113 | assert(v(null));
114 |
115 | assertFalse(v("1"));
116 | assertFalse(v(1));
117 | assertFalse(v(1n));
118 | assertFalse(v(true));
119 | assertFalse(v(undefined));
120 | });
121 |
122 | Deno.test("validator/create_validate, createValidate undefined", () => {
123 | const v = createValidate(undefined);
124 |
125 | assert(v(undefined));
126 |
127 | assertFalse(v("1"));
128 | assertFalse(v(1));
129 | assertFalse(v(1n));
130 | assertFalse(v(true));
131 | assertFalse(v(null));
132 | });
133 |
134 | Deno.test("validator/create_validate, createValidate any", () => {
135 | const v = createValidate(any());
136 |
137 | assert(v(undefined));
138 | assert(v("1"));
139 | assert(v(1));
140 | assert(v(1n));
141 | assert(v(true));
142 | assert(v(null));
143 | });
144 |
145 | Deno.test("validator/create_validate, createValidate object", () => {
146 | const Point = {
147 | x: Number,
148 | y: Number,
149 | };
150 | const v = createValidate({
151 | start: Point,
152 | end: Point,
153 | empty: {},
154 | });
155 |
156 | assert(v({ start: { x: 1, y: 2 }, end: { x: 3, y: 4 }, empty: {} }));
157 | });
158 |
159 | Deno.test("validator/create_validate, createValidate union", () => {
160 | const v = createValidate(union([String, Number, BigInt]));
161 |
162 | assert(v("1"));
163 | assert(v(1));
164 | assert(v(1n));
165 |
166 | assertFalse(v(true));
167 | assertFalse(v(null));
168 | assertFalse(v(undefined));
169 | });
170 |
171 | Deno.test("validator/create_validate, createValidate array of any", () => {
172 | const v = createValidate(Array);
173 |
174 | assert(v([]));
175 | assert(v(["1", 1, 1n]));
176 |
177 | assert(v(["1"]));
178 | assert(v([1]));
179 | assert(v([1n]));
180 | assert(v([true]));
181 | assert(v([null]));
182 | assert(v([undefined]));
183 |
184 | assertFalse(v(1));
185 | assertFalse(v(1n));
186 | assertFalse(v(true));
187 | assertFalse(v(null));
188 | assertFalse(v(undefined));
189 | });
190 |
191 | Deno.test("validator/create_validate, createValidate array", () => {
192 | const v = createValidate([or([String, Number, BigInt])]);
193 |
194 | assert(v([]));
195 | assert(v(["1", 1, 1n]));
196 |
197 | assert(v(["1"]));
198 | assert(v([1]));
199 | assert(v([1n]));
200 | assertFalse(v([true]));
201 | assertFalse(v([null]));
202 | assertFalse(v([undefined]));
203 |
204 | assertFalse(v(1));
205 | assertFalse(v(1n));
206 | assertFalse(v(true));
207 | assertFalse(v(null));
208 | assertFalse(v(undefined));
209 | });
210 |
211 | Deno.test("validator/create_validate, createValidate array by util", () => {
212 | const v = createValidate(array(or([String, Number, BigInt])));
213 |
214 | assert(v([]));
215 | assert(v(["1", 1, 1n]));
216 |
217 | assert(v(["1"]));
218 | assert(v([1]));
219 | assert(v([1n]));
220 | assertFalse(v([true]));
221 | assertFalse(v([null]));
222 | assertFalse(v([undefined]));
223 |
224 | assertFalse(v(1));
225 | assertFalse(v(1n));
226 | assertFalse(v(true));
227 | assertFalse(v(null));
228 | assertFalse(v(undefined));
229 | });
230 |
231 | Deno.test("validator/create_validate, createValidate array", () => {
232 | const v = createValidate(array(or([String, Number, BigInt])));
233 |
234 | assert(v([]));
235 | assert(v(["1", 1, 1n]));
236 |
237 | assert(v(["1"]));
238 | assert(v([1]));
239 | assert(v([1n]));
240 | assertFalse(v([true]));
241 | assertFalse(v([null]));
242 | assertFalse(v([undefined]));
243 |
244 | assertFalse(v(1));
245 | assertFalse(v(1n));
246 | assertFalse(v(true));
247 | assertFalse(v(null));
248 | assertFalse(v(undefined));
249 | });
250 |
251 | Deno.test("validator/create_validate, createValidate decorate", () => {
252 | const v = createValidate(decorate(String, ip("v4")));
253 |
254 | assert(v("127.0.0.1"));
255 | assertFalse(v("1"));
256 | });
257 |
258 | Deno.test("validator/create_validate, createValidate decorate complex", () => {
259 | const v = createValidate(
260 | decorate(
261 | union([decorate(String, trim()), null]),
262 | emptyToNull(),
263 | ),
264 | );
265 |
266 | assert(v(" 127.0.0.1 "));
267 | assert(v(" "));
268 | assert(v(null));
269 | });
270 |
271 | Deno.test("validator/create_validate, createValidate complex", () => {
272 | const typeLat = decorate(Number, between(-90, 90));
273 | const typeLng = decorate(Number, between(-180, 180));
274 | const v = createValidate({
275 | id: Number,
276 | email: decorate(String, [trim(), email()]),
277 | name: optional(String),
278 | password: decorate(String, lengthBetween(8, 20)),
279 | areas: [{
280 | lat: typeLat,
281 | lng: typeLng,
282 | }],
283 | env: {
284 | ip: decorate(String, ip("v4")),
285 | os: {
286 | name: or([
287 | "window" as const,
288 | "osx" as const,
289 | "android" as const,
290 | "iphone" as const,
291 | ]),
292 | version: String,
293 | },
294 | browser: {
295 | name: or([
296 | "chrome" as const,
297 | "firefox" as const,
298 | "edge" as const,
299 | "ie" as const,
300 | ]),
301 | version: String,
302 | },
303 | },
304 | });
305 |
306 | assert(
307 | v({
308 | id: 30,
309 | email: " wan2land@gmail.com ",
310 | name: "wan2land",
311 | password: "12345678",
312 | areas: [
313 | { lat: 0, lng: 0 },
314 | ],
315 | env: {
316 | ip: "127.0.0.1",
317 | os: {
318 | name: "osx",
319 | version: "10.13.1",
320 | },
321 | browser: {
322 | name: "chrome",
323 | version: "62.0.3202.94",
324 | },
325 | },
326 | }),
327 | );
328 | });
329 |
--------------------------------------------------------------------------------
/validator/create_validate.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Ast,
3 | AstLiteral,
4 | AstPrimitive,
5 | AstStrict,
6 | Kind,
7 | PrimitiveType,
8 | } from "../ast/ast.ts";
9 | import { desugar } from "../ast/desugar.ts";
10 | import { EstimateType } from "../ast/estimate_type.ts";
11 | import { Decorator } from "../decorator/decorator.ts";
12 |
13 | const astToIndex = new Map();
14 | let fns: string[] = [];
15 | const decoratorToIdx = new Map, number>();
16 | let decorators: Decorator[] = [];
17 |
18 | export function stringifyLiteral(
19 | value: string | number | boolean | bigint,
20 | ): string {
21 | switch (typeof value) {
22 | case "string":
23 | case "number":
24 | case "boolean": {
25 | return JSON.stringify(value);
26 | }
27 | case "bigint": {
28 | return `${value.toString()}n`;
29 | }
30 | }
31 | throw new Error("Unknown literal type");
32 | }
33 |
34 | const comparators = ["!==", "==="];
35 | export function condPrimitive(
36 | ast: AstPrimitive,
37 | is: 1 | 0,
38 | value: string,
39 | ): string {
40 | switch (ast[1]) {
41 | case PrimitiveType.Any: {
42 | return is ? "true" : "false";
43 | }
44 | case PrimitiveType.Null: {
45 | return `${value}${comparators[is]}null`;
46 | }
47 | case PrimitiveType.Undefined: {
48 | return `typeof ${value}${comparators[is]}"undefined"`;
49 | }
50 | case PrimitiveType.String: {
51 | return `typeof ${value}${comparators[is]}"string"`;
52 | }
53 | case PrimitiveType.Number: {
54 | return `typeof ${value}${comparators[is]}"number"`;
55 | }
56 | case PrimitiveType.Boolean: {
57 | return `typeof ${value}${comparators[is]}"boolean"`;
58 | }
59 | case PrimitiveType.BigInt: {
60 | return `typeof ${value}${comparators[is]}"bigint"`;
61 | }
62 | case PrimitiveType.Symbol: {
63 | return `typeof ${value}${comparators[is]}"symbol"`;
64 | }
65 | }
66 | throw new Error("unsupported primitive type");
67 | }
68 |
69 | export function condLiteral(ast: AstLiteral, is: 1 | 0, value: string): string {
70 | const literalValue = stringifyLiteral(ast[1]);
71 | return `${value}${comparators[is]}${literalValue}`;
72 | }
73 |
74 | function condRoot(ast: AstStrict, is: 1 | 0, value: string): string {
75 | switch (ast[0]) {
76 | case Kind.Primitive: {
77 | return condPrimitive(ast, is, value);
78 | }
79 | case Kind.Literal: {
80 | return condLiteral(ast, is, value);
81 | }
82 | }
83 | if (!astToIndex.has(ast)) {
84 | traverse(ast);
85 | }
86 | const idx = astToIndex.get(ast)!;
87 | return is ? `_${idx}(${value})` : `!_${idx}(${value})`;
88 | }
89 |
90 | function traverse(ast: AstStrict) {
91 | const idx = fns.length;
92 | const name = `_${idx}`;
93 |
94 | astToIndex.set(ast, idx);
95 | fns.push("");
96 |
97 | switch (ast[0]) {
98 | case Kind.Primitive: {
99 | fns[idx] = `function ${name}(v){return ${condPrimitive(ast, 1, "v")}}`;
100 | return;
101 | }
102 | case Kind.Literal: {
103 | fns[idx] = `function ${name}(v){return ${condLiteral(ast, 1, "v")}}`;
104 | return;
105 | }
106 | case Kind.Array: {
107 | let result = `function ${name}(v){`;
108 | result += `if(!Array.isArray(v))return false;`;
109 | result += `for(let i=0;i
127 | condRoot(child, 1, `v[${JSON.stringify(key)}]`)
128 | ).join("&&");
129 | result += `}`; // end fn
130 | fns[idx] = result;
131 | return;
132 | }
133 | case Kind.Union: {
134 | const [_, children] = ast;
135 | if (children.length === 0) {
136 | throw new Error("Union must have at least one subtype");
137 | }
138 | fns[idx] = `function ${name}(v){return ${
139 | children.map((child) => condRoot(child, 1, "v")).join("||")
140 | }}`;
141 | return;
142 | }
143 | case Kind.Decorator: {
144 | let result = `function ${name}(v){`;
145 | const pairs = ast[2].map((decorator): [number, Decorator] => {
146 | if (!decoratorToIdx.has(decorator)) {
147 | decoratorToIdx.set(decorator, decorators.length);
148 | decorators.push(decorator);
149 | }
150 | return [decoratorToIdx.get(decorator)!, decorator];
151 | });
152 | for (const [dId, decorator] of pairs) {
153 | if (decorator.cast) {
154 | result += `v=_d[${dId}].cast(v);`;
155 | }
156 | }
157 | result += `if(${condRoot(ast[1], 0, "v")})return false;`;
158 | for (const [dId, decorator] of pairs) {
159 | if (decorator.validate) {
160 | result += `if(!_d[${dId}].validate(v))return false;`;
161 | }
162 | if (decorator.transform) {
163 | result += `v=_d[${dId}].transform(v);`;
164 | }
165 | }
166 | result += `return true`;
167 | result += `}`;
168 | fns[idx] = result;
169 | return;
170 | }
171 | }
172 | throw new Error("Invalid schema");
173 | }
174 |
175 | export function createValidateSource(ast: AstStrict) {
176 | fns = [];
177 | decorators = [];
178 | astToIndex.clear();
179 | decoratorToIdx.clear();
180 | traverse(ast);
181 | return {
182 | source: fns.join("\n"),
183 | decorators,
184 | };
185 | }
186 |
187 | export function createValidate(
188 | ast: T,
189 | ): (data: unknown) => data is EstimateType {
190 | const { source, decorators } = createValidateSource(desugar(ast));
191 | return new Function(
192 | "_d",
193 | `${source}\nreturn _0`,
194 | )(decorators);
195 | }
196 |
--------------------------------------------------------------------------------
/validator/invalid_value_error.ts:
--------------------------------------------------------------------------------
1 | export class InvalidValueError extends Error {
2 | constructor(
3 | message: string,
4 | public reason: string,
5 | public path: string,
6 | ) {
7 | super(message);
8 | this.name = "InvalidValueError";
9 | }
10 | }
11 |
--------------------------------------------------------------------------------