Skip to content

Commit 40e2bc5

Browse files
Merge pull request #998 from 43081j/actions-everywhere
feat: support more actions in JSON schema conversion
2 parents c28feda + f42b8c4 commit 40e2bc5

File tree

6 files changed

+241
-20
lines changed

6 files changed

+241
-20
lines changed

library/eslint.config.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default tseslint.config(
6161
},
6262
],
6363

64-
// Regexp
64+
// RegExp
6565
'regexp/no-super-linear-move': 'error', // Prevent DoS regexps
6666
'regexp/no-control-character': 'error', // Avoid unneeded regexps characters
6767
'regexp/no-octal': 'error', // Avoid unneeded regexps characters
@@ -91,6 +91,9 @@ export default tseslint.config(
9191
'jsdoc/require-param-type': 'off',
9292
'jsdoc/require-returns-type': 'off',
9393

94+
// RegExp
95+
'regexp/use-ignore-case': 'off', // We sometimes don't use the i flag for a better JSON Schema compatibility
96+
9497
// Security
9598
'security/detect-object-injection': 'off', // Too many false positives
9699
'security/detect-unsafe-regex': 'off', // Too many false positives, see https://github.com/eslint-community/eslint-plugin-security/issues/28 - we use the redos-detector plugin instead

library/src/actions/octal/octal.test.ts

-9
Original file line numberDiff line numberDiff line change
@@ -89,15 +89,6 @@ describe('octal', () => {
8989
'0o1234567',
9090
]);
9191
});
92-
93-
test('for 0O prefix', () => {
94-
expectNoActionIssue(action, [
95-
'0O000000000000000',
96-
'0O777777777777',
97-
'0O01234567',
98-
'0O1234567',
99-
]);
100-
});
10192
});
10293

