Skip to content

Commit ad2109f

Browse files
committed
feat: initial commit
1 parent 3c31fb7 commit ad2109f

File tree

91 files changed

+5697
-89
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+5697
-89
lines changed

.eslintrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@
22
"extends": [
33
"@jedwards1211/eslint-config-typescript",
44
"eslint-config-prettier"
5-
]
5+
],
6+
"rules": {
7+
"@typescript-eslint/no-explicit-any": 0
8+
}
69
}

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
# typescript-library-skeleton
1+
# @jcoreio/typescript-validators
22

3-
[![CircleCI](https://circleci.com/gh/jedwards1211/typescript-library-skeleton.svg?style=svg)](https://circleci.com/gh/jedwards1211/typescript-library-skeleton)
4-
[![Coverage Status](https://codecov.io/gh/jedwards1211/typescript-library-skeleton/branch/master/graph/badge.svg)](https://codecov.io/gh/jedwards1211/typescript-library-skeleton)
3+
[![CircleCI](https://circleci.com/gh/jcoreio/typescript-validators.svg?style=svg)](https://circleci.com/gh/jcoreio/typescript-validators)
4+
[![Coverage Status](https://codecov.io/gh/jcoreio/typescript-validators/branch/master/graph/badge.svg)](https://codecov.io/gh/jcoreio/typescript-validators)
55
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
66
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
7-
[![npm version](https://badge.fury.io/js/typescript-library-skeleton.svg)](https://badge.fury.io/js/typescript-library-skeleton)
7+
[![npm version](https://badge.fury.io/js/%40jcoreio%2Ftypescript-validators.svg)](https://badge.fury.io/js/%40jcoreio%2Ftypescript-validators)
88

99
This is my personal skeleton for creating an typescript library npm package. You are welcome to use it.
1010

1111
## Quick start
1212

1313
```sh
14-
npx 0-60 clone https://github.com/jedwards1211/typescript-library-skeleton.git
14+
npx 0-60 clone https://github.com/jcoreio/typescript-validators.git
1515
```
1616

1717
## Tools used

package.json

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
2-
"name": "typescript-library-skeleton",
2+
"name": "@jcoreio/typescript-validators",
33
"version": "0.0.0-development",
4-
"description": "my personal ES2015 library project skeleton",
4+
"description": "API input validators with user-friendly error output and TypeScript to ensure you don't miss any properties",
55
"main": "index.js",
66
"sideEffects": false,
77
"scripts": {
@@ -69,17 +69,23 @@
6969
},
7070
"repository": {
7171
"type": "git",
72-
"url": "https://github.com/jedwards1211/typescript-library-skeleton.git"
72+
"url": "https://github.com/jcoreio/typescript-validators.git"
7373
},
7474
"keywords": [
75-
"typescript"
75+
"typescript",
76+
"validation",
77+
"validator",
78+
"api",
79+
"assertion",
80+
"assert",
81+
"validate"
7682
],
7783
"author": "Andy Edwards",
7884
"license": "MIT",
7985
"bugs": {
80-
"url": "https://github.com/jedwards1211/typescript-library-skeleton/issues"
86+
"url": "https://github.com/jcoreio/typescript-validators/issues"
8187
},
82-
"homepage": "https://github.com/jedwards1211/typescript-library-skeleton#readme",
88+
"homepage": "https://github.com/jcoreio/typescript-validators#readme",
8389
"devDependencies": {
8490
"@babel/cli": "^7.1.5",
8591
"@babel/core": "^7.1.6",
@@ -122,7 +128,8 @@
122128
"typescript": "^3.7.2"
123129
},
124130
"dependencies": {
125-
"@babel/runtime": "^7.1.5"
131+
"@babel/runtime": "^7.1.5",
132+
"typelevel-ts": "^0.4.0"
126133
},
127134
"renovate": {
128135
"extends": [

src/Validation.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import Type from './types/Type'
2+
import makeJSONError from './errorReporting/makeJSONError'
3+
import { weakSetAdd, weakSetDelete, weakSetHas } from './cyclic'
4+
5+
export type IdentifierPath = Array<string | number | symbol>
6+
7+
export type ErrorTuple = [IdentifierPath, string, Type<any>]
8+
9+
export default class Validation<T> {
10+
input: T
11+
12+
path: string[] = []
13+
14+
prefix: string = ''
15+
16+
errors: ErrorTuple[] = []
17+
18+
// Tracks whether we're in validation of cyclic objects.
19+
cyclic: WeakMap<Type<any>, WeakSet<any>> = new WeakMap()
20+
21+
constructor(input: T) {
22+
this.input = input
23+
}
24+
25+
inCycle(type: Type<any>, input: any): boolean {
26+
const tracked = this.cyclic.get(type)
27+
if (!tracked) {
28+
return false
29+
} else {
30+
return weakSetHas(tracked, input)
31+
}
32+
}
33+
34+
startCycle(type: Type<any>, input: any) {
35+
let tracked = this.cyclic.get(type)
36+
if (!tracked) {
37+
tracked = new WeakSet()
38+
this.cyclic.set(type, tracked)
39+
}
40+
weakSetAdd(tracked, input)
41+
}
42+
43+
endCycle(type: Type<any>, input: any) {
44+
const tracked = this.cyclic.get(type)
45+
if (tracked) {
46+
weakSetDelete(tracked, input)
47+
}
48+
}
49+
50+
hasErrors(path?: IdentifierPath): boolean {
51+
if (path) {
52+
for (const [candidate] of this.errors) {
53+
if (matchPath(path, candidate)) {
54+
return true
55+
}
56+
}
57+
return false
58+
} else {
59+
return this.errors.length > 0
60+
}
61+
}
62+
63+
addError(
64+
path: IdentifierPath,
65+
expectedType: Type<any>,
66+
message: string
67+
): this {
68+
this.errors.push([path, message, expectedType])
69+
return this
70+
}
71+
72+
clearError(path: IdentifierPath | null | undefined): boolean {
73+
let didClear = false
74+
if (path) {
75+
const errors = []
76+
for (const error of this.errors) {
77+
if (matchPath(path, error[0])) {
78+
didClear = true
79+
} else {
80+
errors.push(error)
81+
}
82+
}
83+
this.errors = errors
84+
} else {
85+
didClear = this.errors.length > 0
86+
this.errors = []
87+
}
88+
return didClear
89+
}
90+
91+
resolvePath(path: IdentifierPath): any {
92+
return resolvePath(this.input, path)
93+
}
94+
toJSON(): any {
95+
return makeJSONError(this)
96+
}
97+
}
98+
99+
const validIdentifierOrAccessor = /^[$A-Z_][0-9A-Z_$[\].]*$/i
100+
101+
export function stringifyPath(path: IdentifierPath): string {
102+
if (!path.length) {
103+
return 'Value'
104+
}
105+
const { length } = path
106+
const parts = new Array(length)
107+
for (let i = 0; i < length; i++) {
108+
const part = path[i]
109+
if (part === '[[Return Type]]') {
110+
parts[i] = 'Return Type'
111+
} else if (
112+
typeof part !== 'string' ||
113+
!validIdentifierOrAccessor.test(part)
114+
) {
115+
parts[i] = `[${String(part)}]`
116+
} else if (i > 0) {
117+
parts[i] = `.${String(part)}`
118+
} else {
119+
parts[i] = String(part)
120+
}
121+
}
122+
return parts.join('')
123+
}
124+
125+
export function resolvePath(input: any, path: IdentifierPath): any {
126+
let subject = input
127+
const { length } = path
128+
for (let i = 0; i < length; i++) {
129+
if (subject == null) {
130+
return undefined
131+
}
132+
const part = path[i]
133+
if (part === '[[Return Type]]') {
134+
continue
135+
}
136+
if (subject instanceof Map) {
137+
subject = subject.get(part)
138+
} else {
139+
subject = subject[part]
140+
}
141+
}
142+
return subject
143+
}
144+
145+
export function matchPath(
146+
path: IdentifierPath,
147+
candidate: IdentifierPath
148+
): boolean {
149+
const { length } = path
150+
if (length > candidate.length) {
151+
return false
152+
}
153+
for (let i = 0; i < length; i++) {
154+
if (candidate[i] !== path[i]) {
155+
return false
156+
}
157+
}
158+
return true
159+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* @flow */
2+
3+
import type TypeContext from '../../../TypeContext';
4+
5+
export function pass (t: TypeContext) {
6+
const ThingType = t.object({
7+
id: t.number(),
8+
name: t.string(),
9+
});
10+
11+
const ThingNameType = t.$propertyType(ThingType, 'name');
12+
13+
return ThingNameType.assert('this is fine');
14+
}
15+
16+
17+
export function fail (t: TypeContext) {
18+
const ThingType = t.object({
19+
id: t.number(),
20+
name: t.string(),
21+
});
22+
23+
const ThingNameType = t.$propertyType(ThingType, 'name');
24+
25+
return ThingNameType.assert(false);
26+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/* @flow */
2+
3+
import type TypeContext from '../../../TypeContext';
4+
5+
export function pass (t: TypeContext) {
6+
const ThingType = t.type('Thing', t.object({
7+
id: t.number(),
8+
name: t.string(),
9+
}));
10+
11+
const ThingIdType = t.$propertyType(ThingType, 'id');
12+
13+
return ThingIdType.assert(123);
14+
}
15+
16+
17+
export function fail (t: TypeContext) {
18+
const ThingType = t.object({
19+
id: t.number(),
20+
name: t.string(),
21+
});
22+
23+
const ThingIdType = t.$propertyType(ThingType, 'id');
24+
25+
return ThingIdType.assert('this is bad');
26+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/* @flow */
2+
3+
import type TypeContext from '../../../TypeContext';
4+
5+
function makeType (t: TypeContext) {
6+
const NumberStringType = t.tuple(
7+
t.number(),
8+
t.string()
9+
);
10+
11+
const Pairer = t.function((fn) => {
12+
const V = fn.typeParameter('V');
13+
return [
14+
t.param('v', t.flowInto(V)),
15+
t.return(t.tuple(V, V))
16+
];
17+
});
18+
19+
return t.$tupleMap(NumberStringType, Pairer);
20+
}
21+
22+
export function pass (t: TypeContext) {
23+
const PairedType = makeType(t);
24+
return PairedType.assert([
25+
[1, 1],
26+
['foo', 'bar']
27+
]);
28+
}
29+
30+
31+
export function fail (t: TypeContext) {
32+
const PairedType = makeType(t);
33+
return PairedType.assert([
34+
['nope', 'bar'],
35+
[false]
36+
]);
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* @flow */
2+
3+
import type TypeContext from '../../../TypeContext';
4+
5+
function makeType (t: TypeContext) {
6+
const NumberStringType = t.tuple(
7+
t.number(),
8+
t.string()
9+
);
10+
11+
const Pairer = t.function((fn) => {
12+
const V = fn.typeParameter('V');
13+
return [
14+
t.param('v', t.flowInto(V)),
15+
t.return(t.tuple(V, V))
16+
];
17+
});
18+
19+
return t.$tupleMap(NumberStringType, Pairer).unwrap();
20+
}
21+
22+
export function pass (t: TypeContext) {
23+
const PairedType = makeType(t);
24+
PairedType.assert([
25+
[1, 1],
26+
['foo', 'bar']
27+
]);
28+
return PairedType.typeName === 'TupleType';
29+
}
30+
31+
32+
export function fail (t: TypeContext) {
33+
const PairedType = makeType(t);
34+
return PairedType.assert([
35+
['nope', 'bar'],
36+
[false]
37+
]);
38+
}

0 commit comments

Comments
 (0)