@flow-like/widget-sdk 0.1.1
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/README.md +258 -0
- package/package.json +42 -0
- package/src/contract.ts +121 -0
- package/src/define.ts +91 -0
- package/src/geometry.ts +277 -0
- package/src/global.d.ts +10 -0
- package/src/index.ts +44 -0
- package/src/llm-schemas.ts +1385 -0
- package/src/llm.ts +283 -0
- package/src/media.ts +107 -0
- package/src/microphone.ts +133 -0
- package/src/mount.ts +427 -0
- package/src/protocol.ts +178 -0
- package/src/query.ts +58 -0
- package/src/react.ts +34 -0
- package/src/standalone.ts +54 -0
- package/src/theme.ts +74 -0
- package/src/validate.ts +336 -0
- package/src/vanilla.ts +1 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ThemeMode, ThemeState } from "./protocol";
|
|
2
|
+
import { DEFAULT_DARK_TOKENS, DEFAULT_LIGHT_TOKENS } from "./theme";
|
|
3
|
+
|
|
4
|
+
export function themeForMode(mode: ThemeMode): ThemeState {
|
|
5
|
+
return {
|
|
6
|
+
mode,
|
|
7
|
+
tokens: mode === "dark" ? DEFAULT_DARK_TOKENS : DEFAULT_LIGHT_TOKENS,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function detectColorScheme(): ThemeMode {
|
|
12
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
13
|
+
return "light";
|
|
14
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
15
|
+
? "dark"
|
|
16
|
+
: "light";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function watchColorScheme(
|
|
20
|
+
onChange: (mode: ThemeMode) => void,
|
|
21
|
+
): () => void {
|
|
22
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function")
|
|
23
|
+
return () => {};
|
|
24
|
+
const query = window.matchMedia("(prefers-color-scheme: dark)");
|
|
25
|
+
const listener = (event: MediaQueryListEvent) => {
|
|
26
|
+
onChange(event.matches ? "dark" : "light");
|
|
27
|
+
};
|
|
28
|
+
query.addEventListener("change", listener);
|
|
29
|
+
return () => query.removeEventListener("change", listener);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function renderStandaloneBadge(): () => void {
|
|
33
|
+
if (typeof document === "undefined" || !document.body) return () => {};
|
|
34
|
+
const badge = document.createElement("div");
|
|
35
|
+
badge.textContent = "standalone";
|
|
36
|
+
badge.setAttribute(
|
|
37
|
+
"style",
|
|
38
|
+
[
|
|
39
|
+
"position: fixed",
|
|
40
|
+
"bottom: 8px",
|
|
41
|
+
"right: 8px",
|
|
42
|
+
"z-index: 2147483647",
|
|
43
|
+
"padding: 2px 8px",
|
|
44
|
+
"border-radius: 9999px",
|
|
45
|
+
"font: 11px/1.6 ui-monospace, monospace",
|
|
46
|
+
"color: rgba(255, 255, 255, 0.85)",
|
|
47
|
+
"background: rgba(0, 0, 0, 0.55)",
|
|
48
|
+
"pointer-events: none",
|
|
49
|
+
"user-select: none",
|
|
50
|
+
].join("; "),
|
|
51
|
+
);
|
|
52
|
+
document.body.appendChild(badge);
|
|
53
|
+
return () => badge.remove();
|
|
54
|
+
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { ThemeState } from "./protocol";
|
|
2
|
+
|
|
3
|
+
// Stock Flow-Like token set, copied from packages/ui/global.css so standalone
|
|
4
|
+
// widgets render correctly themed with zero setup.
|
|
5
|
+
export const DEFAULT_LIGHT_TOKENS: Record<string, string> = {
|
|
6
|
+
"--background": "oklch(0.9841 0.0035 39.4848)",
|
|
7
|
+
"--foreground": "oklch(0.184 0.0081 285.5768)",
|
|
8
|
+
"--card": "oklch(1.0 0 0)",
|
|
9
|
+
"--card-foreground": "oklch(0.184 0.0081 285.5768)",
|
|
10
|
+
"--popover": "oklch(1.0 0 0)",
|
|
11
|
+
"--popover-foreground": "oklch(0.184 0.0081 285.5768)",
|
|
12
|
+
"--primary": "oklch(0.6724 0.208 34.7886)",
|
|
13
|
+
"--primary-foreground": "oklch(1.0 0 0)",
|
|
14
|
+
"--secondary": "oklch(0.9466 0.0084 56.3108)",
|
|
15
|
+
"--secondary-foreground": "oklch(0.2503 0.0113 285.5586)",
|
|
16
|
+
"--muted": "oklch(0.9401 0.0068 53.4438)",
|
|
17
|
+
"--muted-foreground": "oklch(0.5137 0.0156 285.8367)",
|
|
18
|
+
"--accent": "oklch(0.6488 0.2155 31.664)",
|
|
19
|
+
"--accent-foreground": "oklch(1.0 0 0)",
|
|
20
|
+
"--destructive": "oklch(0.6227 0.2289 23.472)",
|
|
21
|
+
"--destructive-foreground": "oklch(1.0 0 0)",
|
|
22
|
+
"--border": "oklch(0.8933 0.0111 54.4825)",
|
|
23
|
+
"--input": "oklch(0.8933 0.0111 54.4825)",
|
|
24
|
+
"--ring": "oklch(0.6724 0.208 34.7886)",
|
|
25
|
+
"--chart-1": "oklch(0.6724 0.208 34.7886)",
|
|
26
|
+
"--chart-2": "oklch(0.7069 0.1871 45.3923)",
|
|
27
|
+
"--chart-3": "oklch(0.6227 0.2289 23.472)",
|
|
28
|
+
"--chart-4": "oklch(0.7814 0.1487 63.8806)",
|
|
29
|
+
"--chart-5": "oklch(0.5975 0.0196 285.7923)",
|
|
30
|
+
"--font-sans": '"Inter", ui-sans-serif, system-ui, sans-serif',
|
|
31
|
+
"--font-serif": '"Playfair Display", "Didot", Georgia, serif',
|
|
32
|
+
"--font-mono": '"JetBrains Mono", ui-monospace, monospace',
|
|
33
|
+
"--radius": "0.375rem",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const DEFAULT_DARK_TOKENS: Record<string, string> = {
|
|
37
|
+
"--background": "oklch(0.164 0.0111 268.0057)",
|
|
38
|
+
"--foreground": "oklch(0.9544 0.0059 59.6503)",
|
|
39
|
+
"--card": "oklch(0.1993 0.0111 260.661)",
|
|
40
|
+
"--card-foreground": "oklch(0.9544 0.0059 59.6503)",
|
|
41
|
+
"--popover": "oklch(0.1993 0.0111 260.661)",
|
|
42
|
+
"--popover-foreground": "oklch(0.9544 0.0059 59.6503)",
|
|
43
|
+
"--primary": "oklch(0.6724 0.208 34.7886)",
|
|
44
|
+
"--primary-foreground": "oklch(1.0 0 0)",
|
|
45
|
+
"--secondary": "oklch(0.2515 0.0122 264.3278)",
|
|
46
|
+
"--secondary-foreground": "oklch(0.9247 0.0077 61.447)",
|
|
47
|
+
"--muted": "oklch(0.264 0.012 264.345)",
|
|
48
|
+
"--muted-foreground": "oklch(0.6984 0.0157 264.467)",
|
|
49
|
+
"--accent": "oklch(0.6488 0.2155 31.664)",
|
|
50
|
+
"--accent-foreground": "oklch(1.0 0 0)",
|
|
51
|
+
"--destructive": "oklch(0.6227 0.2289 23.472)",
|
|
52
|
+
"--destructive-foreground": "oklch(1.0 0 0)",
|
|
53
|
+
"--border": "oklch(0.2921 0.014 261.7008)",
|
|
54
|
+
"--input": "oklch(0.2921 0.014 261.7008)",
|
|
55
|
+
"--ring": "oklch(0.6724 0.208 34.7886)",
|
|
56
|
+
"--chart-1": "oklch(0.6724 0.208 34.7886)",
|
|
57
|
+
"--chart-2": "oklch(0.7069 0.1871 45.3923)",
|
|
58
|
+
"--chart-3": "oklch(0.6227 0.2289 23.472)",
|
|
59
|
+
"--chart-4": "oklch(0.7814 0.1487 63.8806)",
|
|
60
|
+
"--chart-5": "oklch(0.6984 0.0157 264.467)",
|
|
61
|
+
"--font-sans": "Open Sans, sans-serif",
|
|
62
|
+
"--font-serif": "Georgia, serif",
|
|
63
|
+
"--font-mono": "Menlo, monospace",
|
|
64
|
+
"--radius": "0.375rem",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function applyTheme(theme: ThemeState): void {
|
|
68
|
+
if (typeof document === "undefined") return;
|
|
69
|
+
const root = document.documentElement;
|
|
70
|
+
for (const [name, value] of Object.entries(theme.tokens)) {
|
|
71
|
+
if (name.startsWith("--")) root.style.setProperty(name, value);
|
|
72
|
+
}
|
|
73
|
+
root.classList.toggle("dark", theme.mode === "dark");
|
|
74
|
+
}
|
package/src/validate.ts
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { ContractInput, JsonSchema } from "./contract";
|
|
2
|
+
import { checkGeometrySchema } from "./geometry";
|
|
3
|
+
import { markedLlmSchema } from "./llm";
|
|
4
|
+
|
|
5
|
+
export interface ValidationResult {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
errors: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const VALID: ValidationResult = { valid: true, errors: [] };
|
|
11
|
+
|
|
12
|
+
function deepEqual(a: unknown, b: unknown): boolean {
|
|
13
|
+
if (a === b) return true;
|
|
14
|
+
if (typeof a !== typeof b) return false;
|
|
15
|
+
if (typeof a !== "object" || a === null || b === null) return false;
|
|
16
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
17
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
18
|
+
if (a.length !== b.length) return false;
|
|
19
|
+
return a.every((item, index) => deepEqual(item, b[index]));
|
|
20
|
+
}
|
|
21
|
+
const aRecord = a as Record<string, unknown>;
|
|
22
|
+
const bRecord = b as Record<string, unknown>;
|
|
23
|
+
const aKeys = Object.keys(aRecord);
|
|
24
|
+
const bKeys = Object.keys(bRecord);
|
|
25
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
26
|
+
return aKeys.every((key) => deepEqual(aRecord[key], bRecord[key]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function matchesType(type: string, value: unknown): boolean {
|
|
30
|
+
switch (type) {
|
|
31
|
+
case "string":
|
|
32
|
+
return typeof value === "string";
|
|
33
|
+
case "number":
|
|
34
|
+
return typeof value === "number";
|
|
35
|
+
case "integer":
|
|
36
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
37
|
+
case "boolean":
|
|
38
|
+
return typeof value === "boolean";
|
|
39
|
+
case "null":
|
|
40
|
+
return value === null;
|
|
41
|
+
case "array":
|
|
42
|
+
return Array.isArray(value);
|
|
43
|
+
case "object":
|
|
44
|
+
return (
|
|
45
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
46
|
+
);
|
|
47
|
+
default:
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function describe(value: unknown): string {
|
|
53
|
+
if (value === null) return "null";
|
|
54
|
+
if (Array.isArray(value)) return "array";
|
|
55
|
+
return typeof value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkNumericBounds(
|
|
59
|
+
schema: JsonSchema,
|
|
60
|
+
value: number,
|
|
61
|
+
path: string,
|
|
62
|
+
errors: string[],
|
|
63
|
+
): void {
|
|
64
|
+
const { minimum, maximum, exclusiveMinimum, exclusiveMaximum } = schema;
|
|
65
|
+
if (typeof minimum === "number" && value < minimum) {
|
|
66
|
+
errors.push(`${path}: ${value} is less than minimum ${minimum}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof maximum === "number" && value > maximum) {
|
|
69
|
+
errors.push(`${path}: ${value} is greater than maximum ${maximum}`);
|
|
70
|
+
}
|
|
71
|
+
if (typeof exclusiveMinimum === "number" && value <= exclusiveMinimum) {
|
|
72
|
+
errors.push(
|
|
73
|
+
`${path}: ${value} is not greater than exclusiveMinimum ${exclusiveMinimum}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (typeof exclusiveMaximum === "number" && value >= exclusiveMaximum) {
|
|
77
|
+
errors.push(
|
|
78
|
+
`${path}: ${value} is not less than exclusiveMaximum ${exclusiveMaximum}`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function checkString(
|
|
84
|
+
schema: JsonSchema,
|
|
85
|
+
value: string,
|
|
86
|
+
path: string,
|
|
87
|
+
errors: string[],
|
|
88
|
+
): void {
|
|
89
|
+
const { minLength, maxLength, pattern } = schema;
|
|
90
|
+
if (typeof minLength === "number" && value.length < minLength) {
|
|
91
|
+
errors.push(`${path}: string shorter than minLength ${minLength}`);
|
|
92
|
+
}
|
|
93
|
+
if (typeof maxLength === "number" && value.length > maxLength) {
|
|
94
|
+
errors.push(`${path}: string longer than maxLength ${maxLength}`);
|
|
95
|
+
}
|
|
96
|
+
if (typeof pattern === "string") {
|
|
97
|
+
let regex: RegExp | null = null;
|
|
98
|
+
try {
|
|
99
|
+
regex = new RegExp(pattern);
|
|
100
|
+
} catch {
|
|
101
|
+
// Invalid pattern in the schema — cannot enforce, treat as passing.
|
|
102
|
+
}
|
|
103
|
+
if (regex && !regex.test(value)) {
|
|
104
|
+
errors.push(`${path}: string does not match pattern ${pattern}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function checkArray(
|
|
110
|
+
schema: JsonSchema,
|
|
111
|
+
value: unknown[],
|
|
112
|
+
path: string,
|
|
113
|
+
errors: string[],
|
|
114
|
+
): void {
|
|
115
|
+
const { minItems, maxItems, items } = schema;
|
|
116
|
+
if (typeof minItems === "number" && value.length < minItems) {
|
|
117
|
+
errors.push(`${path}: array has fewer than minItems ${minItems}`);
|
|
118
|
+
}
|
|
119
|
+
if (typeof maxItems === "number" && value.length > maxItems) {
|
|
120
|
+
errors.push(`${path}: array has more than maxItems ${maxItems}`);
|
|
121
|
+
}
|
|
122
|
+
if (Array.isArray(items)) {
|
|
123
|
+
items.forEach((itemSchema, index) => {
|
|
124
|
+
if (index < value.length && isSchemaObject(itemSchema)) {
|
|
125
|
+
validateAt(itemSchema, value[index], `${path}[${index}]`, errors);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
} else if (isSchemaObject(items)) {
|
|
129
|
+
value.forEach((item, index) => {
|
|
130
|
+
validateAt(items, item, `${path}[${index}]`, errors);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function checkObject(
|
|
136
|
+
schema: JsonSchema,
|
|
137
|
+
value: Record<string, unknown>,
|
|
138
|
+
path: string,
|
|
139
|
+
errors: string[],
|
|
140
|
+
): void {
|
|
141
|
+
const { properties, required, additionalProperties } = schema;
|
|
142
|
+
const props = isSchemaObject(properties) ? properties : undefined;
|
|
143
|
+
|
|
144
|
+
if (Array.isArray(required)) {
|
|
145
|
+
for (const key of required) {
|
|
146
|
+
if (typeof key === "string" && !(key in value)) {
|
|
147
|
+
errors.push(`${path}: missing required property "${key}"`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (props) {
|
|
153
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
154
|
+
if (key in value && isSchemaObject(propSchema)) {
|
|
155
|
+
validateAt(propSchema, value[key], `${path}.${key}`, errors);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (additionalProperties === false) {
|
|
161
|
+
for (const key of Object.keys(value)) {
|
|
162
|
+
if (!props || !(key in props)) {
|
|
163
|
+
errors.push(`${path}: unexpected additional property "${key}"`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
} else if (isSchemaObject(additionalProperties)) {
|
|
167
|
+
for (const key of Object.keys(value)) {
|
|
168
|
+
if (!props || !(key in props)) {
|
|
169
|
+
validateAt(additionalProperties, value[key], `${path}.${key}`, errors);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isSchemaObject(value: unknown): value is JsonSchema {
|
|
176
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateAt(
|
|
180
|
+
schema: JsonSchema,
|
|
181
|
+
value: unknown,
|
|
182
|
+
path: string,
|
|
183
|
+
errors: string[],
|
|
184
|
+
): void {
|
|
185
|
+
let llm: JsonSchema | undefined;
|
|
186
|
+
try {
|
|
187
|
+
checkGeometrySchema(schema, value);
|
|
188
|
+
llm = markedLlmSchema(schema);
|
|
189
|
+
} catch (error) {
|
|
190
|
+
errors.push(
|
|
191
|
+
`${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
192
|
+
);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
// A Flow-Like model type marker validates against the native schema it names.
|
|
196
|
+
if (llm) {
|
|
197
|
+
validateAt(llm, value, path, errors);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Schemas are pre-inlined by the bundler; a leftover $ref cannot be
|
|
202
|
+
// resolved here, so it is treated as passing.
|
|
203
|
+
if ("$ref" in schema) return;
|
|
204
|
+
|
|
205
|
+
const { type } = schema;
|
|
206
|
+
if (typeof type === "string" && !matchesType(type, value)) {
|
|
207
|
+
errors.push(`${path}: expected type ${type}, got ${describe(value)}`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (Array.isArray(type)) {
|
|
211
|
+
const matched = type.some(
|
|
212
|
+
(candidate) =>
|
|
213
|
+
typeof candidate === "string" && matchesType(candidate, value),
|
|
214
|
+
);
|
|
215
|
+
if (!matched) {
|
|
216
|
+
errors.push(
|
|
217
|
+
`${path}: expected one of types [${type.join(", ")}], got ${describe(value)}`,
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if ("enum" in schema) {
|
|
224
|
+
const allowed = schema.enum;
|
|
225
|
+
if (Array.isArray(allowed)) {
|
|
226
|
+
if (!allowed.some((candidate) => deepEqual(candidate, value))) {
|
|
227
|
+
errors.push(`${path}: value is not one of the allowed enum values`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if ("const" in schema && !deepEqual(schema.const, value)) {
|
|
233
|
+
errors.push(`${path}: value does not equal const`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (typeof value === "number")
|
|
237
|
+
checkNumericBounds(schema, value, path, errors);
|
|
238
|
+
if (typeof value === "string") checkString(schema, value, path, errors);
|
|
239
|
+
if (Array.isArray(value)) checkArray(schema, value, path, errors);
|
|
240
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
241
|
+
checkObject(schema, value as Record<string, unknown>, path, errors);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { anyOf, oneOf, allOf } = schema;
|
|
245
|
+
if (Array.isArray(anyOf)) {
|
|
246
|
+
const passes = anyOf.some(
|
|
247
|
+
(sub) => isSchemaObject(sub) && validateSchema(sub, value).valid,
|
|
248
|
+
);
|
|
249
|
+
if (!passes) errors.push(`${path}: value matches no schema in anyOf`);
|
|
250
|
+
}
|
|
251
|
+
if (Array.isArray(oneOf)) {
|
|
252
|
+
const matches = oneOf.filter(
|
|
253
|
+
(sub) => isSchemaObject(sub) && validateSchema(sub, value).valid,
|
|
254
|
+
).length;
|
|
255
|
+
if (matches !== 1) {
|
|
256
|
+
errors.push(
|
|
257
|
+
`${path}: value matches ${matches} schemas in oneOf, expected exactly 1`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (Array.isArray(allOf)) {
|
|
262
|
+
allOf.forEach((sub, index) => {
|
|
263
|
+
if (isSchemaObject(sub)) {
|
|
264
|
+
const result = validateSchema(sub, value);
|
|
265
|
+
if (!result.valid) {
|
|
266
|
+
errors.push(`${path}: value fails allOf[${index}]`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function validateSchema(
|
|
274
|
+
schema: JsonSchema | null | undefined,
|
|
275
|
+
value: unknown,
|
|
276
|
+
): ValidationResult {
|
|
277
|
+
if (schema === null || schema === undefined) return VALID;
|
|
278
|
+
const errors: string[] = [];
|
|
279
|
+
validateAt(schema, value, "$", errors);
|
|
280
|
+
return { valid: errors.length === 0, errors };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function validateInputValue(
|
|
284
|
+
input: ContractInput,
|
|
285
|
+
value: unknown,
|
|
286
|
+
): ValidationResult {
|
|
287
|
+
if (value === undefined) {
|
|
288
|
+
if (input.optional) return VALID;
|
|
289
|
+
return { valid: false, errors: ["$: value is required"] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const errors: string[] = [];
|
|
293
|
+
switch (input.type) {
|
|
294
|
+
case "string":
|
|
295
|
+
if (typeof value !== "string") {
|
|
296
|
+
errors.push(`$: expected string, got ${describe(value)}`);
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
case "number":
|
|
300
|
+
case "integer": {
|
|
301
|
+
const wantsInteger = input.type === "integer";
|
|
302
|
+
if (
|
|
303
|
+
typeof value !== "number" ||
|
|
304
|
+
(wantsInteger && !Number.isInteger(value))
|
|
305
|
+
) {
|
|
306
|
+
errors.push(`$: expected ${input.type}, got ${describe(value)}`);
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
if (input.min !== undefined && value < input.min) {
|
|
310
|
+
errors.push(`$: ${value} is less than min ${input.min}`);
|
|
311
|
+
}
|
|
312
|
+
if (input.max !== undefined && value > input.max) {
|
|
313
|
+
errors.push(`$: ${value} is greater than max ${input.max}`);
|
|
314
|
+
}
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "boolean":
|
|
318
|
+
if (typeof value !== "boolean") {
|
|
319
|
+
errors.push(`$: expected boolean, got ${describe(value)}`);
|
|
320
|
+
}
|
|
321
|
+
break;
|
|
322
|
+
case "enum":
|
|
323
|
+
if (typeof value !== "string" || !input.choices?.includes(value)) {
|
|
324
|
+
errors.push(
|
|
325
|
+
`$: value is not one of [${(input.choices ?? []).join(", ")}]`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
case "json": {
|
|
330
|
+
const result = validateSchema(input.schema, value);
|
|
331
|
+
errors.push(...result.errors);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return { valid: errors.length === 0, errors };
|
|
336
|
+
}
|
package/src/vanilla.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./index";
|