tjs-lang 0.7.3 → 0.7.5
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/demo/docs.json +29 -29
- package/dist/index.js +175 -176
- package/dist/index.js.map +5 -44
- package/dist/scripts/build.d.ts +8 -4
- package/dist/src/lang/runtime.d.ts +8 -4
- package/dist/src/types/Type.d.ts +5 -5
- package/dist/tjs-batteries.js +3 -4
- package/dist/tjs-batteries.js.map +5 -13
- package/dist/tjs-eval.js +47 -0
- package/dist/tjs-eval.js.map +7 -0
- package/dist/tjs-from-ts.js +58 -0
- package/dist/tjs-from-ts.js.map +7 -0
- package/dist/tjs-lang.js +349 -0
- package/dist/tjs-lang.js.map +7 -0
- package/dist/tjs-vm.js +51 -52
- package/dist/tjs-vm.js.map +4 -19
- package/package.json +17 -11
- package/src/lang/emitters/js.ts +4 -4
- package/src/lang/function-predicate.test.ts +8 -6
- package/src/lang/runtime.test.ts +58 -0
- package/src/lang/runtime.ts +27 -13
- package/src/types/Type.test.ts +68 -19
- package/src/types/Type.ts +80 -54
- package/dist/tjs-full.js +0 -437
- package/dist/tjs-full.js.map +0 -46
- package/dist/tjs-transpiler.js +0 -3
- package/dist/tjs-transpiler.js.map +0 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.5",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -31,22 +31,27 @@
|
|
|
31
31
|
"./eval": {
|
|
32
32
|
"bun": "./src/lang/eval.ts",
|
|
33
33
|
"types": "./dist/src/lang/eval.d.ts",
|
|
34
|
-
"default": "./dist/
|
|
34
|
+
"default": "./dist/tjs-eval.js"
|
|
35
35
|
},
|
|
36
36
|
"./lang": {
|
|
37
|
-
"bun": "./src/lang/
|
|
38
|
-
"types": "./dist/src/lang/
|
|
39
|
-
"default": "./dist/lang
|
|
40
|
-
},
|
|
41
|
-
"./lang/eval": {
|
|
42
|
-
"bun": "./src/lang/eval.ts",
|
|
43
|
-
"types": "./dist/src/lang/eval.d.ts",
|
|
44
|
-
"default": "./dist/lang/eval.js"
|
|
37
|
+
"bun": "./src/lang/transpiler.ts",
|
|
38
|
+
"types": "./dist/src/lang/transpiler.d.ts",
|
|
39
|
+
"default": "./dist/tjs-lang.js"
|
|
45
40
|
},
|
|
46
41
|
"./lang/from-ts": {
|
|
47
42
|
"bun": "./src/lang/emitters/from-ts.ts",
|
|
48
43
|
"types": "./dist/src/lang/emitters/from-ts.d.ts",
|
|
49
|
-
"default": "./dist/
|
|
44
|
+
"default": "./dist/tjs-from-ts.js"
|
|
45
|
+
},
|
|
46
|
+
"./vm": {
|
|
47
|
+
"bun": "./src/vm/index.ts",
|
|
48
|
+
"types": "./dist/src/vm/index.d.ts",
|
|
49
|
+
"default": "./dist/tjs-vm.js"
|
|
50
|
+
},
|
|
51
|
+
"./batteries": {
|
|
52
|
+
"bun": "./src/batteries/index.ts",
|
|
53
|
+
"types": "./dist/src/batteries/index.d.ts",
|
|
54
|
+
"default": "./dist/tjs-batteries.js"
|
|
50
55
|
},
|
|
51
56
|
"./src": "./src/index.ts",
|
|
52
57
|
"./editors/monaco": "./editors/monaco/ajs-monarch.js",
|
|
@@ -94,6 +99,7 @@
|
|
|
94
99
|
"acorn-walk": "^8.3.4",
|
|
95
100
|
"chokidar": "^4.0.3",
|
|
96
101
|
"codemirror": "^6.0.2",
|
|
102
|
+
"esbuild": "^0.28.0",
|
|
97
103
|
"eslint": "^8.57.1",
|
|
98
104
|
"firebase": "^10.12.0",
|
|
99
105
|
"firebase-admin": "^13.6.0",
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -822,8 +822,8 @@ export function transpileToJS(
|
|
|
822
822
|
// Core: MonadicError + typeError (needed by almost all validated functions)
|
|
823
823
|
if (needsTypeError) {
|
|
824
824
|
inlineParts.push(
|
|
825
|
-
`class MonadicError extends Error{constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c}}`,
|
|
826
|
-
`function typeError(p,e,v){const a=v===null?'null':typeof v;const
|
|
825
|
+
`class MonadicError extends Error{constructor(m,p,e,a,c,r){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;this.reason=r}}`,
|
|
826
|
+
`function typeError(p,e,v,r){const a=v===null?'null':typeof v;const m=r?'Expected '+e+" for '"+p+"': "+r:'Expected '+e+" for '"+p+"', got "+a;const err=new MonadicError(m,p,e,a,undefined,r);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
|
|
827
827
|
`function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
|
|
828
828
|
)
|
|
829
829
|
}
|
|
@@ -892,8 +892,8 @@ export function transpileToJS(
|
|
|
892
892
|
// bang depends on typeError and isMonadicError — ensure they're inlined
|
|
893
893
|
if (!needsTypeError) {
|
|
894
894
|
inlineParts.push(
|
|
895
|
-
`class MonadicError extends Error{constructor(m,p,e,a,c){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c}}`,
|
|
896
|
-
`function typeError(p,e,v){const a=v===null?'null':typeof v;const
|
|
895
|
+
`class MonadicError extends Error{constructor(m,p,e,a,c,r){super(m);this.name='MonadicError';this.path=p;this.expected=e;this.actual=a;this.callStack=c;this.reason=r}}`,
|
|
896
|
+
`function typeError(p,e,v,r){const a=v===null?'null':typeof v;const m=r?'Expected '+e+" for '"+p+"': "+r:'Expected '+e+" for '"+p+"', got "+a;const err=new MonadicError(m,p,e,a,undefined,r);const c=globalThis.__tjs?.getConfig?.();if(c?.logTypeErrors)console.error('[TJS TypeError] '+err.message);if(c?.throwTypeErrors)throw err;return err}`,
|
|
897
897
|
`function isMonadicError(v){return v instanceof Error&&v.name==='MonadicError'&&'path' in v}`
|
|
898
898
|
)
|
|
899
899
|
}
|
|
@@ -12,9 +12,11 @@ describe('FunctionPredicate runtime', () => {
|
|
|
12
12
|
})
|
|
13
13
|
expect(Callback.check(() => {})).toBe(true)
|
|
14
14
|
expect(Callback.check((x: number) => String(x))).toBe(true)
|
|
15
|
-
expect(Callback.check(42)).toBe(
|
|
16
|
-
expect(Callback.check('not a function')).toBe(
|
|
17
|
-
|
|
15
|
+
expect(Callback.check(42)).toBe('expected function, got number')
|
|
16
|
+
expect(Callback.check('not a function')).toBe(
|
|
17
|
+
'expected function, got string'
|
|
18
|
+
)
|
|
19
|
+
expect(Callback.check(null)).toBe('expected function, got null')
|
|
18
20
|
})
|
|
19
21
|
|
|
20
22
|
it('should create from existing typed function', () => {
|
|
@@ -73,7 +75,7 @@ describe('FunctionPredicate runtime', () => {
|
|
|
73
75
|
x: { type: { kind: 'integer' }, example: 0 },
|
|
74
76
|
},
|
|
75
77
|
}
|
|
76
|
-
expect(Binop.check(wrongArity)).toBe(
|
|
78
|
+
expect(Binop.check(wrongArity)).toBe('expected 2 params, got 1')
|
|
77
79
|
})
|
|
78
80
|
|
|
79
81
|
it('should reject function with wrong param types via __tjs metadata', () => {
|
|
@@ -93,7 +95,7 @@ describe('FunctionPredicate runtime', () => {
|
|
|
93
95
|
;(badFn as any).__tjs = {
|
|
94
96
|
params: { n: { type: { kind: 'integer' }, example: 0 } },
|
|
95
97
|
}
|
|
96
|
-
expect(StrFn.check(badFn)).toBe(
|
|
98
|
+
expect(StrFn.check(badFn)).toBe("param 'name' expected string, got integer")
|
|
97
99
|
})
|
|
98
100
|
|
|
99
101
|
it('should accept function with any-typed params', () => {
|
|
@@ -230,7 +232,7 @@ describe('Generic FunctionPredicate runtime', () => {
|
|
|
230
232
|
expect(StringCreator.returns).toBe('')
|
|
231
233
|
expect(StringCreator.params.x).toBe('')
|
|
232
234
|
expect(StringCreator.check(() => {})).toBe(true)
|
|
233
|
-
expect(StringCreator.check(42)).toBe(
|
|
235
|
+
expect(StringCreator.check(42)).toBe('expected function, got number')
|
|
234
236
|
})
|
|
235
237
|
|
|
236
238
|
it('should use default type arg when none provided', () => {
|
package/src/lang/runtime.test.ts
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
Is,
|
|
29
29
|
IsNot,
|
|
30
30
|
tjsEquals,
|
|
31
|
+
checkType,
|
|
32
|
+
MonadicError,
|
|
33
|
+
isMonadicError,
|
|
31
34
|
} from './runtime'
|
|
32
35
|
import { Eval, SafeFunction } from './eval'
|
|
33
36
|
|
|
@@ -1120,3 +1123,58 @@ function add(a: 1, b: 2): 3 {
|
|
|
1120
1123
|
}
|
|
1121
1124
|
})
|
|
1122
1125
|
})
|
|
1126
|
+
|
|
1127
|
+
describe('predicate reason strings', () => {
|
|
1128
|
+
it('typeError includes reason in message and field', () => {
|
|
1129
|
+
const err = typeError('foo.bar', 'even number', 3, 'value is odd')
|
|
1130
|
+
expect(err.message).toContain('value is odd')
|
|
1131
|
+
expect(err.message).toContain('foo.bar')
|
|
1132
|
+
expect(err.reason).toBe('value is odd')
|
|
1133
|
+
expect(isMonadicError(err)).toBe(true)
|
|
1134
|
+
})
|
|
1135
|
+
|
|
1136
|
+
it('typeError without reason works as before', () => {
|
|
1137
|
+
const err = typeError('foo.bar', 'string', 42)
|
|
1138
|
+
expect(err.message).toContain('got number')
|
|
1139
|
+
expect(err.reason).toBeUndefined()
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
it('checkType captures reason from predicate', () => {
|
|
1143
|
+
const EvenType = {
|
|
1144
|
+
check: (v: unknown): boolean | string => {
|
|
1145
|
+
if (typeof v !== 'number') return `expected number, got ${typeof v}`
|
|
1146
|
+
if (v % 2 !== 0) return `${v} is odd`
|
|
1147
|
+
return true
|
|
1148
|
+
},
|
|
1149
|
+
description: 'even number',
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// Pass
|
|
1153
|
+
expect(checkType(4, EvenType, 'test.val')).toBeNull()
|
|
1154
|
+
|
|
1155
|
+
// Fail with reason
|
|
1156
|
+
const err = checkType(3, EvenType, 'test.val')
|
|
1157
|
+
expect(err).not.toBeNull()
|
|
1158
|
+
expect(err!.message).toContain('3 is odd')
|
|
1159
|
+
expect(err!.reason).toBe('3 is odd')
|
|
1160
|
+
|
|
1161
|
+
// Fail with different reason
|
|
1162
|
+
const err2 = checkType('x', EvenType, 'test.val')
|
|
1163
|
+
expect(err2).not.toBeNull()
|
|
1164
|
+
expect(err2!.message).toContain('expected number, got string')
|
|
1165
|
+
})
|
|
1166
|
+
|
|
1167
|
+
it('checkType works with boolean-only predicates', () => {
|
|
1168
|
+
const PositiveType = {
|
|
1169
|
+
check: (v: unknown) => typeof v === 'number' && v > 0,
|
|
1170
|
+
description: 'positive number',
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
expect(checkType(5, PositiveType, 'x')).toBeNull()
|
|
1174
|
+
|
|
1175
|
+
const err = checkType(-1, PositiveType, 'x')
|
|
1176
|
+
expect(err).not.toBeNull()
|
|
1177
|
+
expect(err!.message).toContain('positive number')
|
|
1178
|
+
expect(err!.reason).toBeUndefined()
|
|
1179
|
+
})
|
|
1180
|
+
})
|
package/src/lang/runtime.ts
CHANGED
|
@@ -142,13 +142,16 @@ export class MonadicError extends Error {
|
|
|
142
142
|
readonly actual?: string
|
|
143
143
|
/** TJS call stack (only in debug mode) - shows source locations */
|
|
144
144
|
readonly callStack?: string[]
|
|
145
|
+
/** Why the check failed (from predicate reason strings) */
|
|
146
|
+
readonly reason?: string
|
|
145
147
|
|
|
146
148
|
constructor(
|
|
147
149
|
message: string,
|
|
148
150
|
path: string,
|
|
149
151
|
expected?: string,
|
|
150
152
|
actual?: string,
|
|
151
|
-
callStack?: string[]
|
|
153
|
+
callStack?: string[],
|
|
154
|
+
reason?: string
|
|
152
155
|
) {
|
|
153
156
|
super(message)
|
|
154
157
|
this.name = 'MonadicError'
|
|
@@ -156,6 +159,7 @@ export class MonadicError extends Error {
|
|
|
156
159
|
this.expected = expected
|
|
157
160
|
this.actual = actual
|
|
158
161
|
this.callStack = callStack
|
|
162
|
+
this.reason = reason
|
|
159
163
|
// Maintains proper stack trace in V8 environments
|
|
160
164
|
if (Error.captureStackTrace) {
|
|
161
165
|
Error.captureStackTrace(this, MonadicError)
|
|
@@ -177,18 +181,16 @@ export class MonadicError extends Error {
|
|
|
177
181
|
export function typeError(
|
|
178
182
|
path: string,
|
|
179
183
|
expected: string,
|
|
180
|
-
value: unknown
|
|
184
|
+
value: unknown,
|
|
185
|
+
reason?: string
|
|
181
186
|
): MonadicError {
|
|
182
187
|
const actual = value === null ? 'null' : typeof value
|
|
183
188
|
// Capture call stack in debug mode (getStack returns [] if not in debug mode)
|
|
184
189
|
const stack = config.callStacks || config.debug ? getStack() : undefined
|
|
185
|
-
const
|
|
186
|
-
`Expected ${expected} for '${path}'
|
|
187
|
-
path,
|
|
188
|
-
|
|
189
|
-
actual,
|
|
190
|
-
stack
|
|
191
|
-
)
|
|
190
|
+
const msg = reason
|
|
191
|
+
? `Expected ${expected} for '${path}': ${reason}`
|
|
192
|
+
: `Expected ${expected} for '${path}', got ${actual}`
|
|
193
|
+
const err = new MonadicError(msg, path, expected, actual, stack, reason)
|
|
192
194
|
|
|
193
195
|
// Track in error history ring buffer (zero cost on happy path)
|
|
194
196
|
if (config.trackErrors !== false) {
|
|
@@ -238,6 +240,8 @@ export interface TJSError {
|
|
|
238
240
|
stack?: string[]
|
|
239
241
|
expected?: string
|
|
240
242
|
actual?: string
|
|
243
|
+
/** Why the check failed (from predicate reason strings) */
|
|
244
|
+
reason?: string
|
|
241
245
|
cause?: Error | TJSError
|
|
242
246
|
/** Source location for error reporting */
|
|
243
247
|
loc?: { start: number; end: number }
|
|
@@ -779,7 +783,9 @@ export function isNativeType(value: unknown, typeName: string): boolean {
|
|
|
779
783
|
*/
|
|
780
784
|
export function checkType(
|
|
781
785
|
value: unknown,
|
|
782
|
-
expected:
|
|
786
|
+
expected:
|
|
787
|
+
| string
|
|
788
|
+
| { check: (v: unknown) => boolean | string; description: string },
|
|
783
789
|
path?: string
|
|
784
790
|
): TJSError | null {
|
|
785
791
|
// If value is already an error, propagate it
|
|
@@ -791,11 +797,17 @@ export function checkType(
|
|
|
791
797
|
expected !== null &&
|
|
792
798
|
'check' in expected
|
|
793
799
|
) {
|
|
794
|
-
|
|
795
|
-
|
|
800
|
+
const result = expected.check(value)
|
|
801
|
+
if (result === true) return null
|
|
802
|
+
const reason = typeof result === 'string' ? result : undefined
|
|
803
|
+
const msg = reason
|
|
804
|
+
? `Expected ${expected.description} for '${path}': ${reason}`
|
|
805
|
+
: `Expected ${expected.description} but got ${typeOf(value)}`
|
|
806
|
+
return error(msg, {
|
|
796
807
|
path,
|
|
797
808
|
expected: expected.description,
|
|
798
809
|
actual: typeOf(value),
|
|
810
|
+
reason,
|
|
799
811
|
})
|
|
800
812
|
}
|
|
801
813
|
|
|
@@ -828,7 +840,9 @@ export function checkType(
|
|
|
828
840
|
}
|
|
829
841
|
|
|
830
842
|
/** Type specifier - either a string name or a RuntimeType */
|
|
831
|
-
type TypeSpec =
|
|
843
|
+
type TypeSpec =
|
|
844
|
+
| string
|
|
845
|
+
| { check: (v: unknown) => boolean | string; description: string }
|
|
832
846
|
|
|
833
847
|
/** Parameter metadata with optional location */
|
|
834
848
|
interface ParamMeta {
|
package/src/types/Type.test.ts
CHANGED
|
@@ -207,8 +207,8 @@ describe('Built-in Types', () => {
|
|
|
207
207
|
it('TString validates strings', () => {
|
|
208
208
|
expect(TString.check('hello')).toBe(true)
|
|
209
209
|
expect(TString.check('')).toBe(true)
|
|
210
|
-
expect(TString.check(123)).toBe(
|
|
211
|
-
expect(TString.check(null)).toBe(
|
|
210
|
+
expect(TString.check(123)).toBe('expected string, got number')
|
|
211
|
+
expect(TString.check(null)).toBe('expected string, got null')
|
|
212
212
|
})
|
|
213
213
|
|
|
214
214
|
it('TNumber validates numbers', () => {
|
|
@@ -216,60 +216,64 @@ describe('Built-in Types', () => {
|
|
|
216
216
|
expect(TNumber.check(0)).toBe(true)
|
|
217
217
|
expect(TNumber.check(-1.5)).toBe(true)
|
|
218
218
|
expect(TNumber.check(NaN)).toBe(true) // NaN is typeof number
|
|
219
|
-
expect(TNumber.check('123')).toBe(
|
|
219
|
+
expect(TNumber.check('123')).toBe('expected number, got string')
|
|
220
220
|
})
|
|
221
221
|
|
|
222
222
|
it('TBoolean validates booleans', () => {
|
|
223
223
|
expect(TBoolean.check(true)).toBe(true)
|
|
224
224
|
expect(TBoolean.check(false)).toBe(true)
|
|
225
|
-
expect(TBoolean.check(1)).toBe(
|
|
226
|
-
expect(TBoolean.check('true')).toBe(
|
|
225
|
+
expect(TBoolean.check(1)).toBe('expected boolean, got number')
|
|
226
|
+
expect(TBoolean.check('true')).toBe('expected boolean, got string')
|
|
227
227
|
})
|
|
228
228
|
|
|
229
229
|
it('TInteger validates integers', () => {
|
|
230
230
|
expect(TInteger.check(1)).toBe(true)
|
|
231
231
|
expect(TInteger.check(0)).toBe(true)
|
|
232
232
|
expect(TInteger.check(-5)).toBe(true)
|
|
233
|
-
expect(TInteger.check(1.5)).toBe(
|
|
234
|
-
expect(TInteger.check('1')).toBe(
|
|
233
|
+
expect(TInteger.check(1.5)).toBe('1.5 is not an integer')
|
|
234
|
+
expect(TInteger.check('1')).toBe('expected integer, got string')
|
|
235
235
|
})
|
|
236
236
|
|
|
237
237
|
it('TPositiveInt validates positive integers', () => {
|
|
238
238
|
expect(TPositiveInt.check(1)).toBe(true)
|
|
239
239
|
expect(TPositiveInt.check(100)).toBe(true)
|
|
240
|
-
expect(TPositiveInt.check(0)).toBe(
|
|
241
|
-
expect(TPositiveInt.check(-1)).toBe(
|
|
242
|
-
expect(TPositiveInt.check(1.5)).toBe(
|
|
240
|
+
expect(TPositiveInt.check(0)).toBe('0 is not positive')
|
|
241
|
+
expect(TPositiveInt.check(-1)).toBe('-1 is not positive')
|
|
242
|
+
expect(TPositiveInt.check(1.5)).toBe('1.5 is not an integer')
|
|
243
243
|
})
|
|
244
244
|
|
|
245
245
|
it('TNonEmptyString validates non-empty strings', () => {
|
|
246
246
|
expect(TNonEmptyString.check('hello')).toBe(true)
|
|
247
247
|
expect(TNonEmptyString.check('a')).toBe(true)
|
|
248
|
-
expect(TNonEmptyString.check('')).toBe(
|
|
249
|
-
expect(TNonEmptyString.check(123)).toBe(
|
|
248
|
+
expect(TNonEmptyString.check('')).toBe('string is empty')
|
|
249
|
+
expect(TNonEmptyString.check(123)).toBe('expected string, got number')
|
|
250
250
|
})
|
|
251
251
|
|
|
252
252
|
it('TEmail validates email addresses', () => {
|
|
253
253
|
expect(TEmail.check('user@example.com')).toBe(true)
|
|
254
254
|
expect(TEmail.check('a@b.c')).toBe(true)
|
|
255
|
-
expect(TEmail.check('invalid')).toBe(
|
|
256
|
-
expect(TEmail.check('no@domain')).toBe(
|
|
257
|
-
expect(TEmail.check('@example.com')).toBe(
|
|
255
|
+
expect(TEmail.check('invalid')).toBe('"invalid" is not a valid email')
|
|
256
|
+
expect(TEmail.check('no@domain')).toBe('"no@domain" is not a valid email')
|
|
257
|
+
expect(TEmail.check('@example.com')).toBe(
|
|
258
|
+
'"@example.com" is not a valid email'
|
|
259
|
+
)
|
|
258
260
|
})
|
|
259
261
|
|
|
260
262
|
it('TUrl validates URLs', () => {
|
|
261
263
|
expect(TUrl.check('https://example.com')).toBe(true)
|
|
262
264
|
expect(TUrl.check('http://localhost:3000')).toBe(true)
|
|
263
265
|
expect(TUrl.check('ftp://files.example.com')).toBe(true)
|
|
264
|
-
expect(TUrl.check('not-a-url')).toBe(
|
|
265
|
-
expect(TUrl.check('example.com')).toBe(
|
|
266
|
+
expect(TUrl.check('not-a-url')).toBe('"not-a-url" is not a valid URL')
|
|
267
|
+
expect(TUrl.check('example.com')).toBe('"example.com" is not a valid URL')
|
|
266
268
|
})
|
|
267
269
|
|
|
268
270
|
it('TUuid validates UUIDs', () => {
|
|
269
271
|
expect(TUuid.check('550e8400-e29b-41d4-a716-446655440000')).toBe(true)
|
|
270
272
|
expect(TUuid.check('550E8400-E29B-41D4-A716-446655440000')).toBe(true)
|
|
271
|
-
expect(TUuid.check('not-a-uuid')).toBe(
|
|
272
|
-
expect(TUuid.check('550e8400-e29b-41d4-a716')).toBe(
|
|
273
|
+
expect(TUuid.check('not-a-uuid')).toBe('"not-a-uuid" is not a valid UUID')
|
|
274
|
+
expect(TUuid.check('550e8400-e29b-41d4-a716')).toBe(
|
|
275
|
+
'"550e8400-e29b-41d4-a716" is not a valid UUID'
|
|
276
|
+
)
|
|
273
277
|
})
|
|
274
278
|
})
|
|
275
279
|
|
|
@@ -676,3 +680,48 @@ describe('Enum', () => {
|
|
|
676
680
|
expect(HttpStatus.members.NotFound).toBe(404)
|
|
677
681
|
})
|
|
678
682
|
})
|
|
683
|
+
|
|
684
|
+
describe('predicate reason strings', () => {
|
|
685
|
+
it('Type() passes through reason strings from predicates', () => {
|
|
686
|
+
const EvenNumber = Type<number>('even number', (v: unknown) => {
|
|
687
|
+
if (typeof v !== 'number') return 'not a number'
|
|
688
|
+
if (v % 2 !== 0) return `${v} is odd`
|
|
689
|
+
return true
|
|
690
|
+
})
|
|
691
|
+
|
|
692
|
+
expect(EvenNumber.check(4)).toBe(true)
|
|
693
|
+
expect(EvenNumber.check(3)).toBe('3 is odd')
|
|
694
|
+
expect(EvenNumber.check('x')).toBe('not a number')
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it('schema-based types still return boolean', () => {
|
|
698
|
+
const StringType = Type('a string', s.string)
|
|
699
|
+
expect(StringType.check('hello')).toBe(true)
|
|
700
|
+
expect(StringType.check(42)).toBe(false)
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
it('Nullable correctly handles reason-returning predicates', () => {
|
|
704
|
+
const EvenNumber = Type<number>('even number', (v: unknown) => {
|
|
705
|
+
if (typeof v !== 'number') return 'not a number'
|
|
706
|
+
if (v % 2 !== 0) return `${v} is odd`
|
|
707
|
+
return true
|
|
708
|
+
})
|
|
709
|
+
const NullableEven = Nullable(EvenNumber)
|
|
710
|
+
|
|
711
|
+
expect(NullableEven.check(null)).toBe(true)
|
|
712
|
+
expect(NullableEven.check(4)).toBe(true)
|
|
713
|
+
expect(NullableEven.check(3)).toBe(false) // reason lost in combinator, but correctly fails
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('TArray correctly handles reason-returning predicates', () => {
|
|
717
|
+
const Positive = Type<number>('positive', (v: unknown) => {
|
|
718
|
+
if (typeof v !== 'number') return 'not a number'
|
|
719
|
+
if (v <= 0) return `${v} is not positive`
|
|
720
|
+
return true
|
|
721
|
+
})
|
|
722
|
+
const PositiveArray = TArray(Positive)
|
|
723
|
+
|
|
724
|
+
expect(PositiveArray.check([1, 2, 3])).toBe(true)
|
|
725
|
+
expect(PositiveArray.check([1, -1, 3])).toBe(false)
|
|
726
|
+
})
|
|
727
|
+
})
|