-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathschema-utils.ts
More file actions
120 lines (101 loc) · 3.17 KB
/
schema-utils.ts
File metadata and controls
120 lines (101 loc) · 3.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
type JSONSchema = {
type?: string | string[];
anyOf?: JSONSchema[];
oneOf?: JSONSchema[];
allOf?: JSONSchema[];
[key: string]: any;
};
interface CoerceOptions {
strict?: boolean;
fieldName?: string;
}
const COMPOSITE_KEYS: Array<'anyOf' | 'oneOf' | 'allOf'> = ['anyOf', 'oneOf', 'allOf'];
const normalizeType = (schemaType?: string | string[]) => {
if (!schemaType) return undefined;
if (typeof schemaType === 'string') {
return schemaType === 'null' ? undefined : schemaType;
}
if (Array.isArray(schemaType)) {
return schemaType.find((type) => type !== 'null');
}
return undefined;
};
export const resolveEffectiveSchema = (schema?: JSONSchema): JSONSchema | undefined => {
if (!schema) return undefined;
const normalizedType = normalizeType(schema.type);
if (normalizedType) {
return { ...schema, type: normalizedType };
}
for (const key of COMPOSITE_KEYS) {
const candidates = schema[key];
if (!Array.isArray(candidates)) continue;
for (const candidate of candidates) {
const resolved = resolveEffectiveSchema(candidate);
if (resolved?.type && resolved.type !== 'null') {
return resolved;
}
}
if (candidates.length > 0) {
return candidates[0];
}
}
return schema;
};
const formatErrorMessage = (fieldName: string | undefined, message: string) =>
fieldName ? `${fieldName} ${message}` : message;
export const coerceValueForSchema = (
rawValue: string,
schema?: JSONSchema,
options?: CoerceOptions,
) => {
if (rawValue === undefined || rawValue === null) return rawValue;
if (typeof rawValue !== 'string') return rawValue;
const trimmed = rawValue.trim();
if (!trimmed) return rawValue;
if (trimmed === 'null') return null;
const resolvedSchema = resolveEffectiveSchema(schema);
const targetType = resolvedSchema?.type;
if (targetType === 'object' || targetType === 'array') {
try {
return JSON.parse(trimmed);
} catch {
if (options?.strict) {
throw new Error(
formatErrorMessage(
options.fieldName,
`must be valid JSON ${targetType === 'array' ? 'array' : 'object'}`,
),
);
}
console.warn('Failed to parse JSON body field value. Sending raw string instead.');
return rawValue;
}
}
if (targetType === 'integer' || targetType === 'number') {
const parsed = Number(trimmed);
if (Number.isNaN(parsed)) {
if (options?.strict) {
throw new Error(
formatErrorMessage(
options.fieldName,
`must be a valid ${targetType === 'integer' ? 'integer' : 'number'}`,
),
);
}
return rawValue;
}
if (options?.strict && targetType === 'integer' && !Number.isInteger(parsed)) {
throw new Error(formatErrorMessage(options.fieldName, 'must be an integer'));
}
return parsed;
}
if (targetType === 'boolean') {
if (trimmed.toLowerCase() === 'true') return true;
if (trimmed.toLowerCase() === 'false') return false;
if (options?.strict) {
throw new Error(formatErrorMessage(options.fieldName, 'must be a boolean (true or false)'));
}
return rawValue;
}
return rawValue;
};