@lowerdeck/validation 1.0.0

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.
Files changed (250) hide show
  1. package/.turbo/turbo-build.log +11 -0
  2. package/.turbo/turbo-test.log +144 -0
  3. package/README.md +158 -0
  4. package/dist/index.cjs +2 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.ts +119 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.modern.js +2 -0
  9. package/dist/index.modern.js.map +1 -0
  10. package/dist/index.module.js +2 -0
  11. package/dist/index.module.js.map +1 -0
  12. package/dist/index.umd.js +2 -0
  13. package/dist/index.umd.js.map +1 -0
  14. package/dist/lib/introspect.d.ts +13 -0
  15. package/dist/lib/introspect.d.ts.map +1 -0
  16. package/dist/lib/result.d.ts +4 -0
  17. package/dist/lib/result.d.ts.map +1 -0
  18. package/dist/lib/result.test.d.ts +2 -0
  19. package/dist/lib/result.test.d.ts.map +1 -0
  20. package/dist/lib/types.d.ts +48 -0
  21. package/dist/lib/types.d.ts.map +1 -0
  22. package/dist/lib/validator.d.ts +3 -0
  23. package/dist/lib/validator.d.ts.map +1 -0
  24. package/dist/lib/validator.test.d.ts +2 -0
  25. package/dist/lib/validator.test.d.ts.map +1 -0
  26. package/dist/modifiers/color.d.ts +5 -0
  27. package/dist/modifiers/color.d.ts.map +1 -0
  28. package/dist/modifiers/color.test.d.ts +2 -0
  29. package/dist/modifiers/color.test.d.ts.map +1 -0
  30. package/dist/modifiers/email.d.ts +5 -0
  31. package/dist/modifiers/email.d.ts.map +1 -0
  32. package/dist/modifiers/email.test.d.ts +2 -0
  33. package/dist/modifiers/email.test.d.ts.map +1 -0
  34. package/dist/modifiers/emoji.d.ts +5 -0
  35. package/dist/modifiers/emoji.d.ts.map +1 -0
  36. package/dist/modifiers/emoji.test.d.ts +2 -0
  37. package/dist/modifiers/emoji.test.d.ts.map +1 -0
  38. package/dist/modifiers/endsWith.d.ts +5 -0
  39. package/dist/modifiers/endsWith.d.ts.map +1 -0
  40. package/dist/modifiers/endsWith.test.d.ts +2 -0
  41. package/dist/modifiers/endsWith.test.d.ts.map +1 -0
  42. package/dist/modifiers/equals.d.ts +8 -0
  43. package/dist/modifiers/equals.d.ts.map +1 -0
  44. package/dist/modifiers/equals.test.d.ts +2 -0
  45. package/dist/modifiers/equals.test.d.ts.map +1 -0
  46. package/dist/modifiers/includes.d.ts +8 -0
  47. package/dist/modifiers/includes.d.ts.map +1 -0
  48. package/dist/modifiers/includes.test.d.ts +2 -0
  49. package/dist/modifiers/includes.test.d.ts.map +1 -0
  50. package/dist/modifiers/index.d.ts +18 -0
  51. package/dist/modifiers/index.d.ts.map +1 -0
  52. package/dist/modifiers/integer.d.ts +5 -0
  53. package/dist/modifiers/integer.d.ts.map +1 -0
  54. package/dist/modifiers/integer.test.d.ts +2 -0
  55. package/dist/modifiers/integer.test.d.ts.map +1 -0
  56. package/dist/modifiers/ip.d.ts +11 -0
  57. package/dist/modifiers/ip.d.ts.map +1 -0
  58. package/dist/modifiers/ip.test.d.ts +2 -0
  59. package/dist/modifiers/ip.test.d.ts.map +1 -0
  60. package/dist/modifiers/isoDate.d.ts +11 -0
  61. package/dist/modifiers/isoDate.d.ts.map +1 -0
  62. package/dist/modifiers/isoDate.test.d.ts +2 -0
  63. package/dist/modifiers/isoDate.test.d.ts.map +1 -0
  64. package/dist/modifiers/length.d.ts +9 -0
  65. package/dist/modifiers/length.d.ts.map +1 -0
  66. package/dist/modifiers/length.test.d.ts +2 -0
  67. package/dist/modifiers/length.test.d.ts.map +1 -0
  68. package/dist/modifiers/multipleOf.d.ts +5 -0
  69. package/dist/modifiers/multipleOf.d.ts.map +1 -0
  70. package/dist/modifiers/multipleOf.test.d.ts +2 -0
  71. package/dist/modifiers/multipleOf.test.d.ts.map +1 -0
  72. package/dist/modifiers/oneOf.d.ts +5 -0
  73. package/dist/modifiers/oneOf.d.ts.map +1 -0
  74. package/dist/modifiers/oneOf.test.d.ts +2 -0
  75. package/dist/modifiers/oneOf.test.d.ts.map +1 -0
  76. package/dist/modifiers/positive.d.ts +8 -0
  77. package/dist/modifiers/positive.d.ts.map +1 -0
  78. package/dist/modifiers/positive.test.d.ts +2 -0
  79. package/dist/modifiers/positive.test.d.ts.map +1 -0
  80. package/dist/modifiers/regex.d.ts +5 -0
  81. package/dist/modifiers/regex.d.ts.map +1 -0
  82. package/dist/modifiers/regex.test.d.ts +2 -0
  83. package/dist/modifiers/regex.test.d.ts.map +1 -0
  84. package/dist/modifiers/size.d.ts +8 -0
  85. package/dist/modifiers/size.d.ts.map +1 -0
  86. package/dist/modifiers/size.test.d.ts +2 -0
  87. package/dist/modifiers/size.test.d.ts.map +1 -0
  88. package/dist/modifiers/startsWith.d.ts +5 -0
  89. package/dist/modifiers/startsWith.d.ts.map +1 -0
  90. package/dist/modifiers/startsWith.test.d.ts +2 -0
  91. package/dist/modifiers/startsWith.test.d.ts.map +1 -0
  92. package/dist/modifiers/url.d.ts +6 -0
  93. package/dist/modifiers/url.d.ts.map +1 -0
  94. package/dist/modifiers/url.test.d.ts +2 -0
  95. package/dist/modifiers/url.test.d.ts.map +1 -0
  96. package/dist/transformers/case.d.ts +4 -0
  97. package/dist/transformers/case.d.ts.map +1 -0
  98. package/dist/transformers/case.test.d.ts +2 -0
  99. package/dist/transformers/case.test.d.ts.map +1 -0
  100. package/dist/transformers/index.d.ts +3 -0
  101. package/dist/transformers/index.d.ts.map +1 -0
  102. package/dist/transformers/trim.d.ts +5 -0
  103. package/dist/transformers/trim.d.ts.map +1 -0
  104. package/dist/transformers/trim.test.d.ts +2 -0
  105. package/dist/transformers/trim.test.d.ts.map +1 -0
  106. package/dist/validators/any.d.ts +4 -0
  107. package/dist/validators/any.d.ts.map +1 -0
  108. package/dist/validators/any.test.d.ts +2 -0
  109. package/dist/validators/any.test.d.ts.map +1 -0
  110. package/dist/validators/array.d.ts +3 -0
  111. package/dist/validators/array.d.ts.map +1 -0
  112. package/dist/validators/array.test.d.ts +2 -0
  113. package/dist/validators/array.test.d.ts.map +1 -0
  114. package/dist/validators/boolean.d.ts +3 -0
  115. package/dist/validators/boolean.d.ts.map +1 -0
  116. package/dist/validators/boolean.test.d.ts +2 -0
  117. package/dist/validators/boolean.test.d.ts.map +1 -0
  118. package/dist/validators/date.d.ts +3 -0
  119. package/dist/validators/date.d.ts.map +1 -0
  120. package/dist/validators/date.test.d.ts +2 -0
  121. package/dist/validators/date.test.d.ts.map +1 -0
  122. package/dist/validators/enum.d.ts +6 -0
  123. package/dist/validators/enum.d.ts.map +1 -0
  124. package/dist/validators/enum.test.d.ts +2 -0
  125. package/dist/validators/enum.test.d.ts.map +1 -0
  126. package/dist/validators/index.d.ts +17 -0
  127. package/dist/validators/index.d.ts.map +1 -0
  128. package/dist/validators/intersection.d.ts +3 -0
  129. package/dist/validators/intersection.d.ts.map +1 -0
  130. package/dist/validators/intersection.test.d.ts +2 -0
  131. package/dist/validators/intersection.test.d.ts.map +1 -0
  132. package/dist/validators/literal.d.ts +3 -0
  133. package/dist/validators/literal.d.ts.map +1 -0
  134. package/dist/validators/literal.test.d.ts +2 -0
  135. package/dist/validators/literal.test.d.ts.map +1 -0
  136. package/dist/validators/null.d.ts +3 -0
  137. package/dist/validators/null.d.ts.map +1 -0
  138. package/dist/validators/null.test.d.ts +2 -0
  139. package/dist/validators/null.test.d.ts.map +1 -0
  140. package/dist/validators/nullable.d.ts +3 -0
  141. package/dist/validators/nullable.d.ts.map +1 -0
  142. package/dist/validators/nullable.test.d.ts +2 -0
  143. package/dist/validators/nullable.test.d.ts.map +1 -0
  144. package/dist/validators/number.d.ts +3 -0
  145. package/dist/validators/number.d.ts.map +1 -0
  146. package/dist/validators/number.test.d.ts +2 -0
  147. package/dist/validators/number.test.d.ts.map +1 -0
  148. package/dist/validators/object.d.ts +12 -0
  149. package/dist/validators/object.d.ts.map +1 -0
  150. package/dist/validators/object.test.d.ts +2 -0
  151. package/dist/validators/object.test.d.ts.map +1 -0
  152. package/dist/validators/optional.d.ts +3 -0
  153. package/dist/validators/optional.d.ts.map +1 -0
  154. package/dist/validators/optional.test.d.ts +2 -0
  155. package/dist/validators/optional.test.d.ts.map +1 -0
  156. package/dist/validators/record.d.ts +3 -0
  157. package/dist/validators/record.d.ts.map +1 -0
  158. package/dist/validators/record.test.d.ts +2 -0
  159. package/dist/validators/record.test.d.ts.map +1 -0
  160. package/dist/validators/string.d.ts +3 -0
  161. package/dist/validators/string.d.ts.map +1 -0
  162. package/dist/validators/string.test.d.ts +2 -0
  163. package/dist/validators/string.test.d.ts.map +1 -0
  164. package/dist/validators/union.d.ts +3 -0
  165. package/dist/validators/union.d.ts.map +1 -0
  166. package/dist/validators/union.test.d.ts +2 -0
  167. package/dist/validators/union.test.d.ts.map +1 -0
  168. package/dist/validators/void.d.ts +3 -0
  169. package/dist/validators/void.d.ts.map +1 -0
  170. package/package.json +31 -0
  171. package/src/index.ts +33 -0
  172. package/src/lib/introspect.ts +33 -0
  173. package/src/lib/result.test.ts +80 -0
  174. package/src/lib/result.ts +11 -0
  175. package/src/lib/types.ts +57 -0
  176. package/src/lib/validator.test.ts +82 -0
  177. package/src/lib/validator.ts +63 -0
  178. package/src/modifiers/color.test.ts +29 -0
  179. package/src/modifiers/color.ts +18 -0
  180. package/src/modifiers/email.test.ts +29 -0
  181. package/src/modifiers/email.ts +18 -0
  182. package/src/modifiers/emoji.test.ts +31 -0
  183. package/src/modifiers/emoji.ts +18 -0
  184. package/src/modifiers/endsWith.test.ts +50 -0
  185. package/src/modifiers/endsWith.ts +18 -0
  186. package/src/modifiers/equals.test.ts +70 -0
  187. package/src/modifiers/equals.ts +41 -0
  188. package/src/modifiers/includes.test.ts +64 -0
  189. package/src/modifiers/includes.ts +35 -0
  190. package/src/modifiers/index.ts +17 -0
  191. package/src/modifiers/integer.test.ts +45 -0
  192. package/src/modifiers/integer.ts +18 -0
  193. package/src/modifiers/ip.test.ts +34 -0
  194. package/src/modifiers/ip.ts +50 -0
  195. package/src/modifiers/isoDate.test.ts +83 -0
  196. package/src/modifiers/isoDate.ts +51 -0
  197. package/src/modifiers/length.test.ts +107 -0
  198. package/src/modifiers/length.ts +54 -0
  199. package/src/modifiers/multipleOf.test.ts +51 -0
  200. package/src/modifiers/multipleOf.ts +18 -0
  201. package/src/modifiers/oneOf.test.ts +27 -0
  202. package/src/modifiers/oneOf.ts +16 -0
  203. package/src/modifiers/positive.test.ts +64 -0
  204. package/src/modifiers/positive.ts +35 -0
  205. package/src/modifiers/regex.test.ts +40 -0
  206. package/src/modifiers/regex.ts +17 -0
  207. package/src/modifiers/size.test.ts +84 -0
  208. package/src/modifiers/size.ts +37 -0
  209. package/src/modifiers/startsWith.test.ts +40 -0
  210. package/src/modifiers/startsWith.ts +18 -0
  211. package/src/modifiers/url.test.ts +48 -0
  212. package/src/modifiers/url.ts +30 -0
  213. package/src/transformers/case.test.ts +36 -0
  214. package/src/transformers/case.ts +5 -0
  215. package/src/transformers/index.ts +2 -0
  216. package/src/transformers/trim.test.ts +32 -0
  217. package/src/transformers/trim.ts +7 -0
  218. package/src/validators/any.test.ts +11 -0
  219. package/src/validators/any.ts +11 -0
  220. package/src/validators/array.test.ts +46 -0
  221. package/src/validators/array.ts +43 -0
  222. package/src/validators/boolean.test.ts +39 -0
  223. package/src/validators/boolean.ts +34 -0
  224. package/src/validators/date.test.ts +39 -0
  225. package/src/validators/date.ts +26 -0
  226. package/src/validators/enum.test.ts +25 -0
  227. package/src/validators/enum.ts +56 -0
  228. package/src/validators/index.ts +16 -0
  229. package/src/validators/intersection.test.ts +35 -0
  230. package/src/validators/intersection.ts +24 -0
  231. package/src/validators/literal.test.ts +42 -0
  232. package/src/validators/literal.ts +26 -0
  233. package/src/validators/null.test.ts +39 -0
  234. package/src/validators/null.ts +18 -0
  235. package/src/validators/nullable.test.ts +34 -0
  236. package/src/validators/nullable.ts +16 -0
  237. package/src/validators/number.test.ts +39 -0
  238. package/src/validators/number.ts +23 -0
  239. package/src/validators/object.test.ts +66 -0
  240. package/src/validators/object.ts +66 -0
  241. package/src/validators/optional.test.ts +33 -0
  242. package/src/validators/optional.ts +16 -0
  243. package/src/validators/record.test.ts +56 -0
  244. package/src/validators/record.ts +56 -0
  245. package/src/validators/string.test.ts +39 -0
  246. package/src/validators/string.ts +30 -0
  247. package/src/validators/union.test.ts +73 -0
  248. package/src/validators/union.ts +38 -0
  249. package/src/validators/void.ts +6 -0
  250. package/tsconfig.json +9 -0
