mevebu 2.0.0 → 2.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/README.md +7 -7
- package/mevebu/app/tailwind.css +9 -3
- package/mevebu/libs/js/core.js +1 -1
- package/mevebu/libs/js/random.js +54 -0
- package/mevebu/libs/js/registry.js +90 -0
- package/mevebu/libs/js/tools/dataset-splitter.js +181 -0
- package/mevebu/libs/js/tools/json-schema-sample.js +254 -0
- package/mevebu/libs/js/tools/mock-api-payload.js +224 -0
- package/mevebu/libs/js/tools/sample-csv.js +2 -36
- package/mevebu/libs/js/tools/survey-responses.js +225 -0
- package/mevebu/libs/js/tools/time-series.js +191 -0
- package/package.json +1 -1
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// Sample documents from a JSON Schema. Covers the keywords that decide whether
|
|
2
|
+
// a document validates, and reports the ones it could not honour, so a passing
|
|
3
|
+
// sample is never mistaken for a complete implementation.
|
|
4
|
+
import {
|
|
5
|
+
el, toast, download, bench, stack, panel, textArea, numberInput, textInput,
|
|
6
|
+
button, actions, toolcard, blank, notice, output,
|
|
7
|
+
} from "../core.js";
|
|
8
|
+
import { seeded, pick, uuid, gaussian } from "../random.js";
|
|
9
|
+
import { zip } from "../zip.js";
|
|
10
|
+
|
|
11
|
+
const SUPPORTED = new Set([
|
|
12
|
+
"$schema", "$id", "title", "description", "type", "properties", "required",
|
|
13
|
+
"items", "enum", "const", "default", "examples", "minimum", "maximum",
|
|
14
|
+
"exclusiveMinimum", "exclusiveMaximum", "multipleOf", "minLength", "maxLength",
|
|
15
|
+
"pattern", "format", "minItems", "maxItems", "uniqueItems", "additionalProperties",
|
|
16
|
+
"minProperties", "maxProperties", "anyOf", "oneOf", "allOf", "not", "nullable",
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
const FORMATS = {
|
|
20
|
+
"date-time": () => new Date(Date.UTC(2025, 0, 1) + Math.floor(Math.random() * 3e10)).toISOString(),
|
|
21
|
+
date: (random) => new Date(Date.UTC(2025, 0, 1) + Math.floor(random() * 700) * 86400000).toISOString().slice(0, 10),
|
|
22
|
+
time: () => `${String(Math.floor(Math.random() * 24)).padStart(2, "0")}:00:00Z`,
|
|
23
|
+
email: (random) => `${pick(random, ["ani", "budi", "citra", "dewi"])}${Math.floor(random() * 99)}@example.com`,
|
|
24
|
+
uri: () => "https://example.com/resource",
|
|
25
|
+
uuid: (random) => uuid(random),
|
|
26
|
+
ipv4: () => `192.168.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
|
27
|
+
hostname: () => "example.com",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function regexFrom(pattern) {
|
|
31
|
+
try {
|
|
32
|
+
return new RegExp(pattern);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stringValue(schema, random) {
|
|
39
|
+
if (schema.format && FORMATS[schema.format]) return FORMATS[schema.format](random);
|
|
40
|
+
if (schema.enum) return pick(random, schema.enum);
|
|
41
|
+
if (schema.const !== undefined) return schema.const;
|
|
42
|
+
|
|
43
|
+
const min = Math.max(0, schema.minLength ?? 0);
|
|
44
|
+
const max = Math.max(min, schema.maxLength ?? Math.max(min, 12));
|
|
45
|
+
const regex = schema.pattern ? regexFrom(schema.pattern) : null;
|
|
46
|
+
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
47
|
+
|
|
48
|
+
// Try a few random strings against the pattern before falling back, so a
|
|
49
|
+
// pattern like ^[A-Z]{3}$ still gets a plausible value.
|
|
50
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
51
|
+
const length = min + Math.floor(random() * Math.max(1, Math.min(max, 24) - min + 1));
|
|
52
|
+
let out = "";
|
|
53
|
+
for (let i = 0; i < length; i++) out += alphabet[Math.floor(random() * alphabet.length)];
|
|
54
|
+
if (!regex || regex.test(out)) return out;
|
|
55
|
+
}
|
|
56
|
+
const literal = schema.pattern?.replace(/[\^$.*+?()[\]{}|\\]/g, "") ?? "";
|
|
57
|
+
if (literal.length >= min) return literal.slice(0, Math.max(min, Math.min(max, literal.length)));
|
|
58
|
+
return "sample".padEnd(min, "x").slice(0, Math.max(min, 1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function numberValue(schema, random, integer) {
|
|
62
|
+
if (schema.enum) return pick(random, schema.enum);
|
|
63
|
+
if (schema.const !== undefined) return schema.const;
|
|
64
|
+
const min = schema.minimum ?? schema.exclusiveMinimum ?? 0;
|
|
65
|
+
const max = schema.maximum ?? schema.exclusiveMaximum ?? (min + 1000);
|
|
66
|
+
let value = min + random() * (max - min);
|
|
67
|
+
if (integer) value = Math.round(value);
|
|
68
|
+
if (schema.multipleOf) value = Math.round(value / schema.multipleOf) * schema.multipleOf;
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sample(schema, random, path, unknown) {
|
|
73
|
+
if (!schema || typeof schema !== "object") return null;
|
|
74
|
+
|
|
75
|
+
if (schema.const !== undefined) return schema.const;
|
|
76
|
+
if (schema.enum) return pick(random, schema.enum);
|
|
77
|
+
if (schema.default !== undefined) return schema.default;
|
|
78
|
+
if (schema.examples?.length) return schema.examples[0];
|
|
79
|
+
if (schema.anyOf?.length) return sample(schema.anyOf[0], random, path, unknown);
|
|
80
|
+
if (schema.oneOf?.length) return sample(schema.oneOf[0], random, path, unknown);
|
|
81
|
+
if (schema.allOf?.length) {
|
|
82
|
+
// Merge the object branches, which is the common shape.
|
|
83
|
+
const merged = schema.allOf.reduce(
|
|
84
|
+
(acc, branch) => ({
|
|
85
|
+
...acc,
|
|
86
|
+
...branch,
|
|
87
|
+
properties: { ...(acc.properties ?? {}), ...(branch.properties ?? {}) },
|
|
88
|
+
required: [...new Set([...(acc.required ?? []), ...(branch.required ?? [])])],
|
|
89
|
+
}),
|
|
90
|
+
{ ...schema, allOf: undefined }
|
|
91
|
+
);
|
|
92
|
+
return sample(merged, random, path, unknown);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const type = Array.isArray(schema.type)
|
|
96
|
+
? schema.type.find((entry) => entry !== "null") ?? "null"
|
|
97
|
+
: schema.type ?? (schema.properties ? "object" : schema.items ? "array" : "string");
|
|
98
|
+
|
|
99
|
+
if (schema.nullable && random() < 0.1) return null;
|
|
100
|
+
|
|
101
|
+
switch (type) {
|
|
102
|
+
case "object": {
|
|
103
|
+
const out = {};
|
|
104
|
+
const properties = schema.properties ?? {};
|
|
105
|
+
for (const [key, child] of Object.entries(properties)) {
|
|
106
|
+
out[key] = sample(child, random, `${path}.${key}`, unknown);
|
|
107
|
+
}
|
|
108
|
+
for (const key of schema.required ?? []) {
|
|
109
|
+
if (!(key in out)) out[key] = sample({}, random, `${path}.${key}`, unknown);
|
|
110
|
+
}
|
|
111
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
112
|
+
out.extra = sample(schema.additionalProperties, random, `${path}.extra`, unknown);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
case "array": {
|
|
117
|
+
const min = Math.max(0, schema.minItems ?? 1);
|
|
118
|
+
const max = Math.max(min, schema.maxItems ?? min + 2);
|
|
119
|
+
const count = min + Math.floor(random() * (Math.min(max, 8) - min + 1));
|
|
120
|
+
const items = Array.from({ length: count }, (_, index) =>
|
|
121
|
+
sample(schema.items ?? {}, random, `${path}[${index}]`, unknown)
|
|
122
|
+
);
|
|
123
|
+
return schema.uniqueItems ? [...new Set(items.map((item) => JSON.stringify(item)))].map((text) => JSON.parse(text)) : items;
|
|
124
|
+
}
|
|
125
|
+
case "integer":
|
|
126
|
+
return numberValue(schema, random, true);
|
|
127
|
+
case "number":
|
|
128
|
+
return Number(numberValue(schema, random, false).toFixed(3));
|
|
129
|
+
case "boolean":
|
|
130
|
+
return random() > 0.5;
|
|
131
|
+
case "null":
|
|
132
|
+
return null;
|
|
133
|
+
default:
|
|
134
|
+
return stringValue(schema, random);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function collectUnknown(schema, found, path = "$") {
|
|
139
|
+
if (!schema || typeof schema !== "object") return;
|
|
140
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
141
|
+
if (["properties", "items", "anyOf", "oneOf", "allOf"].includes(key)) continue;
|
|
142
|
+
if (key.startsWith("$") || key.startsWith("x-")) continue;
|
|
143
|
+
if (!SUPPORTED.has(key) && !found.includes(key)) found.push(key);
|
|
144
|
+
}
|
|
145
|
+
for (const [name, child] of Object.entries(schema.properties ?? {})) {
|
|
146
|
+
collectUnknown(child, found, `${path}.${name}`);
|
|
147
|
+
}
|
|
148
|
+
if (schema.items && typeof schema.items === "object") collectUnknown(schema.items, found, `${path}[]`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function mount(slot) {
|
|
152
|
+
let documents = [];
|
|
153
|
+
let unknown = [];
|
|
154
|
+
|
|
155
|
+
const schema = textArea({
|
|
156
|
+
rows: 12,
|
|
157
|
+
value: JSON.stringify(
|
|
158
|
+
{
|
|
159
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
160
|
+
type: "object",
|
|
161
|
+
required: ["id", "name", "email"],
|
|
162
|
+
properties: {
|
|
163
|
+
id: { type: "string", format: "uuid" },
|
|
164
|
+
name: { type: "string", minLength: 3, maxLength: 24 },
|
|
165
|
+
email: { type: "string", format: "email" },
|
|
166
|
+
age: { type: "integer", minimum: 18, maximum: 90 },
|
|
167
|
+
tags: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 4 },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
null,
|
|
171
|
+
2
|
|
172
|
+
),
|
|
173
|
+
"aria-label": "JSON Schema",
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const count = numberInput({ min: "1", max: "200", step: "1", value: "3", "aria-label": "Documents" });
|
|
177
|
+
const seed = textInput({ value: "mevebu", "aria-label": "Seed", class: "font-mono" });
|
|
178
|
+
const error = notice("warn");
|
|
179
|
+
const summary = el("p", { class: "text-sm text-ink-2" });
|
|
180
|
+
const result = output({ rows: 16 });
|
|
181
|
+
const empty = blank("braces", "Paste a schema to generate documents.");
|
|
182
|
+
|
|
183
|
+
const generate = button("Generate", { icon: "refresh-cw", kind: "fill" });
|
|
184
|
+
const downloadJson = button("Download JSON", { icon: "download" });
|
|
185
|
+
const downloadZip = button("Download ZIP", { icon: "download" });
|
|
186
|
+
|
|
187
|
+
function run() {
|
|
188
|
+
error.write("");
|
|
189
|
+
try {
|
|
190
|
+
const parsed = JSON.parse(schema.value);
|
|
191
|
+
const random = seeded(seed.value || "mevebu");
|
|
192
|
+
unknown = [];
|
|
193
|
+
collectUnknown(parsed, unknown);
|
|
194
|
+
const total = Math.max(1, Math.min(200, Number(count.value) || 1));
|
|
195
|
+
documents = Array.from({ length: total }, () => sample(parsed, random, "$", unknown));
|
|
196
|
+
result.write(JSON.stringify(documents, null, 2));
|
|
197
|
+
summary.textContent = `${total} document${total === 1 ? "" : "s"} · seed ${seed.value || "mevebu"}`;
|
|
198
|
+
empty.hidden = true;
|
|
199
|
+
result.hidden = false;
|
|
200
|
+
if (unknown.length) {
|
|
201
|
+
error.write(`Not implemented, so left out: ${unknown.join(", ")}. Everything else follows the schema.`);
|
|
202
|
+
}
|
|
203
|
+
} catch (caught) {
|
|
204
|
+
documents = [];
|
|
205
|
+
result.write("");
|
|
206
|
+
result.hidden = true;
|
|
207
|
+
empty.hidden = false;
|
|
208
|
+
summary.textContent = "";
|
|
209
|
+
error.write(caught?.message || "That schema is not valid JSON.");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
generate.addEventListener("click", () => {
|
|
214
|
+
run();
|
|
215
|
+
if (documents.length) toast("Documents ready");
|
|
216
|
+
});
|
|
217
|
+
schema.addEventListener("input", run);
|
|
218
|
+
count.addEventListener("input", run);
|
|
219
|
+
seed.addEventListener("input", run);
|
|
220
|
+
|
|
221
|
+
downloadJson.addEventListener("click", () => {
|
|
222
|
+
if (!documents.length) return;
|
|
223
|
+
download("samples.json", JSON.stringify(documents, null, 2), "application/json");
|
|
224
|
+
});
|
|
225
|
+
downloadZip.addEventListener("click", () => {
|
|
226
|
+
if (!documents.length) return;
|
|
227
|
+
const files = documents.map((doc, index) => ({
|
|
228
|
+
name: `sample-${String(index + 1).padStart(3, "0")}.json`,
|
|
229
|
+
bytes: new TextEncoder().encode(JSON.stringify(doc, null, 2)),
|
|
230
|
+
}));
|
|
231
|
+
download("samples.zip", zip(files), "application/zip");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
run();
|
|
235
|
+
|
|
236
|
+
const shell = toolcard({
|
|
237
|
+
body: bench(
|
|
238
|
+
stack(
|
|
239
|
+
panel("Schema", schema),
|
|
240
|
+
panel("Documents", count),
|
|
241
|
+
panel("Seed", seed, "The same seed and schema give the same documents."),
|
|
242
|
+
error
|
|
243
|
+
),
|
|
244
|
+
stack(summary, empty, result)
|
|
245
|
+
),
|
|
246
|
+
foot: actions(downloadJson, downloadZip, generate),
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
slot.append(shell.node);
|
|
250
|
+
|
|
251
|
+
return {};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export default { mount };
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// The JSON an endpoint returns, written once and reused as a fixture. Fields are
|
|
2
|
+
// defined in a small table, and the same definition can be shown as a list with
|
|
3
|
+
// pagination, a single item, or an error.
|
|
4
|
+
import {
|
|
5
|
+
el, icon, toast, download, bench, stack, panel, textInput, selectInput,
|
|
6
|
+
numberInput, button, actions, toolcard, blank, notice, output, chipRow,
|
|
7
|
+
} from "../core.js";
|
|
8
|
+
import { seeded, pick, uuid } from "../random.js";
|
|
9
|
+
|
|
10
|
+
const TYPES = [
|
|
11
|
+
{ value: "text", label: "Text" },
|
|
12
|
+
{ value: "word", label: "Word" },
|
|
13
|
+
{ value: "number", label: "Number" },
|
|
14
|
+
{ value: "decimal", label: "Decimal" },
|
|
15
|
+
{ value: "boolean", label: "Boolean" },
|
|
16
|
+
{ value: "date", label: "Date" },
|
|
17
|
+
{ value: "email", label: "Email" },
|
|
18
|
+
{ value: "uuid", label: "UUID" },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const WORDS = ["north", "cedar", "orbit", "summit", "tide", "willow", "ember", "quartz"];
|
|
22
|
+
|
|
23
|
+
function value(type, random, index) {
|
|
24
|
+
switch (type) {
|
|
25
|
+
case "word": return pick(random, WORDS);
|
|
26
|
+
case "number": return Math.floor(random() * 5000);
|
|
27
|
+
case "decimal": return Number((random() * 200).toFixed(2));
|
|
28
|
+
case "boolean": return random() > 0.5;
|
|
29
|
+
case "date": return new Date(Date.UTC(2025, 0, 1) + Math.floor(random() * 6e10)).toISOString();
|
|
30
|
+
case "email": return `${pick(random, WORDS)}${index + 1}@example.com`;
|
|
31
|
+
case "uuid": return uuid(random);
|
|
32
|
+
default: return `${pick(random, WORDS)} ${pick(random, WORDS)}`;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function mount(slot) {
|
|
37
|
+
let fields = [
|
|
38
|
+
{ name: "id", type: "uuid" },
|
|
39
|
+
{ name: "title", type: "text" },
|
|
40
|
+
{ name: "price", type: "decimal" },
|
|
41
|
+
{ name: "in_stock", type: "boolean" },
|
|
42
|
+
{ name: "created_at", type: "date" },
|
|
43
|
+
];
|
|
44
|
+
let shape = "list";
|
|
45
|
+
|
|
46
|
+
const resource = textInput({ value: "items", "aria-label": "Resource name" });
|
|
47
|
+
const seed = textInput({ value: "mevebu", "aria-label": "Seed", class: "font-mono" });
|
|
48
|
+
const items = numberInput({ min: "1", max: "100", step: "1", value: "5", "aria-label": "Items" });
|
|
49
|
+
const status = selectInput(
|
|
50
|
+
[
|
|
51
|
+
{ value: "200", label: "200 OK" },
|
|
52
|
+
{ value: "201", label: "201 Created" },
|
|
53
|
+
{ value: "400", label: "400 Bad Request" },
|
|
54
|
+
{ value: "401", label: "401 Unauthorized" },
|
|
55
|
+
{ value: "404", label: "404 Not Found" },
|
|
56
|
+
{ value: "422", label: "422 Unprocessable" },
|
|
57
|
+
{ value: "500", label: "500 Server Error" },
|
|
58
|
+
],
|
|
59
|
+
{ value: "200", "aria-label": "HTTP status" }
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const shapeRow = chipRow(
|
|
63
|
+
[
|
|
64
|
+
{ value: "list", label: "List" },
|
|
65
|
+
{ value: "item", label: "Single item" },
|
|
66
|
+
{ value: "error", label: "Error" },
|
|
67
|
+
],
|
|
68
|
+
{ value: "list", label: "Payload shape", onChange: (next) => { shape = next; sync(); run(); } }
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const error = notice("warn");
|
|
72
|
+
const summary = el("p", { class: "text-sm text-ink-2" });
|
|
73
|
+
const result = output({ rows: 16 });
|
|
74
|
+
const empty = blank("braces", "Define a field to build the payload.");
|
|
75
|
+
const list = el("div", { class: "grid gap-2" });
|
|
76
|
+
|
|
77
|
+
const itemsPanel = panel("Items", items);
|
|
78
|
+
const statusPanel = panel("HTTP status", status);
|
|
79
|
+
|
|
80
|
+
function sync() {
|
|
81
|
+
itemsPanel.hidden = shape !== "list";
|
|
82
|
+
statusPanel.hidden = shape !== "error";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderFields() {
|
|
86
|
+
list.replaceChildren(
|
|
87
|
+
...fields.map((field, index) =>
|
|
88
|
+
el("div", { class: "grid gap-2 rounded-r1 border border-orbit bg-deck-2 p-2 [grid-template-columns:minmax(0,1fr)_auto]" },
|
|
89
|
+
el("div", { class: "grid gap-2 sm:grid-cols-2" },
|
|
90
|
+
textInput({
|
|
91
|
+
value: field.name,
|
|
92
|
+
placeholder: `field_${index + 1}`,
|
|
93
|
+
"aria-label": `Field ${index + 1} name`,
|
|
94
|
+
oninput: (event) => { field.name = event.target.value; run(); },
|
|
95
|
+
}),
|
|
96
|
+
selectInput(TYPES.map((type) => ({ value: type.value, label: type.label })), {
|
|
97
|
+
value: field.type,
|
|
98
|
+
"aria-label": `Field ${index + 1} type`,
|
|
99
|
+
onchange: (event) => { field.type = event.target.value; run(); },
|
|
100
|
+
})),
|
|
101
|
+
el("button", {
|
|
102
|
+
class: "inline-flex size-9 cursor-pointer items-center justify-center self-start rounded-[5px] border-0 bg-transparent text-ink-3 hover:bg-deck hover:text-danger disabled:opacity-40",
|
|
103
|
+
type: "button",
|
|
104
|
+
"aria-label": `Remove field ${index + 1}`,
|
|
105
|
+
disabled: fields.length === 1,
|
|
106
|
+
onclick: () => { fields.splice(index, 1); renderFields(); run(); },
|
|
107
|
+
}, icon("trash", "size-4")))
|
|
108
|
+
)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function one(random, index) {
|
|
113
|
+
const out = {};
|
|
114
|
+
for (const field of fields) {
|
|
115
|
+
const key = field.name.trim() || `field_${index + 1}`;
|
|
116
|
+
out[key] = value(field.type, random, index);
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function run() {
|
|
122
|
+
error.write("");
|
|
123
|
+
const random = seeded(seed.value || "mevebu");
|
|
124
|
+
const name = resource.value.trim() || "items";
|
|
125
|
+
const count = Math.max(1, Math.min(100, Number(items.value) || 1));
|
|
126
|
+
const rows = Array.from({ length: count }, (_, index) => one(random, index));
|
|
127
|
+
|
|
128
|
+
let payload;
|
|
129
|
+
let http = Number(status.value);
|
|
130
|
+
if (shape === "list") {
|
|
131
|
+
payload = {
|
|
132
|
+
data: rows,
|
|
133
|
+
meta: { page: 1, per_page: count, total: count * 4, has_more: true },
|
|
134
|
+
};
|
|
135
|
+
http = 200;
|
|
136
|
+
} else if (shape === "item") {
|
|
137
|
+
payload = { data: rows[0] };
|
|
138
|
+
http = 200;
|
|
139
|
+
} else {
|
|
140
|
+
const messages = {
|
|
141
|
+
400: "The request body is not valid JSON.",
|
|
142
|
+
401: "Missing or expired token.",
|
|
143
|
+
404: `No ${name} with that id.`,
|
|
144
|
+
422: "The title field is required.",
|
|
145
|
+
500: "Something went wrong on our side.",
|
|
146
|
+
};
|
|
147
|
+
payload = {
|
|
148
|
+
error: { status: http, code: http === 401 ? "unauthorized" : http === 404 ? "not_found" : "bad_request", message: messages[http] ?? "Request failed." },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
result.write(JSON.stringify(payload, null, 2));
|
|
153
|
+
empty.hidden = true;
|
|
154
|
+
result.hidden = false;
|
|
155
|
+
const bytes = new TextEncoder().encode(JSON.stringify(payload)).length;
|
|
156
|
+
summary.textContent = `${http} · ${shape} · ${bytes} bytes${shape === "list" ? ` · ${count} item${count === 1 ? "" : "s"}` : ""}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resource.addEventListener("input", run);
|
|
160
|
+
seed.addEventListener("input", run);
|
|
161
|
+
items.addEventListener("input", run);
|
|
162
|
+
status.addEventListener("change", run);
|
|
163
|
+
|
|
164
|
+
const addButton = button("Add field", {
|
|
165
|
+
icon: "plus",
|
|
166
|
+
kind: "quiet",
|
|
167
|
+
onclick: () => {
|
|
168
|
+
fields.push({ name: `field_${fields.length + 1}`, type: "text" });
|
|
169
|
+
renderFields();
|
|
170
|
+
run();
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const copyButton = button("Copy JSON", {
|
|
175
|
+
icon: "copy",
|
|
176
|
+
onclick: async () => {
|
|
177
|
+
try {
|
|
178
|
+
await navigator.clipboard.writeText(result.textContent);
|
|
179
|
+
toast("Payload copied");
|
|
180
|
+
} catch {
|
|
181
|
+
error.write("The clipboard is blocked here. Download the file instead.");
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const downloadButton = button("Download .json", {
|
|
187
|
+
icon: "download",
|
|
188
|
+
kind: "fill",
|
|
189
|
+
onclick: () => {
|
|
190
|
+
if (!result.textContent) return;
|
|
191
|
+
download(`${(resource.value.trim() || "payload").replace(/[^\w-]+/g, "-")}-${shape}.json`, result.textContent, "application/json");
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
renderFields();
|
|
196
|
+
sync();
|
|
197
|
+
run();
|
|
198
|
+
|
|
199
|
+
const shell = toolcard({
|
|
200
|
+
body: bench(
|
|
201
|
+
stack(
|
|
202
|
+
panel("Resource", resource, "Used in the error message and the file name."),
|
|
203
|
+
el("div", { class: "grid gap-1.5" }, el("span", { class: "text-sm font-semibold text-ink-2" }, "Shape"), shapeRow),
|
|
204
|
+
itemsPanel,
|
|
205
|
+
statusPanel,
|
|
206
|
+
panel("Seed", seed),
|
|
207
|
+
el("div", { class: "grid gap-1.5" },
|
|
208
|
+
el("div", { class: "flex items-center justify-between gap-2" },
|
|
209
|
+
el("span", { class: "text-sm font-semibold text-ink-2" }, "Fields"),
|
|
210
|
+
addButton),
|
|
211
|
+
list),
|
|
212
|
+
error
|
|
213
|
+
),
|
|
214
|
+
stack(summary, empty, result)
|
|
215
|
+
),
|
|
216
|
+
foot: actions(copyButton, downloadButton),
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
slot.append(shell.node);
|
|
220
|
+
|
|
221
|
+
return {};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export default { mount };
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
numberInput, button, actions, toolcard, blank, notice,
|
|
6
6
|
} from "../core.js";
|
|
7
7
|
import { toCsv } from "../csv.js";
|
|
8
|
+
import { seeded, pick, uuid } from "../random.js";
|
|
8
9
|
|
|
9
10
|
const TYPES = [
|
|
10
11
|
{ value: "text", label: "Text" },
|
|
@@ -24,32 +25,6 @@ const WORDS = [
|
|
|
24
25
|
"umber", "vale", "willow", "xenon", "yarrow", "zephyr",
|
|
25
26
|
];
|
|
26
27
|
|
|
27
|
-
/** Mulberry32: a small deterministic generator, so a seed is worth writing down. */
|
|
28
|
-
function seeded(seed) {
|
|
29
|
-
let state = seed >>> 0;
|
|
30
|
-
return () => {
|
|
31
|
-
state = (state + 0x6d2b79f5) >>> 0;
|
|
32
|
-
let t = state;
|
|
33
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
34
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
35
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pick(random, list) {
|
|
40
|
-
return list[Math.floor(random() * list.length)];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function uuid(random) {
|
|
44
|
-
const hex = "0123456789abcdef";
|
|
45
|
-
let out = "";
|
|
46
|
-
for (let i = 0; i < 32; i++) {
|
|
47
|
-
if (i === 8 || i === 12 || i === 16 || i === 20) out += "-";
|
|
48
|
-
out += i === 12 ? "4" : hex[Math.floor(random() * 16)];
|
|
49
|
-
}
|
|
50
|
-
return out;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
28
|
function value(type, choices, random, rowIndex) {
|
|
54
29
|
switch (type) {
|
|
55
30
|
case "word":
|
|
@@ -153,15 +128,6 @@ export function mount(slot) {
|
|
|
153
128
|
);
|
|
154
129
|
}
|
|
155
130
|
|
|
156
|
-
function hashSeed(text) {
|
|
157
|
-
let hash = 2166136261;
|
|
158
|
-
for (let i = 0; i < text.length; i++) {
|
|
159
|
-
hash ^= text.charCodeAt(i);
|
|
160
|
-
hash = Math.imul(hash, 16777619);
|
|
161
|
-
}
|
|
162
|
-
return hash >>> 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
131
|
function renderPreview() {
|
|
166
132
|
empty.hidden = table.length > 0;
|
|
167
133
|
preview.hidden = table.length === 0;
|
|
@@ -191,7 +157,7 @@ export function mount(slot) {
|
|
|
191
157
|
const names = columns.map((column, index) => column.name.trim() || `column_${index + 1}`);
|
|
192
158
|
if (new Set(names).size !== names.length) error.write("Two columns share a name, so the CSV will have duplicates.");
|
|
193
159
|
|
|
194
|
-
const random = seeded(
|
|
160
|
+
const random = seeded(seed.value || "mevebu");
|
|
195
161
|
header = names;
|
|
196
162
|
table = Array.from({ length: count }, (_, rowIndex) =>
|
|
197
163
|
columns.map((column) =>
|