@fncts/schema 0.0.14 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/_src/Guard.ts ADDED
@@ -0,0 +1,268 @@
1
+ import { globalValue } from "@fncts/base/data/Global";
2
+ import { isRecord } from "@fncts/base/util/predicates";
3
+ import { getKeysForIndexSignature, memoize } from "@fncts/schema/utils";
4
+
5
+ import { ASTTag, getSearchTree } from "./AST.js";
6
+ import { parserFor } from "./Parser.js";
7
+
8
+ /**
9
+ * @tsplus getter fncts.schema.Schema is
10
+ */
11
+ export function is<A>(schema: Schema<A>) {
12
+ return (input: unknown): input is A => {
13
+ return guardFor(schema).is(input);
14
+ };
15
+ }
16
+
17
+ export function guardFor<A>(schema: Schema<A>): Guard<A> {
18
+ return goMemo(schema.ast);
19
+ }
20
+
21
+ const guardStrict = (value: unknown) => Guard((inp): inp is any => inp === value);
22
+
23
+ const guardMemoMap = globalValue(Symbol.for("fncts.schema.Guard.guardMemoMap"), () => new WeakMap<AST, Guard<any>>());
24
+
25
+ function goMemo(ast: AST): Guard<any> {
26
+ const memo = guardMemoMap.get(ast);
27
+ if (memo) {
28
+ return memo;
29
+ }
30
+ const guard = go(ast);
31
+ guardMemoMap.set(ast, guard);
32
+ return guard;
33
+ }
34
+
35
+ function go(ast: AST): Guard<any> {
36
+ AST.concrete(ast);
37
+ switch (ast._tag) {
38
+ case ASTTag.Declaration: {
39
+ const parser = parserFor(ast, true);
40
+ return Guard((inp): inp is any =>
41
+ parser(inp).match(
42
+ () => false,
43
+ () => true,
44
+ ),
45
+ );
46
+ }
47
+ case ASTTag.Literal: {
48
+ return Guard((inp): inp is any => inp === ast.literal);
49
+ }
50
+ case ASTTag.UniqueSymbol: {
51
+ return guardStrict(ast.symbol);
52
+ }
53
+ case ASTTag.VoidKeyword:
54
+ case ASTTag.UndefinedKeyword: {
55
+ return guardStrict(undefined);
56
+ }
57
+ case ASTTag.NeverKeyword: {
58
+ return Guard((inp): inp is never => false);
59
+ }
60
+ case ASTTag.UnknownKeyword:
61
+ case ASTTag.AnyKeyword: {
62
+ return Guard((inp): inp is any => true);
63
+ }
64
+ case ASTTag.NumberKeyword: {
65
+ return Guard.number;
66
+ }
67
+ case ASTTag.BooleanKeyword: {
68
+ return Guard.boolean;
69
+ }
70
+ case ASTTag.StringKeyword: {
71
+ return Guard.string;
72
+ }
73
+ case ASTTag.BigIntKeyword: {
74
+ return Guard.bigint;
75
+ }
76
+ case ASTTag.SymbolKeyword: {
77
+ return Guard((inp): inp is symbol => typeof inp === "symbol");
78
+ }
79
+ case ASTTag.ObjectKeyword: {
80
+ return Guard(isObject);
81
+ }
82
+ case ASTTag.TemplateLiteral: {
83
+ const parser = parserFor(ast, true);
84
+ return Guard((inp): inp is any =>
85
+ parser(inp).match(
86
+ () => false,
87
+ () => true,
88
+ ),
89
+ );
90
+ }
91
+ case ASTTag.Tuple: {
92
+ const elements = ast.elements.map((element) => goMemo(element.type));
93
+ const restElements = ast.rest.match(
94
+ () => Vector.empty<Guard<any>>(),
95
+ (rest) => rest.map(goMemo),
96
+ );
97
+
98
+ return Guard((input): input is any => {
99
+ if (!Array.isArray(input)) {
100
+ return false;
101
+ }
102
+
103
+ let i = 0;
104
+ for (; i < elements.length; i++) {
105
+ if (input.length < i + 1) {
106
+ if (!ast.elements[i]!.isOptional) {
107
+ return false;
108
+ }
109
+ } else {
110
+ const guard = elements[i]!;
111
+ if (!guard.is(input[i])) {
112
+ return false;
113
+ }
114
+ }
115
+ }
116
+
117
+ if (restElements.length > 0) {
118
+ const head = restElements.unsafeHead!;
119
+ const tail = restElements.tail;
120
+ for (; i < input.length - tail.length; i++) {
121
+ if (!head.is(input[i])) {
122
+ return false;
123
+ }
124
+ }
125
+ for (let j = 0; j < tail.length; j++) {
126
+ i += j;
127
+ if (input.length < i + 1) {
128
+ return false;
129
+ }
130
+ const guard = tail[j]!;
131
+ if (!guard.is(input[i])) {
132
+ return false;
133
+ }
134
+ }
135
+ }
136
+
137
+ return true;
138
+ });
139
+ }
140
+ case ASTTag.TypeLiteral: {
141
+ if (ast.propertySignatures.length === 0 && ast.indexSignatures.length === 0) {
142
+ return Guard((input): input is Exclude<typeof input, null> => input !== null);
143
+ }
144
+ const propertySignatureTypes = ast.propertySignatures.map((ps) => goMemo(ps.type));
145
+ const indexSignatures = ast.indexSignatures.map((is) => [goMemo(is.parameter), goMemo(is.type)] as const);
146
+ return Guard((input): input is any => {
147
+ if (!isRecord(input)) {
148
+ return false;
149
+ }
150
+
151
+ const expectedKeys: any = {};
152
+
153
+ console.log(ast.propertySignatures);
154
+ for (let i = 0; i < propertySignatureTypes.length; i++) {
155
+ const ps = ast.propertySignatures[i]!;
156
+ const guard = propertySignatureTypes[i]!;
157
+ const name = ps.name;
158
+ expectedKeys[name] = null;
159
+ if (!Object.prototype.hasOwnProperty.call(input, name)) {
160
+ if (!ps.isOptional) {
161
+ return false;
162
+ }
163
+ } else {
164
+ if (!guard(input[name])) {
165
+ return false;
166
+ }
167
+ }
168
+ }
169
+
170
+ if (indexSignatures.length > 0) {
171
+ for (let i = 0; i < indexSignatures.length; i++) {
172
+ const [parameter, type] = indexSignatures[i]!;
173
+ const keys = getKeysForIndexSignature(input, ast.indexSignatures[i]!.parameter);
174
+ for (const key of keys) {
175
+ if (Object.prototype.hasOwnProperty.call(expectedKeys, key)) {
176
+ continue;
177
+ }
178
+
179
+ if (!parameter(key)) {
180
+ return false;
181
+ }
182
+
183
+ if (!type(input[key])) {
184
+ return false;
185
+ }
186
+ }
187
+ }
188
+ }
189
+
190
+ return true;
191
+ });
192
+ }
193
+ case ASTTag.Union: {
194
+ const searchTree = getSearchTree(ast.types, true);
195
+ const ownKeys = Reflect.ownKeys(searchTree.keys);
196
+ const len = ownKeys.length;
197
+ const otherwise = searchTree.otherwise;
198
+ const map = new Map<any, Guard<any>>();
199
+ ast.types.forEach((ast) => {
200
+ map.set(ast, goMemo(ast));
201
+ });
202
+ return Guard((input): input is any => {
203
+ if (len > 0) {
204
+ if (isRecord(input)) {
205
+ for (let i = 0; i < len; i++) {
206
+ const name = ownKeys[i]!;
207
+ const buckets = searchTree.keys[name]!.buckets;
208
+ if (Object.prototype.hasOwnProperty.call(input, name)) {
209
+ const literal = String(input[name]);
210
+ if (Object.prototype.hasOwnProperty.call(buckets, literal)) {
211
+ const bucket: ReadonlyArray<AST> = buckets[literal]!;
212
+ for (let i = 0; i < bucket.length; i++) {
213
+ if (map.get(bucket[i])!(input)) {
214
+ return true;
215
+ }
216
+ }
217
+ }
218
+ }
219
+ }
220
+ }
221
+ }
222
+ for (let i = 0; i < otherwise.length; i++) {
223
+ if (map.get(otherwise[i])!(input)) {
224
+ return true;
225
+ }
226
+ }
227
+ return false;
228
+ });
229
+ }
230
+ case ASTTag.Lazy: {
231
+ const f = () => goMemo(ast.getAST());
232
+ const get = memoize<void, Guard<any>>(f);
233
+ return Guard((input): input is any => get()(input));
234
+ }
235
+ case ASTTag.Enum: {
236
+ return Guard((input): input is any => ast.enums.some(([_, value]) => value === input));
237
+ }
238
+ case ASTTag.Refinement: {
239
+ const from = goMemo(ast.from);
240
+ return Guard((input): input is any => {
241
+ if (!from(input)) {
242
+ return false;
243
+ }
244
+ if (!ast.predicate(input)) {
245
+ return false;
246
+ }
247
+ return true;
248
+ });
249
+ }
250
+ case ASTTag.Transform: {
251
+ return goMemo(ast.to);
252
+ }
253
+ case ASTTag.Validation: {
254
+ const from = goMemo(ast.from);
255
+ return Guard((input): input is any => {
256
+ if (!from(input)) {
257
+ return false;
258
+ }
259
+ for (const validation of ast.validation) {
260
+ if (!validation.validate(input)) {
261
+ return false;
262
+ }
263
+ }
264
+ return true;
265
+ });
266
+ }
267
+ }
268
+ }
@@ -1,4 +1,4 @@
1
- import type { TemplateLiteral, TemplateLiteralSpan, Validation } from "@fncts/schema/AST";
1
+ import type { Refinement, TemplateLiteral, TemplateLiteralSpan, Transform, Validation } from "@fncts/schema/AST";
2
2
 
