openuispec 0.1.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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/cli/index.ts +49 -0
- package/cli/init.ts +390 -0
- package/drift/index.ts +398 -0
- package/examples/taskflow/README.md +103 -0
- package/examples/taskflow/contracts/README.md +18 -0
- package/examples/taskflow/contracts/action_trigger.yaml +7 -0
- package/examples/taskflow/contracts/collection.yaml +7 -0
- package/examples/taskflow/contracts/data_display.yaml +7 -0
- package/examples/taskflow/contracts/feedback.yaml +7 -0
- package/examples/taskflow/contracts/input_field.yaml +7 -0
- package/examples/taskflow/contracts/nav_container.yaml +7 -0
- package/examples/taskflow/contracts/surface.yaml +7 -0
- package/examples/taskflow/contracts/x_media_player.yaml +185 -0
- package/examples/taskflow/flows/create_task.yaml +171 -0
- package/examples/taskflow/flows/edit_task.yaml +131 -0
- package/examples/taskflow/locales/en.json +158 -0
- package/examples/taskflow/openuispec.yaml +144 -0
- package/examples/taskflow/platform/android.yaml +32 -0
- package/examples/taskflow/platform/ios.yaml +39 -0
- package/examples/taskflow/platform/web.yaml +35 -0
- package/examples/taskflow/screens/calendar.yaml +23 -0
- package/examples/taskflow/screens/home.yaml +220 -0
- package/examples/taskflow/screens/profile_edit.yaml +70 -0
- package/examples/taskflow/screens/project_detail.yaml +65 -0
- package/examples/taskflow/screens/projects.yaml +142 -0
- package/examples/taskflow/screens/settings.yaml +178 -0
- package/examples/taskflow/screens/task_detail.yaml +317 -0
- package/examples/taskflow/tokens/color.yaml +88 -0
- package/examples/taskflow/tokens/elevation.yaml +27 -0
- package/examples/taskflow/tokens/icons.yaml +189 -0
- package/examples/taskflow/tokens/layout.yaml +156 -0
- package/examples/taskflow/tokens/motion.yaml +41 -0
- package/examples/taskflow/tokens/spacing.yaml +23 -0
- package/examples/taskflow/tokens/themes.yaml +34 -0
- package/examples/taskflow/tokens/typography.yaml +61 -0
- package/package.json +43 -0
- package/schema/custom-contract.schema.json +257 -0
- package/schema/defs/action.schema.json +272 -0
- package/schema/defs/adaptive.schema.json +13 -0
- package/schema/defs/common.schema.json +330 -0
- package/schema/defs/data-binding.schema.json +119 -0
- package/schema/defs/validation.schema.json +121 -0
- package/schema/flow.schema.json +164 -0
- package/schema/locale.schema.json +26 -0
- package/schema/openuispec.schema.json +287 -0
- package/schema/platform.schema.json +95 -0
- package/schema/screen.schema.json +346 -0
- package/schema/tokens/color.schema.json +104 -0
- package/schema/tokens/elevation.schema.json +84 -0
- package/schema/tokens/icons.schema.json +149 -0
- package/schema/tokens/layout.schema.json +170 -0
- package/schema/tokens/motion.schema.json +83 -0
- package/schema/tokens/spacing.schema.json +93 -0
- package/schema/tokens/themes.schema.json +92 -0
- package/schema/tokens/typography.schema.json +106 -0
- package/schema/validate.ts +258 -0
- package/spec/openuispec-v0.1.md +3677 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Validate all TaskFlow example files against their OpenUISpec JSON Schemas.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npm run validate # validate everything
|
|
7
|
+
* npm run validate:tokens # validate only tokens
|
|
8
|
+
* npx tsx schema/validate.ts screens flows # multiple groups
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
12
|
+
import { resolve, join, basename } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { createRequire } from "node:module";
|
|
15
|
+
import type { ErrorObject } from "ajv";
|
|
16
|
+
import YAML from "yaml";
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const Ajv2020 = require("ajv/dist/2020") as typeof import("ajv").default;
|
|
20
|
+
const addFormats = require("ajv-formats") as typeof import("ajv-formats").default;
|
|
21
|
+
|
|
22
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
23
|
+
const SCHEMA_DIR = resolve(__dirname);
|
|
24
|
+
const EXAMPLES_DIR = resolve(SCHEMA_DIR, "..", "examples", "taskflow");
|
|
25
|
+
|
|
26
|
+
type AjvInstance = InstanceType<typeof Ajv2020>;
|
|
27
|
+
|
|
28
|
+
// ── helpers ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
function loadJson(filePath: string): Record<string, unknown> {
|
|
31
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadYaml(filePath: string): unknown {
|
|
35
|
+
return YAML.parse(readFileSync(filePath, "utf-8"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function loadData(filePath: string): unknown {
|
|
39
|
+
return filePath.endsWith(".json") ? loadJson(filePath) : loadYaml(filePath);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function listFiles(dir: string, ext: string): string[] {
|
|
43
|
+
try {
|
|
44
|
+
return readdirSync(dir)
|
|
45
|
+
.filter((f) => f.endsWith(ext))
|
|
46
|
+
.sort()
|
|
47
|
+
.map((f) => join(dir, f));
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── build Ajv instance with all schemas ──────────────────────────────
|
|
54
|
+
|
|
55
|
+
function buildAjv(): AjvInstance {
|
|
56
|
+
const ajv = new Ajv2020({
|
|
57
|
+
strict: false,
|
|
58
|
+
allErrors: true,
|
|
59
|
+
verbose: true,
|
|
60
|
+
});
|
|
61
|
+
addFormats(ajv);
|
|
62
|
+
|
|
63
|
+
const schemaFiles = [
|
|
64
|
+
...listFiles(join(SCHEMA_DIR, "defs"), ".schema.json"),
|
|
65
|
+
...listFiles(join(SCHEMA_DIR, "tokens"), ".schema.json"),
|
|
66
|
+
...listFiles(SCHEMA_DIR, ".schema.json"),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const schemas = schemaFiles.map((f) => loadJson(f));
|
|
70
|
+
ajv.addSchema(schemas);
|
|
71
|
+
|
|
72
|
+
return ajv;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── validate one file ────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
function validateFile(
|
|
78
|
+
ajv: AjvInstance,
|
|
79
|
+
dataPath: string,
|
|
80
|
+
schemaId: string,
|
|
81
|
+
label?: string,
|
|
82
|
+
): number {
|
|
83
|
+
const name = label ?? basename(dataPath);
|
|
84
|
+
const data = loadData(dataPath);
|
|
85
|
+
const validate = ajv.getSchema(schemaId);
|
|
86
|
+
|
|
87
|
+
if (!validate) {
|
|
88
|
+
console.log(` SKIP ${name} (schema ${schemaId} not found)`);
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const valid = validate(data);
|
|
93
|
+
if (valid) {
|
|
94
|
+
console.log(` OK ${name}`);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const errors: ErrorObject[] = validate.errors ?? [];
|
|
99
|
+
console.log(` FAIL ${name} (${errors.length} error(s))`);
|
|
100
|
+
for (const e of errors.slice(0, 5)) {
|
|
101
|
+
const instancePath = e.instancePath || "(root)";
|
|
102
|
+
console.log(` [${instancePath}] ${e.message}`);
|
|
103
|
+
if (e.params) {
|
|
104
|
+
const info = Object.entries(e.params)
|
|
105
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
106
|
+
.join(", ");
|
|
107
|
+
if (info) console.log(` ${info}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (errors.length > 5) {
|
|
111
|
+
console.log(` ... and ${errors.length - 5} more`);
|
|
112
|
+
}
|
|
113
|
+
return errors.length;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── validation groups ────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const BASE = "https://openuispec.org/schema/";
|
|
119
|
+
|
|
120
|
+
interface ValidationGroup {
|
|
121
|
+
label: string;
|
|
122
|
+
run(ajv: AjvInstance): number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const GROUPS: Record<string, ValidationGroup> = {
|
|
126
|
+
manifest: {
|
|
127
|
+
label: "Root manifest",
|
|
128
|
+
run(ajv) {
|
|
129
|
+
return validateFile(
|
|
130
|
+
ajv,
|
|
131
|
+
join(EXAMPLES_DIR, "openuispec.yaml"),
|
|
132
|
+
`${BASE}openuispec.schema.json`,
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
tokens: {
|
|
138
|
+
label: "Tokens",
|
|
139
|
+
run(ajv) {
|
|
140
|
+
let errors = 0;
|
|
141
|
+
const tokenMap: Record<string, string> = {
|
|
142
|
+
"color.yaml": "color.schema.json",
|
|
143
|
+
"typography.yaml": "typography.schema.json",
|
|
144
|
+
"spacing.yaml": "spacing.schema.json",
|
|
145
|
+
"elevation.yaml": "elevation.schema.json",
|
|
146
|
+
"motion.yaml": "motion.schema.json",
|
|
147
|
+
"layout.yaml": "layout.schema.json",
|
|
148
|
+
"themes.yaml": "themes.schema.json",
|
|
149
|
+
"icons.yaml": "icons.schema.json",
|
|
150
|
+
};
|
|
151
|
+
for (const [data, schema] of Object.entries(tokenMap)) {
|
|
152
|
+
errors += validateFile(
|
|
153
|
+
ajv,
|
|
154
|
+
join(EXAMPLES_DIR, "tokens", data),
|
|
155
|
+
`${BASE}tokens/${schema}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return errors;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
screens: {
|
|
163
|
+
label: "Screens",
|
|
164
|
+
run(ajv) {
|
|
165
|
+
let errors = 0;
|
|
166
|
+
for (const f of listFiles(join(EXAMPLES_DIR, "screens"), ".yaml")) {
|
|
167
|
+
errors += validateFile(ajv, f, `${BASE}screen.schema.json`);
|
|
168
|
+
}
|
|
169
|
+
return errors;
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
flows: {
|
|
174
|
+
label: "Flows",
|
|
175
|
+
run(ajv) {
|
|
176
|
+
let errors = 0;
|
|
177
|
+
for (const f of listFiles(join(EXAMPLES_DIR, "flows"), ".yaml")) {
|
|
178
|
+
errors += validateFile(ajv, f, `${BASE}flow.schema.json`);
|
|
179
|
+
}
|
|
180
|
+
return errors;
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
platform: {
|
|
185
|
+
label: "Platform",
|
|
186
|
+
run(ajv) {
|
|
187
|
+
let errors = 0;
|
|
188
|
+
for (const f of listFiles(join(EXAMPLES_DIR, "platform"), ".yaml")) {
|
|
189
|
+
errors += validateFile(ajv, f, `${BASE}platform.schema.json`);
|
|
190
|
+
}
|
|
191
|
+
return errors;
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
locales: {
|
|
196
|
+
label: "Locales",
|
|
197
|
+
run(ajv) {
|
|
198
|
+
let errors = 0;
|
|
199
|
+
for (const f of listFiles(join(EXAMPLES_DIR, "locales"), ".json")) {
|
|
200
|
+
errors += validateFile(ajv, f, `${BASE}locale.schema.json`);
|
|
201
|
+
}
|
|
202
|
+
return errors;
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
custom_contracts: {
|
|
207
|
+
label: "Custom contracts",
|
|
208
|
+
run(ajv) {
|
|
209
|
+
let errors = 0;
|
|
210
|
+
for (const f of listFiles(join(EXAMPLES_DIR, "contracts"), ".yaml")) {
|
|
211
|
+
if (basename(f).startsWith("x_")) {
|
|
212
|
+
errors += validateFile(
|
|
213
|
+
ajv,
|
|
214
|
+
f,
|
|
215
|
+
`${BASE}custom-contract.schema.json`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return errors;
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ── main ─────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
function main(): void {
|
|
227
|
+
const args = process.argv.slice(2);
|
|
228
|
+
const selected =
|
|
229
|
+
args.length > 0
|
|
230
|
+
? args.filter((a) => a in GROUPS)
|
|
231
|
+
: Object.keys(GROUPS);
|
|
232
|
+
|
|
233
|
+
if (selected.length === 0) {
|
|
234
|
+
console.error(
|
|
235
|
+
`Unknown group(s). Available: ${Object.keys(GROUPS).join(", ")}`,
|
|
236
|
+
);
|
|
237
|
+
process.exit(2);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ajv = buildAjv();
|
|
241
|
+
let totalErrors = 0;
|
|
242
|
+
|
|
243
|
+
for (const key of selected) {
|
|
244
|
+
const group = GROUPS[key];
|
|
245
|
+
console.log(`\n${group.label}:`);
|
|
246
|
+
totalErrors += group.run(ajv);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
console.log(`\n${"=".repeat(50)}`);
|
|
250
|
+
if (totalErrors > 0) {
|
|
251
|
+
console.log(`FAILED: ${totalErrors} total validation error(s)`);
|
|
252
|
+
process.exit(1);
|
|
253
|
+
} else {
|
|
254
|
+
console.log("ALL PASSED: Every example file validates successfully");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
main();
|