10394
describe('should return dataset with issues', () => {

library/src/regex.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,18 @@ export const EMOJI_REGEX: RegExp =
4242

4343
/**
4444
* [Hexadecimal](https://en.wikipedia.org/wiki/Hexadecimal) regex.
45+
*
46+
* Hint: We decided against the `i` flag for better JSON Schema compatibility.
4547
*/
46-
export const HEXADECIMAL_REGEX: RegExp = /^(?:0[hx])?[\da-f]+$/iu;
48+
export const HEXADECIMAL_REGEX: RegExp = /^(?:0[hx])?[\da-fA-F]+$/u;
4749

4850
/**
4951
* [Hex color](https://en.wikipedia.org/wiki/Web_colors#Hex_triplet) regex.
52+
*
53+
* Hint: We decided against the `i` flag for better JSON Schema compatibility.
5054
*/
5155
export const HEX_COLOR_REGEX: RegExp =
52-
/^#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})$/iu;
56+
/^#(?:[\da-fA-F]{3,4}|[\da-fA-F]{6}|[\da-fA-F]{8})$/u;
5357

5458
/**
5559
* [IMEI](https://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity) regex.
@@ -135,7 +139,7 @@ export const NANO_ID_REGEX: RegExp = /^[\w-]+$/u;
135139
/**
136140
* [Octal](https://en.wikipedia.org/wiki/Octal) regex.
137141
*/
138-
export const OCTAL_REGEX: RegExp = /^(?:0o)?[0-7]+$/iu;
142+
export const OCTAL_REGEX: RegExp = /^(?:0o)?[0-7]+$/u;
139143

140144
/**
141145
* [RFC 5322 email address](https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1) regex.
@@ -147,8 +151,10 @@ export const RFC_EMAIL_REGEX: RegExp =
147151

148152
/**
149153
* [ULID](https://github.com/ulid/spec) regex.
154+
*
155+
* Hint: We decided against the `i` flag for better JSON Schema compatibility.
150156
*/
151-
export const ULID_REGEX: RegExp = /^[\da-hjkmnp-tv-z]{26}$/iu;
157+
export const ULID_REGEX: RegExp = /^[\da-hjkmnp-tv-zA-HJKMNP-TV-Z]{26}$/u;
152158

153159
/**
154160
* [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier) regex.

packages/to-json-schema/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to the library will be documented in this file.
66

77
- Add support for `undefinedable` schema
88
- Add support for `base64`, `isoTime`, `isoDateTime`, `nonEmpty` and `url` action (pull request #962)
9+
- Add support for `bic`, `cuid2`, `empty`, `decimal`, `digits`, `emoji`, `hex_color`, `hexadecimal`, `nanoid`, `octal` and `ulid` action (pull request #998)
910
- Change Valibot peer dependency to v1.0.0
1011
- Change extraction of default value from `nullable`, `nullish` and `optional` schema
1112
- Change `force` to `errorMode` in config for better control (issue #889)

packages/to-json-schema/src/convertAction.test.ts

+173
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,54 @@ describe('convertAction', () => {
1717
});
1818
});
1919

20+
test('should convert bic action', () => {
21+
expect(convertAction({}, v.bic<string>(), undefined)).toStrictEqual({
22+
pattern: v.BIC_REGEX.source,
23+
});
24+
expect(
25+
convertAction({ type: 'string' }, v.bic<string>(), undefined)
26+
).toStrictEqual({
27+
type: 'string',
28+
pattern: v.BIC_REGEX.source,
29+
});
30+
});
31+
32+
test('should convert cuid2 action', () => {
33+
expect(convertAction({}, v.cuid2<string>(), undefined)).toStrictEqual({
34+
pattern: v.CUID2_REGEX.source,
35+
});
36+
expect(
37+
convertAction({ type: 'string' }, v.cuid2<string>(), undefined)
38+
).toStrictEqual({
39+
type: 'string',
40+
pattern: v.CUID2_REGEX.source,
41+
});
42+
});
43+
44+
test('should convert decimal action', () => {
45+
expect(convertAction({}, v.decimal<string>(), undefined)).toStrictEqual({
46+
pattern: v.DECIMAL_REGEX.source,
47+
});
48+
expect(
49+
convertAction({ type: 'string' }, v.decimal<string>(), undefined)
50+
).toStrictEqual({
51+
type: 'string',
52+
pattern: v.DECIMAL_REGEX.source,
53+
});
54+
});
55+
56+
test('should convert digits action', () => {
57+
expect(convertAction({}, v.digits<string>(), undefined)).toStrictEqual({
58+
pattern: v.DIGITS_REGEX.source,
59+
});
60+
expect(
61+
convertAction({ type: 'string' }, v.digits<string>(), undefined)
62+
).toStrictEqual({
63+
type: 'string',
64+
pattern: v.DIGITS_REGEX.source,
65+
});
66+
});
67+
2068
test('should convert description action', () => {
2169
expect(convertAction({}, v.description('test'), undefined)).toStrictEqual({
2270
description: 'test',
@@ -35,6 +83,95 @@ describe('convertAction', () => {
3583
});
3684
});
3785

86+
test('should convert emoji action', () => {
87+
expect(convertAction({}, v.emoji<string>(), undefined)).toStrictEqual({
88+
pattern: v.EMOJI_REGEX.source,
89+
});
90+
expect(
91+
convertAction({ type: 'string' }, v.emoji<string>(), undefined)
92+
).toStrictEqual({
93+
type: 'string',
94+
pattern: v.EMOJI_REGEX.source,
95+
});
96+
});
97+
98+
test('should convert empty action for strings', () => {
99+
expect(
100+
convertAction({ type: 'string' }, v.empty(), undefined)
101+
).toStrictEqual({
102+
type: 'string',
103+
maxLength: 0,
104+
});
105+
});
106+
107+
test('should convert empty action for arrays', () => {
108+
expect(
109+
convertAction({ type: 'array' }, v.empty(), undefined)
110+
).toStrictEqual({
111+
type: 'array',
112+
maxItems: 0,
113+
});
114+
});
115+
116+
test('should throw error for empty action with invalid type', () => {
117+
const action = v.empty();
118+
const error1 = 'The "empty" action is not supported on type "undefined".';
119+
expect(() => convertAction({}, action, undefined)).toThrowError(error1);
120+
expect(() =>
121+
convertAction({}, action, { errorMode: 'throw' })
122+
).toThrowError(error1);
123+
const error2 = 'The "empty" action is not supported on type "object".';
124+
expect(() =>
125+
convertAction({ type: 'object' }, action, undefined)
126+
).toThrowError(error2);
127+
expect(() =>
128+
convertAction({ type: 'object' }, action, { errorMode: 'throw' })
129+
).toThrowError(error2);
130+
});
131+
132+
test('should warn error for empty action with invalid type', () => {
133+
expect(convertAction({}, v.empty(), { errorMode: 'warn' })).toStrictEqual({
134+
maxLength: 0,
135+
});
136+
expect(console.warn).toHaveBeenLastCalledWith(
137+
'The "empty" action is not supported on type "undefined".'
138+
);
139+
expect(
140+
convertAction({ type: 'object' }, v.empty(), {
141+
errorMode: 'warn',
142+
})
143+
).toStrictEqual({ type: 'object', maxLength: 0 });
144+
expect(console.warn).toHaveBeenLastCalledWith(
145+
'The "empty" action is not supported on type "object".'
146+
);
147+
});
148+
149+
test('should convert hexadecimal action', () => {
150+
expect(convertAction({}, v.hexadecimal<string>(), undefined)).toStrictEqual(
151+
{
152+
pattern: v.HEXADECIMAL_REGEX.source,
153+
}
154+
);
155+
expect(
156+
convertAction({ type: 'string' }, v.hexadecimal<string>(), undefined)
157+
).toStrictEqual({
158+
type: 'string',
159+
pattern: v.HEXADECIMAL_REGEX.source,
160+
});
161+
});
162+
163+
test('should convert hex color action', () => {
164+
expect(convertAction({}, v.hexColor<string>(), undefined)).toStrictEqual({
165+
pattern: v.HEX_COLOR_REGEX.source,
166+
});
167+
expect(
168+
convertAction({ type: 'string' }, v.hexColor<string>(), undefined)
169+
).toStrictEqual({
170+
type: 'string',
171+
pattern: v.HEX_COLOR_REGEX.source,
172+
});
173+
});
174+
38175
test('should convert integer action', () => {
39176
expect(convertAction({}, v.integer<number>(), undefined)).toStrictEqual({
40177
type: 'integer',
@@ -418,6 +555,18 @@ describe('convertAction', () => {
418555
});
419556
});
420557

558+
test('should convert Nano ID action', () => {
559+
expect(convertAction({}, v.nanoid<string>(), undefined)).toStrictEqual({
560+
pattern: v.NANO_ID_REGEX.source,
561+
});
562+
expect(
563+
convertAction({ type: 'string' }, v.nanoid<string>(), undefined)
564+
).toStrictEqual({
565+
type: 'string',
566+
pattern: v.NANO_ID_REGEX.source,
567+
});
568+
});
569+
421570
test('should convert non empty action for strings', () => {
422571
expect(
423572
convertAction({ type: 'string' }, v.nonEmpty(), undefined)
@@ -472,6 +621,18 @@ describe('convertAction', () => {
472621
);
473622
});
474623

624+
test('should convert octal action', () => {
625+
expect(convertAction({}, v.octal<string>(), undefined)).toStrictEqual({
626+
pattern: v.OCTAL_REGEX.source,
627+
});
628+
expect(
629+
convertAction({ type: 'string' }, v.octal<string>(), undefined)
630+
).toStrictEqual({
631+
type: 'string',
632+
pattern: v.OCTAL_REGEX.source,
633+
});
634+
});
635+
475636
test('should convert supported regex action', () => {
476637
expect(
477638
convertAction({ type: 'string' }, v.regex<string>(/[a-zA-Z]/), undefined)
@@ -510,6 +671,18 @@ describe('convertAction', () => {
510671
});
511672
});
512673

674+
test('should convert ULID action', () => {
675+
expect(convertAction({}, v.ulid<string>(), undefined)).toStrictEqual({
676+
pattern: v.ULID_REGEX.source,
677+
});
678+
expect(
679+
convertAction({ type: 'string' }, v.ulid<string>(), undefined)
680+
).toStrictEqual({
681+
type: 'string',
682+
pattern: v.ULID_REGEX.source,
683+
});
684+
});
685+
513686
test('should convert url action', () => {
514687
expect(convertAction({}, v.url<string>(), undefined)).toStrictEqual({
515688
format: 'uri',

packages/to-json-schema/src/convertAction.ts

+53-6
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,30 @@ import type * as v from 'valibot';
33
import type { ConversionConfig } from './type.ts';
44
import { handleError } from './utils/index.ts';
55

6-
// TODO: Add support for more actions (for example all regex-based actions)
7-
86
/**
97
* Action type.
108
*/
119
type Action =
1210
| v.Base64Action<string, v.ErrorMessage<v.Base64Issue<string>> | undefined>
11+
| v.BicAction<string, v.ErrorMessage<v.BicIssue<string>> | undefined>
12+
| v.Cuid2Action<string, v.ErrorMessage<v.Cuid2Issue<string>> | undefined>
13+
| v.DecimalAction<string, v.ErrorMessage<v.DecimalIssue<string>> | undefined>
1314
| v.DescriptionAction<unknown, string>
15+
| v.DigitsAction<string, v.ErrorMessage<v.DigitsIssue<string>> | undefined>
1416
| v.EmailAction<string, v.ErrorMessage<v.EmailIssue<string>> | undefined>
17+
| v.EmojiAction<string, v.ErrorMessage<v.EmojiIssue<string>> | undefined>
18+
| v.EmptyAction<
19+
v.LengthInput,
20+
v.ErrorMessage<v.EmptyIssue<v.LengthInput>> | undefined
21+
>
22+
| v.HexadecimalAction<
23+
string,
24+
v.ErrorMessage<v.HexadecimalIssue<string>> | undefined
25+
>
26+
| v.HexColorAction<
27+
string,
28+
v.ErrorMessage<v.HexColorIssue<string>> | undefined
29+
>
1530
| v.IntegerAction<number, v.ErrorMessage<v.IntegerIssue<number>> | undefined>
1631
| v.Ipv4Action<string, v.ErrorMessage<v.Ipv4Issue<string>> | undefined>
1732
| v.Ipv6Action<string, v.ErrorMessage<v.Ipv6Issue<string>> | undefined>
@@ -30,10 +45,6 @@ type Action =
3045
number,
3146
v.ErrorMessage<v.LengthIssue<v.LengthInput, number>> | undefined
3247
>
33-
| v.NonEmptyAction<
34-
v.LengthInput,
35-
v.ErrorMessage<v.NonEmptyIssue<v.LengthInput>> | undefined
36-
>
3748
| v.MaxLengthAction<
3849
v.LengthInput,
3950
number,
@@ -59,8 +70,15 @@ type Action =
5970
number,
6071
v.ErrorMessage<v.MultipleOfIssue<number, number>> | undefined
6172
>
73+
| v.NanoIDAction<string, v.ErrorMessage<v.NanoIDIssue<string>> | undefined>
74+
| v.NonEmptyAction<
75+
v.LengthInput,
76+
v.ErrorMessage<v.NonEmptyIssue<v.LengthInput>> | undefined
77+
>
78+
| v.OctalAction<string, v.ErrorMessage<v.OctalIssue<string>> | undefined>
6279
| v.RegexAction<string, v.ErrorMessage<v.RegexIssue<string>> | undefined>
6380
| v.TitleAction<unknown, string>
81+
| v.UlidAction<string, v.ErrorMessage<v.UlidIssue<string>> | undefined>
6482
| v.UrlAction<string, v.ErrorMessage<v.UrlIssue<string>> | undefined>
6583
| v.UuidAction<string, v.ErrorMessage<v.UuidIssue<string>> | undefined>
6684
| v.ValueAction<
@@ -89,6 +107,20 @@ export function convertAction(
89107
break;
90108
}
91109

110+
case 'bic':
111+
case 'cuid2':
112+
case 'decimal':
113+
case 'digits':
114+
case 'emoji':
115+
case 'hexadecimal':
116+
case 'hex_color':
117+
case 'nanoid':
118+
case 'octal':
119+
case 'ulid': {
120+
jsonSchema.pattern = valibotAction.requirement.source;
121+
break;
122+
}
123+
92124
case 'description': {
93125
jsonSchema.description = valibotAction.description;
94126
break;
@@ -99,6 +131,21 @@ export function convertAction(
99131
break;
100132
}
101133

134+
case 'empty': {
135+
if (jsonSchema.type === 'array') {
136+
jsonSchema.maxItems = 0;
137+
} else {
138+
if (jsonSchema.type !== 'string') {
139+
handleError(
140+
`The "${valibotAction.type}" action is not supported on type "${jsonSchema.type}".`,
141+
config
142+
);
143+
}
144+
jsonSchema.maxLength = 0;
145+
}
146+
break;
147+
}
148+
102149
case 'integer': {
103150
jsonSchema.type = 'integer';
104151
break;

0 commit comments

Comments
 (0)