3
3
  import { showWithOptions } from "@fncts/base/data/Showable";
4
4
  import { concrete } from "@fncts/schema/AST";
@@ -12,6 +12,8 @@ export const enum ParseErrorTag {
12
12
  Missing,
13
13
  Unexpected,
14
14
  UnionMember,
15
+ Refinement,
16
+ Transformation,
15
17
  }
16
18
 
17
19
  /**
@@ -24,8 +26,9 @@ export type ParseError =
24
26
  | KeyError
25
27
  | MissingError
26
28
  | UnexpectedError
27
- | UnexpectedError
28
- | UnionMemberError;
29
+ | UnionMemberError
30
+ | RefinementError
31
+ | TransformationError;
29
32
 
30
33
  /**
31
34
  * @tsplus companion fncts.schema.ParseError.TypeError
@@ -78,7 +81,7 @@ export class KeyError {
78
81
  }
79
82
 
80
83
  /**
81
- * @tsplus static fncts.schema.ParseError.IndexError __call
84
+ * @tsplus static fncts.schema.ParseError.KeyError __call
82
85
  * @tsplus static fncts.schema.ParseErrorOps KeyError
83
86
  */
84
87
  export function keyError(keyAST: AST, key: any, errors: Vector<ParseError>): ParseError {
@@ -129,6 +132,58 @@ export function unionMemberError(errors: Vector<ParseError>): ParseError {
129
132
  return new UnionMemberError(errors);
130
133
  }
131
134
 
135
+ /**
136
+ * @tsplus companion fncts.schema.ParseError.RefinementError
137
+ */
138
+ export class RefinementError {
139
+ readonly _tag = ParseErrorTag.Refinement;
140
+ constructor(
141
+ readonly ast: Refinement,
142
+ readonly actual: unknown,
143
+ readonly kind: "From" | "Predicate",
144
+ readonly errors: Vector<ParseError>,
145
+ ) {}
146
+ }
147
+
148
+ /**
149
+ * @tsplus static fncts.schema.ParseError.RefinementError __call
150
+ * @tsplus static fncts.schema.ParseErrorOps RefinementError
151
+ */
152
+ export function refinementError(
153
+ ast: Refinement,
154
+ actual: unknown,
155
+ kind: "From" | "Predicate",
156
+ errors: Vector<ParseError>,
157
+ ): ParseError {
158
+ return new RefinementError(ast, actual, kind, errors);
159
+ }
160
+
161
+ /**
162
+ * @tsplus companion fncts.schema.ParseError.TransformationError
163
+ */
164
+ export class TransformationError {
165
+ readonly _tag = ParseErrorTag.Transformation;
166
+ constructor(
167
+ readonly ast: Transform,
168
+ readonly actual: unknown,
169
+ readonly kind: "Encoded" | "Transformation" | "Type",
170
+ readonly errors: Vector<ParseError>,
171
+ ) {}
172
+ }
173
+
174
+ /**
175
+ * @tsplus static fncts.schema.ParseError.TransformationError __call
176
+ * @tsplus static fncts.schema.ParseErrorOps TransformationError
177
+ */
178
+ export function transformationError(
179
+ ast: Transform,
180
+ actual: unknown,
181
+ kind: "Encoded" | "Transformation" | "Type",
182
+ errors: Vector<ParseError>,
183
+ ): ParseError {
184
+ return new TransformationError(ast, actual, kind, errors);
185
+ }
186
+
132
187
  /**
133
188
  * @tsplus static fncts.schema.ParseErrorOps format
134
189
  */
@@ -153,6 +208,31 @@ function formatTemplateLiteral(ast: TemplateLiteral): string {
153
208
  return ast.head + ast.spans.map((span) => formatTemplateLiteralSpan(span) + span.literal).join("");
154
209
  }
155
210
 
211
+ function formatRefinementKind(error: RefinementError): string {
212
+ switch (error.kind) {
213
+ case "From": {
214
+ return "From side refinement failure";
215
+ }
216
+ case "Predicate": {
217
+ return "Predicate refinement failure";
218
+ }
219
+ }
220
+ }
221
+
222
+ function formatTransformationKind(error: TransformationError): string {
223
+ switch (error.kind) {
224
+ case "Encoded": {
225
+ return "Encoded side transformation failure";
226
+ }
227
+ case "Transformation": {
228
+ return "Transformation process failure";
229
+ }
230
+ case "Type": {
231
+ return "Type side transformation failure";
232
+ }
233
+ }
234
+ }
235
+
156
236
  function getExpected(ast: AST): Maybe<string> {
157
237
  return ast.annotations
158
238
  .get(ASTAnnotation.Identifier)
@@ -243,5 +323,9 @@ function go(error: ParseError): RoseTree<string> {
243
323
  return RoseTree("is missing");
244
324
  case ParseErrorTag.UnionMember:
245
325
  return RoseTree("union member", error.errors.map(go));
326
+ case ParseErrorTag.Refinement:
327
+ return RoseTree(formatRefinementKind(error), error.errors.map(go));
328
+ case ParseErrorTag.Transformation:
329
+ return RoseTree(formatTransformationKind(error), error.errors.map(go));
246
330
  }
247
331
  }
@@ -7,7 +7,7 @@ import { parserFor } from "./interpreter.js";
7
7
  * @tsplus getter fncts.schema.Parser decode
8
8
  */
9
9
  export function decode<A>(schema: Schema<A>): Parser<A> {
10
- return parserFor(schema.ast);
10
+ return parserFor(schema.ast, true);
11
11
  }
12
12
 
13
13
  /**
@@ -23,7 +23,7 @@ export function decodeMaybe<A>(schema: Schema<A>): <A>(input: A, options?: Parse
23
23
  * @tsplus getter fncts.schema.Parser encode
24
24
  */
25
25
  export function encode<A>(schema: Schema<A>): <A>(input: A, options?: ParseOptions) => ParseResult<unknown> {
26
- return parserFor(schema.ast.reverse);
26
+ return parserFor(schema.ast, false);
27
27
  }
28
28
 
29
29
  /**
@@ -31,35 +31,22 @@ export function encode<A>(schema: Schema<A>): <A>(input: A, options?: ParseOptio
31
31
  * @tsplus getter fncts.schema.Parser encodeMaybe
32
32
  */
33
33
  export function encodeMaybe<A>(schema: Schema<A>): <A>(input: A, options?: ParseOptions) => Maybe<unknown> {
34
- return parseMaybe(schema.ast.reverse);
35
- }
36
-
37
- /**
38
- * @tsplus getter fncts.schema.Schema is
39
- * @tsplus getter fncts.schema.Parser is
40
- */
41
- export function is<A>(schema: Schema<A>) {
42
- return (input: unknown, options?: ParseOptions): input is A => {
43
- return parserFor(schema.ast)(input, options).isRight();
44
- };
34
+ return (input, options) => encode(schema)(input, options).toMaybe;
45
35
  }
46
36
 
47
37
  function parseMaybe(ast: AST) {
48
- const parse = parserFor(ast);
38
+ const parse = parserFor(ast, true);
49
39
  return (input: unknown, options?: ParseOptions): Maybe<any> => {
50
40
  return parse(input, options).toMaybe;
51
41
  };
52
42
  }
53
43
 
54
44
  function parseOrThrow(ast: AST) {
55
- const parser = parserFor(ast);
45
+ const parser = parserFor(ast, true);
56
46
  return (input: unknown, options?: ParseOptions) => {
57
- return parser(input, options).match({
58
- Left: (failure) => {
59
- throw new Error(ParseError.format(failure.errors));
60
- },
61
- Right: Function.identity,
62
- });
47
+ return parser(input, options).match((failure) => {
48
+ throw new Error(ParseError.format(failure.errors));
49
+ }, Function.identity);
63
50
  };
64
51
  }
65
52