@yuneta/gobj-ui 6.0.0 → 6.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 +95 -2
- package/dist/gobj-ui.cjs.js +5374 -476
- package/dist/gobj-ui.es.js +5374 -477
- package/index.js +1 -0
- package/package.json +1 -1
- package/src/c_yui_schema_editor.css +133 -0
- package/src/c_yui_schema_editor.js +3217 -0
- package/src/c_yui_treedb_schema.js +22 -3
- package/src/schema_descs.js +164 -0
- package/src/schema_descs.test.js +145 -0
- package/src/schema_flags.js +237 -0
- package/src/schema_flags.test.js +133 -0
- package/src/schema_import.js +413 -0
- package/src/schema_import.test.js +234 -0
- package/src/schema_model.js +512 -0
- package/src/schema_model.test.js +233 -0
- package/src/schema_to_c.js +367 -0
- package/src/schema_to_c.test.js +218 -0
- package/src/schema_validate.js +289 -0
- package/src/schema_validate.test.js +265 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_model.test.js
|
|
3
|
+
*
|
|
4
|
+
* The three flat topics turned back into a schema, pinned.
|
|
5
|
+
*
|
|
6
|
+
* Most of these are about the two ways the grouping can go wrong
|
|
7
|
+
* quietly: a fkey shape the backend answers with and the parser
|
|
8
|
+
* does not know (the record disappears from its topic), and an id
|
|
9
|
+
* split on '.' standing in for the link (the record lands under
|
|
10
|
+
* the wrong topic the day a name carries a dot).
|
|
11
|
+
***********************************************************************/
|
|
12
|
+
import { describe, test, expect } from "vitest";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_ORDER,
|
|
15
|
+
parse_fkey_ref,
|
|
16
|
+
record_name,
|
|
17
|
+
record_order,
|
|
18
|
+
build_schema_model,
|
|
19
|
+
find_treedb,
|
|
20
|
+
find_topic,
|
|
21
|
+
find_col,
|
|
22
|
+
fkey_ref,
|
|
23
|
+
next_order,
|
|
24
|
+
moved_orders,
|
|
25
|
+
} from "./schema_model.js";
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
function col(treedb, topic, name, order, extra)
|
|
29
|
+
{
|
|
30
|
+
return Object.assign({
|
|
31
|
+
id: `${treedb}.${topic}.${name}`,
|
|
32
|
+
value: name,
|
|
33
|
+
order: order,
|
|
34
|
+
topics: [`topics^${treedb}.${topic}^cols`],
|
|
35
|
+
}, extra || {});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function topic(treedb, name, order, extra)
|
|
39
|
+
{
|
|
40
|
+
return Object.assign({
|
|
41
|
+
id: `${treedb}.${name}`,
|
|
42
|
+
value: name,
|
|
43
|
+
order: order,
|
|
44
|
+
treedbs: [`treedbs^${treedb}^topics`],
|
|
45
|
+
}, extra || {});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
describe("parse_fkey_ref — the four shapes of one fact", () => {
|
|
50
|
+
test("the bare ref string", () => {
|
|
51
|
+
expect(parse_fkey_ref("topics^db.users^cols")).toEqual([
|
|
52
|
+
{topic_name: "topics", id: "db.users", hook_name: "cols"}
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("a list of ref strings", () => {
|
|
57
|
+
expect(parse_fkey_ref(["topics^db.users^cols"])[0].id).toBe("db.users");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("the expanded object", () => {
|
|
61
|
+
expect(parse_fkey_ref([{topic_name: "topics", id: "db.users", hook_name: "cols"}])).toEqual([
|
|
62
|
+
{topic_name: "topics", id: "db.users", hook_name: "cols"}
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a dict keyed BY the ref", () => {
|
|
67
|
+
expect(parse_fkey_ref({"topics^db.users^cols": true})[0].id).toBe("db.users");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a dict whose value is the expanded node reads the value, not the key", () => {
|
|
71
|
+
const refs = parse_fkey_ref({
|
|
72
|
+
"topics^stale^cols": {topic_name: "topics", id: "db.users", hook_name: "cols"}
|
|
73
|
+
});
|
|
74
|
+
expect(refs[0].id).toBe("db.users");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("nothing, and rubbish, contribute nothing", () => {
|
|
78
|
+
expect(parse_fkey_ref(null)).toEqual([]);
|
|
79
|
+
expect(parse_fkey_ref(undefined)).toEqual([]);
|
|
80
|
+
expect(parse_fkey_ref([])).toEqual([]);
|
|
81
|
+
expect(parse_fkey_ref("not-a-ref")).toEqual([]);
|
|
82
|
+
expect(parse_fkey_ref(["a^b"])).toEqual([]);
|
|
83
|
+
expect(parse_fkey_ref([{topic_name: "topics"}])).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("record_name / record_order", () => {
|
|
88
|
+
test("the name is the pkey2, not the qualified id", () => {
|
|
89
|
+
expect(record_name({id: "db.users.name", value: "name"})).toBe("name");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("no pkey2 falls back to the id — an older store keyed by rowid", () => {
|
|
93
|
+
expect(record_name({id: "181"})).toBe("181");
|
|
94
|
+
expect(record_name({id: "181", value: ""})).toBe("181");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("an order arriving as a STRING still sorts as a number", () => {
|
|
98
|
+
expect(record_order({order: "10"})).toBe(10);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("no order at all goes last", () => {
|
|
102
|
+
expect(record_order({})).toBe(DEFAULT_ORDER);
|
|
103
|
+
expect(record_order({order: "not a number"})).toBe(DEFAULT_ORDER);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe("build_schema_model", () => {
|
|
108
|
+
const records = {
|
|
109
|
+
treedbs: [
|
|
110
|
+
{id: "treedb_b", schema_version: 3, c_schema_version: 3},
|
|
111
|
+
{id: "treedb_a", schema_version: 24, c_schema_version: 23},
|
|
112
|
+
],
|
|
113
|
+
topics: [
|
|
114
|
+
topic("treedb_a", "users", 2, {pkey: "id", topic_version: 7}),
|
|
115
|
+
topic("treedb_a", "departments", 1, {pkey: "id", topic_version: 4}),
|
|
116
|
+
topic("treedb_b", "logs", 1),
|
|
117
|
+
],
|
|
118
|
+
cols: [
|
|
119
|
+
col("treedb_a", "users", "name", 2),
|
|
120
|
+
col("treedb_a", "users", "id", 1),
|
|
121
|
+
col("treedb_a", "departments", "id", 1),
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
test("treedbs carry their topics, topics their columns", () => {
|
|
126
|
+
const model = build_schema_model(records);
|
|
127
|
+
expect(model.treedbs.map(d => d.id)).toEqual(["treedb_a", "treedb_b"]);
|
|
128
|
+
const a = find_treedb(model, "treedb_a");
|
|
129
|
+
expect(a.topics.map(x => x.name)).toEqual(["departments", "users"]);
|
|
130
|
+
expect(find_topic(model, "treedb_a", "users").cols.map(c => c.name)).toEqual(["id", "name"]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("the schema keeps its DECLARED order, not the store's", () => {
|
|
134
|
+
/* `users` is order 2 and comes first in the record list; the model
|
|
135
|
+
puts `departments` (order 1) ahead of it. */
|
|
136
|
+
const model = build_schema_model(records);
|
|
137
|
+
expect(find_treedb(model, "treedb_a").topics[0].name).toBe("departments");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("versions and topic metadata travel with the entry", () => {
|
|
141
|
+
const model = build_schema_model(records);
|
|
142
|
+
expect(find_treedb(model, "treedb_a").schema_version).toBe(24);
|
|
143
|
+
expect(find_treedb(model, "treedb_a").c_schema_version).toBe(23);
|
|
144
|
+
expect(find_topic(model, "treedb_a", "users").topic_version).toBe(7);
|
|
145
|
+
expect(find_topic(model, "treedb_a", "users").pkey).toBe("id");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("grouping follows the FKEY, so a name carrying a dot still lands right", () => {
|
|
149
|
+
/* Splitting the qualified id on '.' would file this column under a
|
|
150
|
+
treedb called "my" and a topic called "db". */
|
|
151
|
+
const model = build_schema_model({
|
|
152
|
+
treedbs: [{id: "my.db"}],
|
|
153
|
+
topics: [{id: "my.db.users", value: "users", treedbs: ["treedbs^my.db^topics"]}],
|
|
154
|
+
cols: [{id: "my.db.users.name", value: "name",
|
|
155
|
+
topics: ["topics^my.db.users^cols"]}],
|
|
156
|
+
});
|
|
157
|
+
expect(find_col(model, "my.db", "users", "name")).toBeTruthy();
|
|
158
|
+
expect(model.orphan_cols).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("a record whose parent is gone is an ORPHAN, not a silent drop", () => {
|
|
162
|
+
const model = build_schema_model({
|
|
163
|
+
treedbs: [{id: "treedb_a"}],
|
|
164
|
+
topics: [topic("treedb_gone", "users", 1)],
|
|
165
|
+
cols: [col("treedb_a", "vanished", "name", 1)],
|
|
166
|
+
});
|
|
167
|
+
expect(model.orphan_topics.map(x => x.name)).toEqual(["users"]);
|
|
168
|
+
expect(model.orphan_cols.map(x => x.name)).toEqual(["name"]);
|
|
169
|
+
expect(find_treedb(model, "treedb_a").topics).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("equal orders break the tie by name, so two runs draw the same schema", () => {
|
|
173
|
+
const model = build_schema_model({
|
|
174
|
+
treedbs: [{id: "db"}],
|
|
175
|
+
topics: [topic("db", "zeta"), topic("db", "alpha")],
|
|
176
|
+
cols: [],
|
|
177
|
+
});
|
|
178
|
+
expect(find_treedb(model, "db").topics.map(x => x.name)).toEqual(["alpha", "zeta"]);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("empty and absent inputs give an empty model, never a throw", () => {
|
|
182
|
+
expect(build_schema_model().treedbs).toEqual([]);
|
|
183
|
+
expect(build_schema_model({}).orphan_cols).toEqual([]);
|
|
184
|
+
expect(build_schema_model({treedbs: null, topics: 7}).treedbs).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a record with no id is not a record", () => {
|
|
188
|
+
const model = build_schema_model({treedbs: [{}, {id: "db"}], topics: [], cols: []});
|
|
189
|
+
expect(model.treedbs.map(d => d.id)).toEqual(["db"]);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("writing helpers", () => {
|
|
194
|
+
test("fkey_ref composes what a new child must carry", () => {
|
|
195
|
+
expect(fkey_ref("topics", "db.users", "cols")).toBe("topics^db.users^cols");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("a new sibling goes AFTER the last one, not into the 9999 crowd", () => {
|
|
199
|
+
expect(next_order([{order: 1}, {order: 3}, {order: 2}])).toBe(4);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("siblings that all defaulted to 9999 still start a real numbering", () => {
|
|
203
|
+
expect(next_order([{order: DEFAULT_ORDER}, {order: DEFAULT_ORDER}])).toBe(1);
|
|
204
|
+
expect(next_order([])).toBe(1);
|
|
205
|
+
expect(next_order(null)).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("moved_orders writes only the rows whose place actually changed", () => {
|
|
209
|
+
const list = [
|
|
210
|
+
{id: "a", order: 2, record: {order: 2}},
|
|
211
|
+
{id: "b", order: 1, record: {order: 1}},
|
|
212
|
+
{id: "c", order: 3, record: {order: 3}},
|
|
213
|
+
];
|
|
214
|
+
/* `a` moved to the front: only a and b change, c keeps order 3. */
|
|
215
|
+
expect(moved_orders(list)).toEqual([{id: "a", order: 1}, {id: "b", order: 2}]);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a list already in order is no writes at all", () => {
|
|
219
|
+
const list = [
|
|
220
|
+
{id: "a", order: 1, record: {order: 1}},
|
|
221
|
+
{id: "b", order: 2, record: {order: 2}},
|
|
222
|
+
];
|
|
223
|
+
expect(moved_orders(list)).toEqual([]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("a list of 9999s is renumbered from 1", () => {
|
|
227
|
+
const list = [
|
|
228
|
+
{id: "a", order: DEFAULT_ORDER, record: {}},
|
|
229
|
+
{id: "b", order: DEFAULT_ORDER, record: {}},
|
|
230
|
+
];
|
|
231
|
+
expect(moved_orders(list)).toEqual([{id: "a", order: 1}, {id: "b", order: 2}]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/***********************************************************************
|
|
2
|
+
* schema_to_c.js
|
|
3
|
+
*
|
|
4
|
+
* The stored schema written back as the C literal it came from.
|
|
5
|
+
*
|
|
6
|
+
* A treedb's schema has two homes. It is DECLARED in C, as a
|
|
7
|
+
* `treedb_schema_*.c` string literal compiled into the yuno, and
|
|
8
|
+
* it is STORED in `treedb_system_schema`, which is what the
|
|
9
|
+
* operator edits here. The stored one wins at run time, so an
|
|
10
|
+
* edit made in this console works — and lives nowhere the next
|
|
11
|
+
* build knows about. Rebuild that yuno on a machine with an empty
|
|
12
|
+
* store and the column is gone.
|
|
13
|
+
*
|
|
14
|
+
* `diff-schema` says the two halves have drifted. This is the
|
|
15
|
+
* other half of that answer: the edit, in the form that can be
|
|
16
|
+
* pasted into the source, so the declaration catches up with what
|
|
17
|
+
* the node is actually running.
|
|
18
|
+
*
|
|
19
|
+
* Two outputs, because they answer different questions:
|
|
20
|
+
* schema_to_json() — the schema as a value: to diff, to keep, to
|
|
21
|
+
* feed back in.
|
|
22
|
+
* schema_to_c() — the same value as the literal: single
|
|
23
|
+
* quotes, `\n\` continuations, padded, ready
|
|
24
|
+
* to paste.
|
|
25
|
+
*
|
|
26
|
+
* Copyright (c) 2026, ArtGins.
|
|
27
|
+
* All Rights Reserved.
|
|
28
|
+
***********************************************************************/
|
|
29
|
+
import {
|
|
30
|
+
col_flags,
|
|
31
|
+
col_hook,
|
|
32
|
+
col_enum,
|
|
33
|
+
topic_pkey2s,
|
|
34
|
+
as_json,
|
|
35
|
+
} from "./schema_model.js";
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/* Column where the `\n\` continuation sits in the .c literals. Not a
|
|
39
|
+
* law — the files in the tree do not agree on it (68 in the app
|
|
40
|
+
* schemas, 56 in the meta one) — so it is an option with the most
|
|
41
|
+
* common value as its default. */
|
|
42
|
+
const DEFAULT_PAD = 68;
|
|
43
|
+
|
|
44
|
+
/* The order a topic declares itself in. The store has no order of its
|
|
45
|
+
* own (a JSON object), so without this the literal comes out shuffled
|
|
46
|
+
* and every export diffs against the last one. */
|
|
47
|
+
const TOPIC_KEY_ORDER = [
|
|
48
|
+
"id", "pkey", "pkey2s", "system_flag", "tkey",
|
|
49
|
+
"topic_version", "system_topic"
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/* Same for a column, in the order the .c literals write it. */
|
|
53
|
+
const COL_KEY_ORDER = [
|
|
54
|
+
"header", "fillspace", "type", "enum", "flag", "hook",
|
|
55
|
+
"default", "placeholder", "description", "template", "properties"
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
/* Written as quoted strings in the literals even though they are
|
|
59
|
+
* integer columns. Kept that way so an export can be pasted next to a
|
|
60
|
+
* hand-written schema without looking foreign. */
|
|
61
|
+
const VERSION_KEYS = ["schema_version", "topic_version"];
|
|
62
|
+
|
|
63
|
+
/* Storage-only: they say where the record lives, not what it declares. */
|
|
64
|
+
const COL_SKIP = ["id", "value", "topics", "order", "__md_treedb__"];
|
|
65
|
+
const TOPIC_SKIP = ["id", "value", "treedbs", "cols", "order", "__md_treedb__"];
|
|
66
|
+
|
|
67
|
+
/* Fields whose stored form may be the JSON text of the value. */
|
|
68
|
+
const JSON_FIELDS = ["enum", "hook", "default", "template", "properties", "pkey2s"];
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
/* Escaping a value crosses TWO layers, and the second one is not
|
|
72
|
+
* JSON's.
|
|
73
|
+
*
|
|
74
|
+
* The literal is a C string, so a backslash is `\\` and a quote is
|
|
75
|
+
* `\"` in the source. Then the yuno calls helper_quote2doublequote()
|
|
76
|
+
* on the whole block before parsing it (c_controlcenter.c and every
|
|
77
|
+
* other schema loader): EVERY single quote becomes a double one. That
|
|
78
|
+
* is what makes `'id'` legal JSON — and it means a single quote inside
|
|
79
|
+
* a VALUE cannot survive as itself, whatever it is delimited with.
|
|
80
|
+
*
|
|
81
|
+
* So a quote is written as the JSON escape `\u0027`, which that pass
|
|
82
|
+
* cannot see. Spelled out as constants because counting backslashes
|
|
83
|
+
* inside a regexp replacement is how the wrong number gets committed. */
|
|
84
|
+
const BACKSLASH = "\\";
|
|
85
|
+
const DQUOTE = "\"";
|
|
86
|
+
const C_BACKSLASH = BACKSLASH + BACKSLASH; /* \\ -> \ */
|
|
87
|
+
const JSON_BACKSLASH = C_BACKSLASH + C_BACKSLASH; /* \\\\ -> \\ -> \ */
|
|
88
|
+
const JSON_DQUOTE = C_BACKSLASH + BACKSLASH + DQUOTE; /* \\\" -> \" -> " */
|
|
89
|
+
const JSON_QUOTE = C_BACKSLASH + "u0027"; /* \\u0027 -> \u0027 -> ' */
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
/***************************************************************
|
|
93
|
+
* Order the keys of a plain object: the known ones first, in
|
|
94
|
+
* the declared order, the rest after in a stable one.
|
|
95
|
+
***************************************************************/
|
|
96
|
+
function ordered_entries(obj, order)
|
|
97
|
+
{
|
|
98
|
+
let entries = [];
|
|
99
|
+
let seen = {};
|
|
100
|
+
|
|
101
|
+
for(let key of order) {
|
|
102
|
+
if(Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
103
|
+
entries.push([key, obj[key]]);
|
|
104
|
+
seen[key] = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
let rest = Object.keys(obj).filter(k => !seen[k]);
|
|
108
|
+
rest.sort();
|
|
109
|
+
for(let key of rest) {
|
|
110
|
+
entries.push([key, obj[key]]);
|
|
111
|
+
}
|
|
112
|
+
return entries;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/***************************************************************
|
|
116
|
+
* schema_to_json(treedb) -> the schema as the .c literal holds it
|
|
117
|
+
*
|
|
118
|
+
* {id, schema_version, topics: [{id, pkey, ..., cols: {...}}]}
|
|
119
|
+
*
|
|
120
|
+
* `topics` is a LIST (the order is the schema's) and `cols` is a
|
|
121
|
+
* DICT keyed by column name (its order is the insertion order,
|
|
122
|
+
* which is the schema's too).
|
|
123
|
+
***************************************************************/
|
|
124
|
+
function schema_to_json(treedb)
|
|
125
|
+
{
|
|
126
|
+
if(!treedb) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* A version is an INTEGER column of the store and a QUOTED STRING
|
|
131
|
+
* in every .c literal in the tree. This output is the literal's
|
|
132
|
+
* shape, so it is the literal's spelling too — otherwise an export
|
|
133
|
+
* re-read is not equal to the export. */
|
|
134
|
+
let version = (v) => {
|
|
135
|
+
return (v === null || v === undefined) ? v : String(v);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
let out = {
|
|
139
|
+
id: treedb.id,
|
|
140
|
+
schema_version: version(treedb.schema_version),
|
|
141
|
+
topics: []
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
for(let topic of (treedb.topics || [])) {
|
|
145
|
+
let record = topic.record || {};
|
|
146
|
+
let jn_topic = {id: topic.name};
|
|
147
|
+
|
|
148
|
+
for(let [key, value] of Object.entries(record)) {
|
|
149
|
+
if(TOPIC_SKIP.indexOf(key) >= 0) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if(value === null || value === undefined || value === "") {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
jn_topic[key] = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
|
|
156
|
+
}
|
|
157
|
+
if(jn_topic.topic_version !== undefined) {
|
|
158
|
+
jn_topic.topic_version = version(jn_topic.topic_version);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let pkey2s = topic_pkey2s(record);
|
|
162
|
+
if(pkey2s.length === 1) {
|
|
163
|
+
jn_topic.pkey2s = pkey2s[0]; /* the literals write the single one bare */
|
|
164
|
+
} else if(pkey2s.length > 1) {
|
|
165
|
+
jn_topic.pkey2s = pkey2s;
|
|
166
|
+
} else {
|
|
167
|
+
delete jn_topic.pkey2s;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let cols = {};
|
|
171
|
+
for(let col of (topic.cols || [])) {
|
|
172
|
+
let col_record = col.record || {};
|
|
173
|
+
let jn_col = {};
|
|
174
|
+
for(let [key, value] of Object.entries(col_record)) {
|
|
175
|
+
if(COL_SKIP.indexOf(key) >= 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if(value === null || value === undefined || value === "") {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
jn_col[key] = JSON_FIELDS.indexOf(key) >= 0 ? as_json(value) : value;
|
|
182
|
+
}
|
|
183
|
+
let flags = col_flags(col_record);
|
|
184
|
+
if(flags.length > 0) {
|
|
185
|
+
jn_col.flag = flags;
|
|
186
|
+
} else {
|
|
187
|
+
delete jn_col.flag;
|
|
188
|
+
}
|
|
189
|
+
let hook = col_hook(col_record);
|
|
190
|
+
if(hook) {
|
|
191
|
+
jn_col.hook = hook;
|
|
192
|
+
}
|
|
193
|
+
let e = col_enum(col_record);
|
|
194
|
+
if(e.length > 0) {
|
|
195
|
+
jn_col.enum = e;
|
|
196
|
+
}
|
|
197
|
+
let ordered_col = {};
|
|
198
|
+
for(let [key, value] of ordered_entries(jn_col, COL_KEY_ORDER)) {
|
|
199
|
+
ordered_col[key] = value;
|
|
200
|
+
}
|
|
201
|
+
cols[col.name] = ordered_col;
|
|
202
|
+
}
|
|
203
|
+
jn_topic.cols = cols;
|
|
204
|
+
|
|
205
|
+
/* Re-created in the declared order: an object built by
|
|
206
|
+
* assignment keeps its insertion order, and the loop above ran
|
|
207
|
+
* in whatever order the record's keys happened to be in. */
|
|
208
|
+
let ordered = {};
|
|
209
|
+
for(let [key, value] of ordered_entries(jn_topic, TOPIC_KEY_ORDER)) {
|
|
210
|
+
if(key === "cols") {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
ordered[key] = value;
|
|
214
|
+
}
|
|
215
|
+
ordered.cols = jn_topic.cols;
|
|
216
|
+
out.topics.push(ordered);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/***************************************************************
|
|
223
|
+
* A scalar as the literal writes it. Single quotes, because the
|
|
224
|
+
* whole thing lives inside a C string delimited by double ones —
|
|
225
|
+
* a value that carries a single quote has to swap, and escape.
|
|
226
|
+
***************************************************************/
|
|
227
|
+
function c_scalar(value, key)
|
|
228
|
+
{
|
|
229
|
+
if(typeof value === "boolean") {
|
|
230
|
+
return value ? "true" : "false";
|
|
231
|
+
}
|
|
232
|
+
if(typeof value === "number") {
|
|
233
|
+
if(VERSION_KEYS.indexOf(key) >= 0) {
|
|
234
|
+
return `'${value}'`;
|
|
235
|
+
}
|
|
236
|
+
return String(value);
|
|
237
|
+
}
|
|
238
|
+
return `'${c_text(String(value))}'`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/***************************************************************
|
|
242
|
+
* The characters of a string that cannot be written as
|
|
243
|
+
* themselves.
|
|
244
|
+
***************************************************************/
|
|
245
|
+
function c_text(text)
|
|
246
|
+
{
|
|
247
|
+
return text
|
|
248
|
+
.replace(/\\/g, JSON_BACKSLASH)
|
|
249
|
+
.replace(/"/g, JSON_DQUOTE)
|
|
250
|
+
.replace(/'/g, JSON_QUOTE);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/***************************************************************
|
|
254
|
+
* Render a value at `indent`, appending its lines to `lines`.
|
|
255
|
+
* Collections always go multiline: that is how every schema in
|
|
256
|
+
* the tree is written, and a one-line array in the middle of
|
|
257
|
+
* them reads as a different file.
|
|
258
|
+
***************************************************************/
|
|
259
|
+
function c_value(lines, value, indent, key, trailing)
|
|
260
|
+
{
|
|
261
|
+
let pad = " ".repeat(indent);
|
|
262
|
+
let end = trailing ? "," : "";
|
|
263
|
+
|
|
264
|
+
if(Array.isArray(value)) {
|
|
265
|
+
lines.push(`${pad}[`);
|
|
266
|
+
for(let i = 0; i < value.length; i++) {
|
|
267
|
+
c_value(lines, value[i], indent + 4, "", i < value.length - 1);
|
|
268
|
+
}
|
|
269
|
+
lines.push(`${pad}]${end}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if(value && typeof value === "object") {
|
|
273
|
+
lines.push(`${pad}{`);
|
|
274
|
+
let entries = Object.entries(value);
|
|
275
|
+
for(let i = 0; i < entries.length; i++) {
|
|
276
|
+
c_pair(lines, entries[i][0], entries[i][1], indent + 4, i < entries.length - 1);
|
|
277
|
+
}
|
|
278
|
+
lines.push(`${pad}}${end}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
lines.push(`${pad}${c_scalar(value, key)}${end}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/***************************************************************
|
|
285
|
+
* Render `'key': value` at `indent`.
|
|
286
|
+
***************************************************************/
|
|
287
|
+
function c_pair(lines, key, value, indent, trailing)
|
|
288
|
+
{
|
|
289
|
+
let pad = " ".repeat(indent);
|
|
290
|
+
let end = trailing ? "," : "";
|
|
291
|
+
|
|
292
|
+
if(Array.isArray(value)) {
|
|
293
|
+
lines.push(`${pad}'${c_text(key)}': [`);
|
|
294
|
+
for(let i = 0; i < value.length; i++) {
|
|
295
|
+
c_value(lines, value[i], indent + 4, key, i < value.length - 1);
|
|
296
|
+
}
|
|
297
|
+
lines.push(`${pad}]${end}`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if(value && typeof value === "object") {
|
|
301
|
+
lines.push(`${pad}'${c_text(key)}': {`);
|
|
302
|
+
let entries = Object.entries(value);
|
|
303
|
+
for(let i = 0; i < entries.length; i++) {
|
|
304
|
+
c_pair(lines, entries[i][0], entries[i][1], indent + 4, i < entries.length - 1);
|
|
305
|
+
}
|
|
306
|
+
lines.push(`${pad}}${end}`);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
lines.push(`${pad}'${c_text(key)}': ${c_scalar(value, key)}${end}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/***************************************************************
|
|
313
|
+
* schema_to_c(treedb, options) -> the C source text
|
|
314
|
+
*
|
|
315
|
+
* options {var_name, pad}
|
|
316
|
+
* var_name the array's name; defaults to
|
|
317
|
+
* `treedb_schema_<treedb id without its prefix>`
|
|
318
|
+
* pad column of the `\n\` continuation
|
|
319
|
+
***************************************************************/
|
|
320
|
+
function schema_to_c(treedb, options)
|
|
321
|
+
{
|
|
322
|
+
let opts = options || {};
|
|
323
|
+
let json = schema_to_json(treedb);
|
|
324
|
+
|
|
325
|
+
if(!json) {
|
|
326
|
+
return "";
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let name = opts.var_name;
|
|
330
|
+
if(!name) {
|
|
331
|
+
let id = String(json.id || "schema");
|
|
332
|
+
name = id.indexOf("treedb_") === 0 ? `treedb_schema_${id.slice("treedb_".length)}`
|
|
333
|
+
: `treedb_schema_${id}`;
|
|
334
|
+
}
|
|
335
|
+
let pad = (typeof opts.pad === "number" && opts.pad > 0) ? opts.pad : DEFAULT_PAD;
|
|
336
|
+
|
|
337
|
+
let lines = [];
|
|
338
|
+
lines.push("{");
|
|
339
|
+
c_pair(lines, "id", json.id, 4, true);
|
|
340
|
+
c_pair(lines, "schema_version", json.schema_version, 4, true);
|
|
341
|
+
lines.push(" 'topics': [");
|
|
342
|
+
for(let i = 0; i < json.topics.length; i++) {
|
|
343
|
+
c_value(lines, json.topics[i], 8, "", i < json.topics.length - 1);
|
|
344
|
+
}
|
|
345
|
+
lines.push(" ]");
|
|
346
|
+
lines.push("}");
|
|
347
|
+
|
|
348
|
+
/* The continuation is what makes it a C literal: every line ends
|
|
349
|
+
* with `\n\`, padded to the same column so the block reads as a
|
|
350
|
+
* block. A line longer than the pad keeps one space — losing the
|
|
351
|
+
* separator would glue the `\n\` to the value. */
|
|
352
|
+
let body = lines.map((line) => {
|
|
353
|
+
let padding = line.length < pad ? " ".repeat(pad - line.length) : " ";
|
|
354
|
+
return `${line}${padding}\\n\\`;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return `static char ${name}[]= "\\\n${body.join("\n")}\n";\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
export {
|
|
362
|
+
DEFAULT_PAD,
|
|
363
|
+
TOPIC_KEY_ORDER,
|
|
364
|
+
COL_KEY_ORDER,
|
|
365
|
+
schema_to_json,
|
|
366
|
+
schema_to_c,
|
|
367
|
+
};
|