-
-
Notifications
You must be signed in to change notification settings - Fork 213
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add json string validation action
- Loading branch information
1 parent
65f0e59
commit 850c22f
Showing
5 changed files
with
289 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './json.ts'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import { describe, expectTypeOf, test } from 'vitest'; | ||
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; | ||
import { json, type JsonAction, type JsonIssue } from './json.ts'; | ||
|
||
describe('json', () => { | ||
describe('should return action object', () => { | ||
test('with undefined message', () => { | ||
type Action = JsonAction<string, undefined>; | ||
expectTypeOf(json<string>()).toEqualTypeOf<Action>(); | ||
expectTypeOf(json<string, undefined>(undefined)).toEqualTypeOf<Action>(); | ||
}); | ||
|
||
test('with string message', () => { | ||
expectTypeOf(json<string, 'message'>('message')).toEqualTypeOf< | ||
JsonAction<string, 'message'> | ||
>(); | ||
}); | ||
|
||
test('with function message', () => { | ||
expectTypeOf(json<string, () => string>(() => 'message')).toEqualTypeOf< | ||
JsonAction<string, () => string> | ||
>(); | ||
}); | ||
}); | ||
|
||
describe('should infer correct types', () => { | ||
type Action = JsonAction<string, undefined>; | ||
|
||
test('of input', () => { | ||
expectTypeOf<InferInput<Action>>().toEqualTypeOf<string>(); | ||
}); | ||
|
||
test('of output', () => { | ||
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<string>(); | ||
}); | ||
|
||
test('of issue', () => { | ||
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<JsonIssue<string>>(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { describe, expect, test } from 'vitest'; | ||
import { HEX_COLOR_REGEX } from '../../regex.ts'; | ||
import type { StringIssue } from '../../schemas/index.ts'; | ||
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts'; | ||
import { | ||
json, | ||
type JsonAction, | ||
type JsonIssue, | ||
} from './json.ts'; | ||
|
||
describe('json', () => { | ||
describe('should return action object', () => { | ||
const baseAction: Omit<JsonAction<string, never>, 'message'> = { | ||
kind: 'validation', | ||
type: 'json', | ||
reference: json, | ||
expects: null, | ||
requirement: expect.any(Function), | ||
async: false, | ||
'~run': expect.any(Function), | ||
}; | ||
|
||
test('with undefined message', () => { | ||
const action: JsonAction<string, undefined> = { | ||
...baseAction, | ||
message: undefined, | ||
}; | ||
expect(json()).toStrictEqual(action); | ||
expect(json(undefined)).toStrictEqual(action); | ||
}); | ||
|
||
test('with string message', () => { | ||
expect(json('message')).toStrictEqual({ | ||
...baseAction, | ||
message: 'message', | ||
} satisfies JsonAction<string, string>); | ||
}); | ||
|
||
test('with function message', () => { | ||
const message = () => 'message'; | ||
expect(json(message)).toStrictEqual({ | ||
...baseAction, | ||
message, | ||
} satisfies JsonAction<string, typeof message>); | ||
}); | ||
}); | ||
|
||
describe('should return dataset without issues', () => { | ||
const action = json(); | ||
|
||
test('for untyped inputs', () => { | ||
const issues: [StringIssue] = [ | ||
{ | ||
kind: 'schema', | ||
type: 'string', | ||
input: null, | ||
expected: 'string', | ||
received: 'null', | ||
message: 'message', | ||
}, | ||
]; | ||
expect( | ||
action['~run']({ typed: false, value: null, issues }, {}) | ||
).toStrictEqual({ | ||
typed: false, | ||
value: null, | ||
issues, | ||
}); | ||
}); | ||
|
||
test('for literals', () => { | ||
expectNoActionIssue(action, [ | ||
'{}', | ||
'[]', | ||
'null', | ||
'123', | ||
'"example"', | ||
'12.3', | ||
'"escaped \\"quote"', | ||
]); | ||
}); | ||
|
||
test('for complex values', () => { | ||
expectNoActionIssue(action, [ | ||
'{"name":"John Doe","age":30,"color":null,"children":["Alice","Bob"]}', | ||
'[123, "John Doe", null, {"fruit":"apple"}]', | ||
]); | ||
}); | ||
}); | ||
|
||
describe('should return dataset with issues', () => { | ||
const action = json('message'); | ||
const baseIssue: Omit<JsonIssue<string>, 'input' | 'received'> = { | ||
kind: 'validation', | ||
type: 'json', | ||
expected: null, | ||
message: 'message', | ||
requirement: expect.any(Function), | ||
}; | ||
|
||
test('for empty strings', () => { | ||
expectActionIssue(action, baseIssue, ['', ' ', '\n']); | ||
}); | ||
|
||
test('for malformed strings', () => { | ||
expectActionIssue(action, baseIssue, [ | ||
'{"key:"value"}', | ||
'{key":"value"}', | ||
"'key'", | ||
'"unescaped "quote""', | ||
'{]', | ||
'[}', | ||
]); | ||
}); | ||
|
||
test('for malformed arrays', () => { | ||
expectActionIssue(action, baseIssue, [ | ||
'[1, 2, , 3]', | ||
'[1, 2, "key":"value"]', | ||
]); | ||
}); | ||
|
||
test('for malformed objects', () => { | ||
expectActionIssue(action, baseIssue, [ | ||
'{"key":"value",,"key2": "value2"}', | ||
'{"key":"value","key2"}', | ||
'{{}}', | ||
]); | ||
}); | ||
|
||
test('for trailing commas', () => { | ||
expectActionIssue(action, baseIssue, [ | ||
'{"key":"value"},', | ||
'{"key":"value",}', | ||
'[1, 2, 3,]', | ||
]); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import type { | ||
BaseIssue, | ||
BaseValidation, | ||
ErrorMessage, | ||
} from '../../types/index.ts'; | ||
import { _addIssue } from '../../utils/index.ts'; | ||
|
||
/** | ||
* JSON issue type. | ||
*/ | ||
export interface JsonIssue<TInput extends string> extends BaseIssue<TInput> { | ||
/** | ||
* The issue kind. | ||
*/ | ||
readonly kind: 'validation'; | ||
/** | ||
* The issue type. | ||
*/ | ||
readonly type: 'json'; | ||
/** | ||
* The expected property. | ||
*/ | ||
readonly expected: null; | ||
/** | ||
* The received property. | ||
*/ | ||
readonly received: `"${string}"`; | ||
/** | ||
* The JSON validation requirement. | ||
*/ | ||
readonly requirement: (input: string) => boolean; | ||
} | ||
|
||
/** | ||
* JSON action type. | ||
*/ | ||
export interface JsonAction< | ||
TInput extends string, | ||
TMessage extends ErrorMessage<JsonIssue<TInput>> | undefined, | ||
> extends BaseValidation<TInput, TInput, JsonIssue<TInput>> { | ||
/** | ||
* The action type. | ||
*/ | ||
readonly type: 'json'; | ||
/** | ||
* The action reference. | ||
*/ | ||
readonly reference: typeof json; | ||
/** | ||
* The expected property. | ||
*/ | ||
readonly expects: null; | ||
/** | ||
* The JSON validation requirement. | ||
*/ | ||
readonly requirement: (input: string) => boolean; | ||
/** | ||
* The error message. | ||
*/ | ||
readonly message: TMessage; | ||
} | ||
|
||
/** | ||
* Creates a [JSON](https://en.wikipedia.org/wiki/JSON) validation action. | ||
* | ||
* @returns A JSON action. | ||
*/ | ||
export function json<TInput extends string>(): JsonAction<TInput, undefined>; | ||
|
||
/** | ||
* Creates a [JSON](https://en.wikipedia.org/wiki/JSON) validation action. | ||
* | ||
* @param message The error message. | ||
* | ||
* @returns A JSON action. | ||
*/ | ||
export function json< | ||
TInput extends string, | ||
const TMessage extends ErrorMessage<JsonIssue<TInput>> | undefined, | ||
>(message: TMessage): JsonAction<TInput, TMessage>; | ||
|
||
export function json( | ||
message?: ErrorMessage<JsonIssue<string>> | ||
): JsonAction<string, ErrorMessage<JsonIssue<string>> | undefined> { | ||
return { | ||
kind: 'validation', | ||
type: 'json', | ||
reference: json, | ||
async: false, | ||
expects: null, | ||
requirement(input) { | ||
try { | ||
JSON.parse(input); | ||
return true; | ||
} catch { | ||
return false; | ||
} | ||
}, | ||
message, | ||
'~run'(dataset, config) { | ||
if (dataset.typed && !this.requirement(dataset.value)) { | ||
_addIssue(this, 'JSON', dataset, config); | ||
} | ||
return dataset; | ||
}, | ||
}; | ||
} |