@@ -0,0 +1,16 @@
1
+ import { ValidationType, ValidationTypeValue } from '../lib/types';
2
+
3
+ export let nullable = <A extends ValidationType<any>>(
4
+ validator: A
5
+ ): ValidationType<ValidationTypeValue<A> | null> => ({
6
+ ...validator,
7
+
8
+ nullable: true,
9
+ validate: value => {
10
+ if (value === null) {
11
+ return { success: true, value };
12
+ }
13
+
14
+ return validator.validate(value);
15
+ }
16
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { number } from './number';
3
+
4
+ describe('number', () => {
5
+ test('should return success for a valid number', () => {
6
+ let result = number({}).validate(42);
7
+ expect(result).toEqual({ success: true, value: 42 });
8
+ });
9
+
10
+ test('should return an error for an invalid number', () => {
11
+ let result = number({}).validate('not a number');
12
+ expect(result).toEqual({
13
+ success: false,
14
+ errors: [
15
+ {
16
+ code: 'invalid_type',
17
+ message: 'Invalid input, expected number, received string',
18
+ received: 'string',
19
+ expected: 'number'
20
+ }
21
+ ]
22
+ });
23
+ });
24
+
25
+ test('should use the provided error message', () => {
26
+ let result = number({ message: 'Custom error message' }).validate('not a number');
27
+ expect(result).toEqual({
28
+ success: false,
29
+ errors: [
30
+ {
31
+ code: 'invalid_type',
32
+ message: 'Custom error message',
33
+ received: 'string',
34
+ expected: 'number'
35
+ }
36
+ ]
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,23 @@
1
+ import { error } from '../lib/result';
2
+ import { ValidatorOptions } from '../lib/types';
3
+ import { createValidator } from '../lib/validator';
4
+
5
+ export let number = createValidator<number, ValidatorOptions<number>>(
6
+ 'number',
7
+ (opts, value) => {
8
+ let number = typeof value == 'string' ? parseFloat(value) : value;
9
+
10
+ if (typeof number != 'number' || isNaN(number)) {
11
+ return error([
12
+ {
13
+ code: 'invalid_type',
14
+ message: opts.message ?? `Invalid input, expected number, received ${typeof value}`,
15
+ received: typeof value,
16
+ expected: 'number'
17
+ }
18
+ ]);
19
+ }
20
+
21
+ return { success: true, value: number };
22
+ }
23
+ );
@@ -0,0 +1,66 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { number } from './number';
3
+ import { object } from './object';
4
+ import { string } from './string';
5
+
6
+ describe('object', () => {
7
+ test('should validate an object with the correct shape', () => {
8
+ let validator = object({
9
+ name: string(),
10
+ age: number()
11
+ });
12
+
13
+ let result: any = validator.validate({
14
+ name: 'John Doe',
15
+ age: 42
16
+ });
17
+
18
+ expect(result.success).toBe(true);
19
+ expect(result.value).toEqual({
20
+ name: 'John Doe',
21
+ age: 42
22
+ });
23
+ });
24
+
25
+ test('should return an error for an object with an invalid shape', () => {
26
+ let validator = object({
27
+ name: string(),
28
+ age: number()
29
+ });
30
+
31
+ let result: any = validator.validate({
32
+ name: 'John Doe',
33
+ age: 'Test'
34
+ });
35
+
36
+ expect(result.success).toBe(false);
37
+ expect(result.errors).toEqual([
38
+ {
39
+ code: 'invalid_type',
40
+ message: 'Invalid input, expected number, received string',
41
+ received: 'string',
42
+ expected: 'number',
43
+ path: ['age']
44
+ }
45
+ ]);
46
+ });
47
+
48
+ test('should return an error for a non-object input', () => {
49
+ let validator = object({
50
+ name: string(),
51
+ age: number()
52
+ });
53
+
54
+ let result: any = validator.validate('not an object');
55
+
56
+ expect(result.success).toBe(false);
57
+ expect(result.errors).toEqual([
58
+ {
59
+ code: 'invalid_type',
60
+ message: 'Invalid input, expected object, received string',
61
+ received: 'string',
62
+ expected: 'object'
63
+ }
64
+ ]);
65
+ });
66
+ });
@@ -0,0 +1,66 @@
1
+ import { error, success } from '../lib/result';
2
+ import { ValidationType, ValidationTypeValue, ValidatorOptions } from '../lib/types';
3
+
4
+ type KeysWhichExtend<T, SelectedType> = {
5
+ [key in keyof T]: SelectedType extends T[key] ? key : never;
6
+ }[keyof T];
7
+ type Optional<T> = Partial<Pick<T, KeysWhichExtend<T, undefined>>>;
8
+ type Required<T> = Omit<T, KeysWhichExtend<T, undefined>>;
9
+ export type UndefinedIsOptional<T> = Optional<T> & Required<T>;
10
+
11
+ export let object = <Validator extends { [key: string]: ValidationType<any> }>(
12
+ shape: Validator,
13
+ opts?: ValidatorOptions<Record<string, ValidationTypeValue<Validator>>>
14
+ ): ValidationType<
15
+ UndefinedIsOptional<{
16
+ [key in keyof Validator]: ValidationTypeValue<Validator[key]>;
17
+ }>
18
+ > => ({
19
+ type: 'object',
20
+ name: opts?.name,
21
+ description: opts?.description,
22
+ properties: shape,
23
+ examples: [
24
+ Object.fromEntries(
25
+ Object.entries(shape)
26
+ .map(([key, validator]) => {
27
+ let example = validator.examples?.[0];
28
+ // For nullable fields, if there's no example, don't include it
29
+ // This prevents undefined from becoming {} in Object.fromEntries
30
+ if (example === undefined && validator.nullable) {
31
+ return null; // Will be filtered out
32
+ }
33
+ return [key, example];
34
+ })
35
+ .filter((entry): entry is [string, any] => entry !== null)
36
+ )
37
+ ] as any,
38
+ validate: value => {
39
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
40
+ return error([
41
+ {
42
+ code: 'invalid_type',
43
+ message: `Invalid input, expected object, received ${typeof value}`,
44
+ received: Array.isArray(value) ? 'array' : typeof value,
45
+ expected: 'object'
46
+ }
47
+ ]);
48
+ }
49
+
50
+ let values: {
51
+ [key in keyof Validator]: ValidationTypeValue<Validator[key]>;
52
+ } = {} as any;
53
+
54
+ for (let [key, validator] of Object.entries(shape)) {
55
+ let result = validator.validate(value[key]);
56
+
57
+ if (!result.success) {
58
+ return error(result.errors.map(e => ({ ...e, path: [key, ...(e.path ?? [])] })));
59
+ }
60
+
61
+ values[key as keyof Validator] = result.value;
62
+ }
63
+
64
+ return success(values);
65
+ }
66
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { optional } from './optional';
3
+ import { string } from './string';
4
+
5
+ describe('optional', () => {
6
+ test('should return a new validator with optional values', () => {
7
+ let validator = optional(string());
8
+
9
+ expect(validator.type).toBe('string');
10
+ expect(validator.optional).toBe(true);
11
+
12
+ expect(validator.validate(undefined)).toEqual({ success: true, value: undefined });
13
+ expect(validator.validate('hello')).toEqual({ success: true, value: 'hello' });
14
+ });
15
+
16
+ test('should pass through the validation result of the original validator', () => {
17
+ let validator = optional(string());
18
+
19
+ expect(validator.validate(undefined)).toEqual({ success: true, value: undefined });
20
+ expect(validator.validate('hello')).toEqual({ success: true, value: 'hello' });
21
+ expect(validator.validate(123)).toEqual({
22
+ success: false,
23
+ errors: [
24
+ {
25
+ code: 'invalid_type',
26
+ message: 'Invalid input, expected string, received number',
27
+ received: 'number',
28
+ expected: 'string'
29
+ }
30
+ ]
31
+ });
32
+ });
33
+ });
@@ -0,0 +1,16 @@
1
+ import { ValidationType, ValidationTypeValue } from '../lib/types';
2
+
3
+ export let optional = <A extends ValidationType<any>>(
4
+ validator: A
5
+ ): ValidationType<ValidationTypeValue<A> | undefined> => ({
6
+ ...validator,
7
+
8
+ optional: true,
9
+ validate: value => {
10
+ if (value === undefined) {
11
+ return { success: true, value };
12
+ }
13
+
14
+ return validator.validate(value);
15
+ }
16
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { number } from './number';
3
+ import { record } from './record';
4
+ import { string } from './string';
5
+
6
+ describe('record', () => {
7
+ test('should validate an empty object', () => {
8
+ let validator = record(string());
9
+ let result: any = validator.validate({});
10
+ expect(result.success).toBe(true);
11
+ expect(result.value).toEqual({});
12
+ });
13
+
14
+ test('should validate an object with string keys and string values', () => {
15
+ let validator = record(string());
16
+ let result: any = validator.validate({ foo: 'bar', baz: 'qux' });
17
+ expect(result.success).toBe(true);
18
+ expect(result.value).toEqual({ foo: 'bar', baz: 'qux' });
19
+ });
20
+
21
+ test('should validate an object with string keys and number values', () => {
22
+ let validator = record(number());
23
+ let result: any = validator.validate({ foo: 1, bar: 2 });
24
+ expect(result.success).toBe(true);
25
+ expect(result.value).toEqual({ foo: 1, bar: 2 });
26
+ });
27
+
28
+ test('should fail to validate a non-object', () => {
29
+ let validator = record(string());
30
+ let result: any = validator.validate('not an object');
31
+ expect(result.success).toBe(false);
32
+ expect(result.errors).toEqual([
33
+ {
34
+ code: 'invalid_type',
35
+ message: 'Invalid input, expected object',
36
+ received: 'not an object',
37
+ expected: 'object'
38
+ }
39
+ ]);
40
+ });
41
+
42
+ test('should fail to validate an object with invalid values', () => {
43
+ let validator = record(string());
44
+ let result: any = validator.validate({ foo: 'bar', baz: 123 });
45
+ expect(result.success).toBe(false);
46
+ expect(result.errors).toEqual([
47
+ {
48
+ code: 'invalid_type',
49
+ message: 'Invalid input, expected string, received number',
50
+ received: 'number',
51
+ expected: 'string',
52
+ path: ['baz']
53
+ }
54
+ ]);
55
+ });
56
+ });
@@ -0,0 +1,56 @@
1
+ import { error, success } from '../lib/result';
2
+ import { ValidationType, ValidationTypeValue, ValidatorOptions } from '../lib/types';
3
+
4
+ export let record = <Validator extends ValidationType<any>>(
5
+ validator: Validator,
6
+ opts?: ValidatorOptions<Record<string, ValidationTypeValue<Validator>>>
7
+ ): ValidationType<Record<string, ValidationTypeValue<Validator>>> => ({
8
+ type: 'record',
9
+ items: validator,
10
+ name: opts?.name,
11
+ description: opts?.description,
12
+ examples: Array.isArray(validator.examples)
13
+ ? [
14
+ {
15
+ key1: validator.examples[0]
16
+ }
17
+ ]
18
+ : undefined,
19
+ validate: value => {
20
+ if (typeof value != 'object' || value === null || Array.isArray(value)) {
21
+ return error([
22
+ {
23
+ code: 'invalid_type',
24
+ message: `Invalid input, expected object`,
25
+ received: value,
26
+ expected: 'object'
27
+ }
28
+ ]);
29
+ }
30
+
31
+ let values: Record<string, ValidationTypeValue<Validator>> = {};
32
+
33
+ for (let [key, val] of Object.entries(value)) {
34
+ if (typeof key != 'string') {
35
+ return error([
36
+ {
37
+ code: 'invalid_type',
38
+ message: `Invalid input, expected object with string keys, received ${typeof key}`,
39
+ received: key,
40
+ expected: 'string'
41
+ }
42
+ ]);
43
+ }
44
+
45
+ let result = validator.validate(val);
46
+
47
+ if (!result.success) {
48
+ return error(result.errors.map(e => ({ ...e, path: [key, ...(e.path ?? [])] })));
49
+ }
50
+
51
+ values[key] = result.value;
52
+ }
53
+
54
+ return success(values);
55
+ }
56
+ });
@@ -0,0 +1,39 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { string } from './string';
3
+
4
+ describe('string', () => {
5
+ test('should return success when given a string', () => {
6
+ let result = string({}).validate('hello');
7
+ expect(result).toEqual({ success: true, value: 'hello' });
8
+ });
9
+
10
+ test('should return an error when given a non-string value', () => {
11
+ let result = string({}).validate(123);
12
+ expect(result).toEqual({
13
+ success: false,
14
+ errors: [
15
+ {
16
+ code: 'invalid_type',
17
+ message: 'Invalid input, expected string, received number',
18
+ received: 'number',
19
+ expected: 'string'
20
+ }
21
+ ]
22
+ });
23
+ });
24
+
25
+ test('should use the provided error message', () => {
26
+ let result = string({ message: 'Custom error message' }).validate(123);
27
+ expect(result).toEqual({
28
+ success: false,
29
+ errors: [
30
+ {
31
+ code: 'invalid_type',
32
+ message: 'Custom error message',
33
+ received: 'number',
34
+ expected: 'string'
35
+ }
36
+ ]
37
+ });
38
+ });
39
+ });
@@ -0,0 +1,30 @@
1
+ import { error } from '../lib/result';
2
+ import { ValidatorOptions } from '../lib/types';
3
+ import { createValidator } from '../lib/validator';
4
+
5
+ export let string = createValidator<string, ValidatorOptions<string>>(
6
+ 'string',
7
+ (opts, value) => {
8
+ if (typeof value != 'string') {
9
+ return error([
10
+ {
11
+ code: 'invalid_type',
12
+ message: opts.message ?? `Invalid input, expected string, received ${typeof value}`,
13
+ received: typeof value,
14
+ expected: 'string'
15
+ }
16
+ ]);
17
+ }
18
+
19
+ if (value.length > 100_000) {
20
+ return error([
21
+ {
22
+ code: 'max_length',
23
+ message: 'Input is too long'
24
+ }
25
+ ]);
26
+ }
27
+
28
+ return { success: true, value };
29
+ }
30
+ );
@@ -0,0 +1,73 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { error, success } from '../lib/result';
3
+ import { array, number, object, string } from './index';
4
+ import { union } from './union';
5
+
6
+ describe('union', () => {
7
+ test('should validate a string', () => {
8
+ let validator = union([string(), number()]);
9
+
10
+ expect(validator.validate('hello')).toEqual(success('hello'));
11
+ });
12
+
13
+ test('should validate a number', () => {
14
+ let validator = union([string(), number()]);
15
+
16
+ expect(validator.validate(42)).toEqual(success(42));
17
+ });
18
+
19
+ test('should not validate an object', () => {
20
+ let validator = union([string(), number()]);
21
+
22
+ expect(validator.validate({})).toEqual(
23
+ error([
24
+ {
25
+ code: 'invalid_type',
26
+ message: 'Invalid input, expected string, received object',
27
+ expected: 'string',
28
+ received: 'object'
29
+ },
30
+ {
31
+ code: 'invalid_type',
32
+ expected: 'number',
33
+ message: 'Invalid input, expected number, received object',
34
+ received: 'object'
35
+ }
36
+ ])
37
+ );
38
+ });
39
+
40
+ test('should validate an array of strings', () => {
41
+ let validator = union([string(), array(string())]);
42
+
43
+ expect(validator.validate(['hello', 'world'])).toEqual(success(['hello', 'world']));
44
+ });
45
+
46
+ test('should validate an array of numbers', () => {
47
+ let validator = union([number(), array(number())]);
48
+
49
+ expect(validator.validate([1, 2, 3])).toEqual(success([1, 2, 3]));
50
+ });
51
+
52
+ test('should not validate an array of objects', () => {
53
+ let validator = union([string(), array(object({}))]);
54
+
55
+ expect(validator.validate(['hello', {}])).toEqual(
56
+ error([
57
+ {
58
+ code: 'invalid_type',
59
+ expected: 'string',
60
+ message: 'Invalid input, expected string, received object',
61
+ received: 'object'
62
+ },
63
+ {
64
+ code: 'invalid_type',
65
+ expected: 'object',
66
+ message: 'Invalid input, expected object, received string',
67
+ path: ['0'],
68
+ received: 'string'
69
+ }
70
+ ])
71
+ );
72
+ });
73
+ });
@@ -0,0 +1,38 @@
1
+ import { error } from '../lib/result';
2
+ import {
3
+ ValidationError,
4
+ ValidationType,
5
+ ValidationTypeValue,
6
+ ValidatorOptions
7
+ } from '../lib/types';
8
+
9
+ export let union = <
10
+ A extends ValidationType<any>,
11
+ B extends ValidationType<any>,
12
+ Rest extends ValidationType<any>[]
13
+ >(
14
+ validators: [A, B, ...Rest],
15
+ opts?: ValidatorOptions<
16
+ ValidationTypeValue<A> | ValidationTypeValue<B> | ValidationTypeValue<Rest[number]>
17
+ >
18
+ ): ValidationType<
19
+ ValidationTypeValue<A> | ValidationTypeValue<B> | ValidationTypeValue<Rest[number]>
20
+ > => ({
21
+ type: 'union',
22
+ name: opts?.name,
23
+ items: validators,
24
+ description: opts?.description,
25
+ examples: validators.flatMap(v => v.examples || []),
26
+ validate: value => {
27
+ let errors: ValidationError[] = [];
28
+
29
+ for (let validator of validators) {
30
+ let result = validator.validate(value);
31
+ if (result.success) return result;
32
+
33
+ errors.push(...result.errors);
34
+ }
35
+
36
+ return error(errors);
37
+ }
38
+ });
@@ -0,0 +1,6 @@
1
+ import { ValidatorOptions } from '../lib/types';
2
+ import { createValidator } from '../lib/validator';
3
+
4
+ export let voidType = createValidator<void, ValidatorOptions<void>>('void', (opts, value) => {
5
+ return { success: true, value: undefined };
6
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/tsconfig",
3
+ "extends": "@lowerdeck/tsconfig/base.json",
4
+ "exclude": ["dist"],
5
+ "include": ["src"],
6
+ "compilerOptions": {
7
+ "outDir": "dist",
8
+ }
9
+ }