@milaboratories/pl-model-common 1.47.2 → 1.48.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/dist/bmodel/block_kind_ref.cjs +45 -0
- package/dist/bmodel/block_kind_ref.cjs.map +1 -0
- package/dist/bmodel/block_kind_ref.d.ts +57 -0
- package/dist/bmodel/block_kind_ref.d.ts.map +1 -0
- package/dist/bmodel/block_kind_ref.js +43 -0
- package/dist/bmodel/block_kind_ref.js.map +1 -0
- package/dist/bmodel/container.d.ts +8 -0
- package/dist/bmodel/container.d.ts.map +1 -1
- package/dist/bmodel/index.cjs +4 -0
- package/dist/bmodel/index.d.ts +2 -1
- package/dist/bmodel/index.js +2 -1
- package/dist/columns/dedup.cjs +1 -1
- package/dist/columns/dedup.cjs.map +1 -1
- package/dist/columns/dedup.d.ts +1 -1
- package/dist/columns/dedup.js +1 -1
- package/dist/columns/dedup.js.map +1 -1
- package/dist/columns/providers.cjs +1 -1
- package/dist/columns/providers.cjs.map +1 -1
- package/dist/columns/providers.d.ts +1 -1
- package/dist/columns/providers.js +1 -1
- package/dist/columns/providers.js.map +1 -1
- package/dist/drivers/index.cjs +4 -0
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/index.js +2 -2
- package/dist/drivers/pframe/data_types.cjs.map +1 -1
- package/dist/drivers/pframe/data_types.d.ts +24 -0
- package/dist/drivers/pframe/data_types.d.ts.map +1 -1
- package/dist/drivers/pframe/data_types.js.map +1 -1
- package/dist/drivers/pframe/index.cjs +4 -0
- package/dist/drivers/pframe/index.d.ts +2 -2
- package/dist/drivers/pframe/index.js +2 -2
- package/dist/drivers/pframe/spec/ids.cjs +151 -0
- package/dist/drivers/pframe/spec/ids.cjs.map +1 -1
- package/dist/drivers/pframe/spec/ids.d.ts +53 -1
- package/dist/drivers/pframe/spec/ids.d.ts.map +1 -1
- package/dist/drivers/pframe/spec/ids.js +150 -3
- package/dist/drivers/pframe/spec/ids.js.map +1 -1
- package/dist/drivers/pframe/spec/index.cjs +4 -0
- package/dist/drivers/pframe/spec/index.d.ts +2 -2
- package/dist/drivers/pframe/spec/index.js +2 -2
- package/dist/index.cjs +29 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -2
- package/dist/plid.cjs +1 -1
- package/dist/plid.cjs.map +1 -1
- package/dist/plid.d.ts +3 -2
- package/dist/plid.d.ts.map +1 -1
- package/dist/plid.js +1 -1
- package/dist/plid.js.map +1 -1
- package/dist/template/index.cjs +20 -0
- package/dist/template/index.d.ts +5 -0
- package/dist/template/index.js +5 -0
- package/dist/template/kind_selector.cjs +92 -0
- package/dist/template/kind_selector.cjs.map +1 -0
- package/dist/template/kind_selector.d.ts +80 -0
- package/dist/template/kind_selector.d.ts.map +1 -0
- package/dist/template/kind_selector.js +87 -0
- package/dist/template/kind_selector.js.map +1 -0
- package/dist/template/project_template_v1.cjs +231 -0
- package/dist/template/project_template_v1.cjs.map +1 -0
- package/dist/template/project_template_v1.d.ts +215 -0
- package/dist/template/project_template_v1.d.ts.map +1 -0
- package/dist/template/project_template_v1.js +225 -0
- package/dist/template/project_template_v1.js.map +1 -0
- package/dist/template/template_ref_form.cjs +73 -0
- package/dist/template/template_ref_form.cjs.map +1 -0
- package/dist/template/template_ref_form.d.ts +74 -0
- package/dist/template/template_ref_form.d.ts.map +1 -0
- package/dist/template/template_ref_form.js +72 -0
- package/dist/template/template_ref_form.js.map +1 -0
- package/dist/template/template_relocate.cjs +46 -0
- package/dist/template/template_relocate.cjs.map +1 -0
- package/dist/template/template_relocate.d.ts +32 -0
- package/dist/template/template_relocate.d.ts.map +1 -0
- package/dist/template/template_relocate.js +46 -0
- package/dist/template/template_relocate.js.map +1 -0
- package/package.json +5 -5
- package/src/bmodel/block_kind_ref.ts +59 -0
- package/src/bmodel/container.ts +9 -0
- package/src/bmodel/index.ts +1 -0
- package/src/columns/dedup.ts +1 -1
- package/src/columns/providers.ts +1 -1
- package/src/drivers/pframe/data_types.ts +24 -0
- package/src/drivers/pframe/spec/ids.test.ts +90 -0
- package/src/drivers/pframe/spec/ids.ts +191 -1
- package/src/index.ts +1 -0
- package/src/plid.ts +5 -5
- package/src/template/index.ts +4 -0
- package/src/template/kind_selector.ts +126 -0
- package/src/template/project_template_v1.test.ts +315 -0
- package/src/template/project_template_v1.ts +444 -0
- package/src/template/template_ref_form.test.ts +86 -0
- package/src/template/template_ref_form.ts +108 -0
- package/src/template/template_relocate.test.ts +182 -0
- package/src/template/template_relocate.ts +61 -0
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
} from "./filtered_column";
|
|
9
9
|
import {
|
|
10
10
|
createPObjectId,
|
|
11
|
+
isGlobalPObjectKey,
|
|
12
|
+
isLocalPObjectKey,
|
|
11
13
|
isPObjectId,
|
|
12
14
|
isPObjectKey,
|
|
13
15
|
LocalPObjectKey,
|
|
@@ -28,7 +30,7 @@ import {
|
|
|
28
30
|
type ColumnOverriddenId,
|
|
29
31
|
type ColumnOverriddenKey,
|
|
30
32
|
} from "./overridden";
|
|
31
|
-
import { canonicalizeJson } from "../../../json";
|
|
33
|
+
import { canonicalizeJson, parseJsonSafely } from "../../../json";
|
|
32
34
|
import { AxisSpec, PColumnSpec } from "./spec";
|
|
33
35
|
import { isString } from "es-toolkit";
|
|
34
36
|
|
|
@@ -97,6 +99,194 @@ export function parseColumnIdSafely(
|
|
|
97
99
|
}
|
|
98
100
|
}
|
|
99
101
|
|
|
102
|
+
/** Whether `value` is any of the five key forms a {@link ColumnUniversalId} serializes. */
|
|
103
|
+
export function isColumnUniversalKey(value: unknown): value is ColumnUniversalKey {
|
|
104
|
+
return (
|
|
105
|
+
isPObjectKey(value) ||
|
|
106
|
+
isColumnFilteredKey(value) ||
|
|
107
|
+
isColumnDiscoveredKey(value) ||
|
|
108
|
+
isColumnOverriddenKey(value)
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isColumnUniversalId(value: unknown): value is ColumnUniversalId {
|
|
113
|
+
const key = isString(value) ? parseJsonSafely(value, false) : false;
|
|
114
|
+
return key === false ? false : isColumnUniversalKey(key);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* A JSON string with its escape padding taken off, and how many passes that took.
|
|
119
|
+
*
|
|
120
|
+
* `layers` counts the `JSON.stringify` passes *above* the encoded value: a canonical id
|
|
121
|
+
* is `layers: 0`, the same id run through `JSON.stringify` once more is `layers: 1`. Keep
|
|
122
|
+
* it to put the value back the way it was found.
|
|
123
|
+
*/
|
|
124
|
+
export type PeeledJsonLayers = {
|
|
125
|
+
readonly value: unknown;
|
|
126
|
+
readonly layers: number;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Take a value out of however many `JSON.stringify` passes wrapped it, or `undefined`
|
|
131
|
+
* when `s` is not JSON at all.
|
|
132
|
+
*
|
|
133
|
+
* The one definition of "how a value can be hiding inside a string" — a block id can sit
|
|
134
|
+
* under several layers of escaping, and a walk over object properties reaches none of
|
|
135
|
+
* them. Callers differ in what they do at the bottom (this deliberately says nothing
|
|
136
|
+
* about which values count as identifiers), but they must agree on the mechanics, or
|
|
137
|
+
* "what carries a block id" ends up with two answers that drift.
|
|
138
|
+
*
|
|
139
|
+
* The gate is cheap and does NOT require any marker in the body: a filtered id whose
|
|
140
|
+
* innermost leaf is a {@link LocalPObjectKey} carries no `__isRef`, so demanding one
|
|
141
|
+
* would miss it.
|
|
142
|
+
*/
|
|
143
|
+
export function peelJsonLayers(s: string): PeeledJsonLayers | undefined {
|
|
144
|
+
let current = s;
|
|
145
|
+
let layers = 0;
|
|
146
|
+
for (;;) {
|
|
147
|
+
const c0 = current.charCodeAt(0);
|
|
148
|
+
if (c0 !== 0x7b /* { */ && c0 !== 0x22 /* " */) return undefined;
|
|
149
|
+
let parsed: unknown;
|
|
150
|
+
try {
|
|
151
|
+
parsed = JSON.parse(current);
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
// A pass that yielded another string was escape padding, so peel again. The string
|
|
156
|
+
// is strictly shorter each time, which is what bounds the loop.
|
|
157
|
+
if (isString(parsed)) {
|
|
158
|
+
if (parsed.length >= current.length) return undefined;
|
|
159
|
+
current = parsed;
|
|
160
|
+
layers++;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
return { value: parsed, layers };
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Rewrite every block id buried inside a column id.
|
|
169
|
+
*
|
|
170
|
+
* A {@link GlobalPObjectKey} leaf names its upstream by block id, and the wrapper key forms
|
|
171
|
+
* nest by *string* id rather than by object — so a block id can sit under several layers of
|
|
172
|
+
* JSON escaping, and `queriesQualifications` carries one in a map *key*. A caller that only
|
|
173
|
+
* walks object properties never reaches any of them, which is why moving a column id between
|
|
174
|
+
* projects needs this rather than a generic walk.
|
|
175
|
+
*
|
|
176
|
+
* Recursion re-canonicalizes bottom-up, so every level is canonical afterwards — including
|
|
177
|
+
* the rebuilt `queriesQualifications`, whose keys the canonical form sorts. That is the
|
|
178
|
+
* property a textual rewrite cannot have: redirecting an id that is a map key changes what
|
|
179
|
+
* the sorted order should be, and only rebuilding restores it.
|
|
180
|
+
*
|
|
181
|
+
* Returns the input itself when no block id changed, so a caller mapping ids to themselves
|
|
182
|
+
* gets its value back byte-for-byte and never re-serializes a stored id. Any `string` is
|
|
183
|
+
* accepted for the same reason: a caller sweeping a params object cannot know which of its
|
|
184
|
+
* strings are ids, and one that is not is returned as-is.
|
|
185
|
+
*
|
|
186
|
+
* @param remapBlockId old block id → new block id. Throw from it to reject an id that cannot
|
|
187
|
+
* be mapped.
|
|
188
|
+
*/
|
|
189
|
+
export function remapColumnIdBlockIds<T extends string | ColumnUniversalKey>(
|
|
190
|
+
id: T,
|
|
191
|
+
remapBlockId: (blockId: string) => string,
|
|
192
|
+
): T {
|
|
193
|
+
const remapped = isString(id) ? remapIdString(id, remapBlockId) : remapKey(id, remapBlockId);
|
|
194
|
+
return (remapped ?? id) as T;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The string half of {@link remapColumnIdBlockIds}. `undefined` means "nothing to change",
|
|
199
|
+
* which is what keeps an unaffected id from being re-serialized.
|
|
200
|
+
*
|
|
201
|
+
* Escape padding is peeled and put back, so an id that reached params through an extra
|
|
202
|
+
* `JSON.stringify` is rewritten in place and comes back wrapped as it was found. A string
|
|
203
|
+
* that does not peel to a column key is left alone: params hold ordinary strings too.
|
|
204
|
+
*/
|
|
205
|
+
function remapIdString(id: string, remapBlockId: (blockId: string) => string): string | undefined {
|
|
206
|
+
const peeled = peelJsonLayers(id);
|
|
207
|
+
if (peeled === undefined || !isColumnUniversalKey(peeled.value)) return undefined;
|
|
208
|
+
|
|
209
|
+
const remappedKey = remapKey(peeled.value, remapBlockId);
|
|
210
|
+
if (remappedKey === undefined) return undefined;
|
|
211
|
+
|
|
212
|
+
let rebuilt: string = stringifyColumnId(remappedKey);
|
|
213
|
+
for (let layer = 0; layer < peeled.layers; layer++) rebuilt = JSON.stringify(rebuilt);
|
|
214
|
+
return rebuilt;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** The key half of {@link remapColumnIdBlockIds}. `undefined` means "nothing to change". */
|
|
218
|
+
function remapKey(
|
|
219
|
+
key: ColumnUniversalKey,
|
|
220
|
+
remapBlockId: (blockId: string) => string,
|
|
221
|
+
): ColumnUniversalKey | undefined {
|
|
222
|
+
if (isGlobalPObjectKey(key)) {
|
|
223
|
+
const blockId = remapBlockId(key.blockId);
|
|
224
|
+
return blockId === key.blockId ? undefined : { ...key, blockId };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A local leaf names its column by a path inside its own block — no block id.
|
|
228
|
+
if (isLocalPObjectKey(key)) return undefined;
|
|
229
|
+
|
|
230
|
+
if (isColumnFilteredKey(key)) {
|
|
231
|
+
const source = remapIdString(key.source, remapBlockId);
|
|
232
|
+
return source === undefined ? undefined : { ...key, source: source as ColumnUniversalId };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isColumnOverriddenKey(key)) {
|
|
236
|
+
const source = remapIdString(key.source, remapBlockId);
|
|
237
|
+
// Remapping preserves the id's shape, so `source` is still not an Overridden id.
|
|
238
|
+
return source === undefined
|
|
239
|
+
? undefined
|
|
240
|
+
: { ...key, source: source as ColumnOverriddenKey["source"] };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isColumnDiscoveredKey(key)) return remapDiscoveredKey(key, remapBlockId);
|
|
244
|
+
|
|
245
|
+
throw new Error(
|
|
246
|
+
`remapColumnIdBlockIds: unrecognized column id structure: ${JSON.stringify(key)}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Discovered is the only key form carrying more than one nested id: the column it
|
|
252
|
+
* discovered, one per linker hop, and one per entry in `queriesQualifications` — where the
|
|
253
|
+
* id is the map key, not the value.
|
|
254
|
+
*/
|
|
255
|
+
function remapDiscoveredKey(
|
|
256
|
+
key: ColumnDiscoveredKey,
|
|
257
|
+
remapBlockId: (blockId: string) => string,
|
|
258
|
+
): ColumnDiscoveredKey | undefined {
|
|
259
|
+
const column = remapIdString(key.column, remapBlockId);
|
|
260
|
+
|
|
261
|
+
let pathChanged = false;
|
|
262
|
+
const path = key.path?.map((item) => {
|
|
263
|
+
const itemColumn = remapIdString(item.column, remapBlockId);
|
|
264
|
+
if (itemColumn === undefined) return item;
|
|
265
|
+
pathChanged = true;
|
|
266
|
+
return { ...item, column: itemColumn as ColumnUniversalId };
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
let queriesChanged = false;
|
|
270
|
+
const queriesQualifications =
|
|
271
|
+
key.queriesQualifications &&
|
|
272
|
+
(Object.fromEntries(
|
|
273
|
+
Object.entries(key.queriesQualifications).map(([queryId, qualifications]) => {
|
|
274
|
+
const remappedId = remapIdString(queryId, remapBlockId);
|
|
275
|
+
if (remappedId === undefined) return [queryId, qualifications];
|
|
276
|
+
queriesChanged = true;
|
|
277
|
+
return [remappedId, qualifications];
|
|
278
|
+
}),
|
|
279
|
+
) as ColumnDiscoveredKey["queriesQualifications"]);
|
|
280
|
+
|
|
281
|
+
if (column === undefined && !pathChanged && !queriesChanged) return undefined;
|
|
282
|
+
return {
|
|
283
|
+
...key,
|
|
284
|
+
...(column !== undefined ? { column: column as ColumnUniversalId } : {}),
|
|
285
|
+
...(pathChanged ? { path } : {}),
|
|
286
|
+
...(queriesChanged ? { queriesQualifications } : {}),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
100
290
|
/**
|
|
101
291
|
* Walk a rich column id down to its terminal leaf {@link PObjectId}.
|
|
102
292
|
*/
|
package/src/index.ts
CHANGED
package/src/plid.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { base32Encode } from "./base32_encode";
|
|
3
|
+
import { Branded } from "@milaboratories/helpers";
|
|
3
4
|
|
|
4
5
|
/** Number of raw bytes in the PlId. */
|
|
5
6
|
export const PlIdBytes = 15;
|
|
@@ -9,19 +10,18 @@ export const PlIdLength = 24; // = 15 bytes * 8 bits / 5 bits per char in base32
|
|
|
9
10
|
export const PlId = z
|
|
10
11
|
.string()
|
|
11
12
|
.length(PlIdLength)
|
|
12
|
-
.regex(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]/) // RFC4648
|
|
13
|
-
|
|
14
|
-
export type PlId = z.infer<typeof PlId>;
|
|
13
|
+
.regex(/[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]/); // RFC4648
|
|
14
|
+
export type PlId = Branded<z.infer<typeof PlId>, "PlId">;
|
|
15
15
|
|
|
16
16
|
export function uniquePlId(): PlId {
|
|
17
17
|
const data = new Uint8Array(PlIdBytes);
|
|
18
18
|
crypto.getRandomValues(data);
|
|
19
|
-
return PlId.parse(base32Encode(data, "RFC4648"));
|
|
19
|
+
return PlId.parse(base32Encode(data, "RFC4648")) as PlId;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
export function plId(bytes: Uint8Array): PlId {
|
|
23
23
|
if (bytes.length !== PlIdBytes) throw new Error(`Wrong number of bytes: ${bytes.length}`);
|
|
24
|
-
return PlId.parse(base32Encode(bytes, "RFC4648"));
|
|
24
|
+
return PlId.parse(base32Encode(bytes, "RFC4648")) as PlId;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export async function digestPlId(data: string): Promise<PlId> {
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Branded } from "@milaboratories/helpers";
|
|
2
|
+
import type { BlockKindReference } from "../bmodel/block_kind_ref";
|
|
3
|
+
import { parseKindRef, splitVersionedName } from "../bmodel/block_kind_ref";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Version-selection tier of a template entry's `kind` field.
|
|
7
|
+
*
|
|
8
|
+
* - `exact` — `X.Y.Z`: this version and no other.
|
|
9
|
+
* - `patch` — `~X.Y.Z`: patch floor, behavior frozen.
|
|
10
|
+
* - `minor` — `^X.Y.Z`: minor floor, behavior floats.
|
|
11
|
+
*/
|
|
12
|
+
export type KindSelectorOp = "exact" | "patch" | "minor";
|
|
13
|
+
|
|
14
|
+
/** The version half of a `{name}@{selector}` kind reference, split into parts. */
|
|
15
|
+
export type KindSelector = {
|
|
16
|
+
readonly op: KindSelectorOp;
|
|
17
|
+
readonly version: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* On-wire reference to a *set* of block kind versions: `{name}@{selector}`, e.g.
|
|
22
|
+
* `@platforma-open/milaboratories.mixcr-clonotyping.kind@~1.2.0`.
|
|
23
|
+
*
|
|
24
|
+
* The template-file form of a kind reference, and the only form the
|
|
25
|
+
* `template-v1` schema accepts in an entry's `kind` field. It is the same string
|
|
26
|
+
* shape as {@link BlockKindReference} widened by the `~`/`^` tiers, but branded
|
|
27
|
+
* separately so a *resolved* kind reference is never silently passed where a
|
|
28
|
+
* selector is expected, or vice versa. Widen an exact reference explicitly with
|
|
29
|
+
* {@link kindReferenceToSelectorReference}.
|
|
30
|
+
*/
|
|
31
|
+
export type BlockKindSelectorReference = Branded<string, "BlockKindSelectorReference">;
|
|
32
|
+
|
|
33
|
+
/** `X.Y.Z` with optional semver prerelease and build metadata. */
|
|
34
|
+
const semVerRegex =
|
|
35
|
+
/^\d+\.\d+\.\d+(?:-[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?(?:\+[\dA-Za-z-]+(?:\.[\dA-Za-z-]+)*)?$/;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Split a raw selector string (`1.2.0`, `~1.2.0`, `^1.2.0`) into its parts.
|
|
39
|
+
*
|
|
40
|
+
* The version is validated as `X.Y.Z`, so a range that is legal npm but not part
|
|
41
|
+
* of the kind grammar (`>=1.0.0`, `1.x`, `latest`) is rejected here rather than
|
|
42
|
+
* reaching resolution. Note the deliberate divergence from
|
|
43
|
+
* `tools/block-tools`'s `parseSelector`, which additionally tolerates a leading
|
|
44
|
+
* `@` as `exact`: after the `{name}@{selector}` split a leading `@` can only
|
|
45
|
+
* come from a doubled separator, which is malformed.
|
|
46
|
+
*
|
|
47
|
+
* Mapping a selector onto a concrete version is resolution, not parsing, and
|
|
48
|
+
* lives with the resolver (`kind_resolver.selectorToRange`).
|
|
49
|
+
*
|
|
50
|
+
* @throws if the version part is not `X.Y.Z`
|
|
51
|
+
*/
|
|
52
|
+
export function parseKindSelector(raw: string): KindSelector {
|
|
53
|
+
const s = raw.trim();
|
|
54
|
+
const op: KindSelectorOp = s.startsWith("~") ? "patch" : s.startsWith("^") ? "minor" : "exact";
|
|
55
|
+
const version = op === "exact" ? s : s.slice(1);
|
|
56
|
+
if (!semVerRegex.test(version)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`Malformed kind version selector (expected 'X.Y.Z', '~X.Y.Z' or '^X.Y.Z'): ${raw}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return { op, version };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Render a {@link KindSelector} back to its on-wire string. */
|
|
65
|
+
export function formatKindSelector(sel: KindSelector): string {
|
|
66
|
+
switch (sel.op) {
|
|
67
|
+
case "exact":
|
|
68
|
+
return sel.version;
|
|
69
|
+
case "patch":
|
|
70
|
+
return `~${sel.version}`;
|
|
71
|
+
case "minor":
|
|
72
|
+
return `^${sel.version}`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Split a {@link BlockKindSelectorReference} into `{ name, selector }`.
|
|
78
|
+
*
|
|
79
|
+
* @throws if the reference carries no version segment, or the selector is
|
|
80
|
+
* outside the `X.Y.Z` / `~X.Y.Z` / `^X.Y.Z` grammar
|
|
81
|
+
*/
|
|
82
|
+
export function parseKindSelectorReference(ref: BlockKindSelectorReference): {
|
|
83
|
+
name: string;
|
|
84
|
+
selector: KindSelector;
|
|
85
|
+
} {
|
|
86
|
+
const { name, version } = splitVersionedName(ref, "kind selector reference", "{name}@{selector}");
|
|
87
|
+
return { name, selector: parseKindSelector(version) };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compose a {@link BlockKindSelectorReference} from a name and selector.
|
|
92
|
+
*
|
|
93
|
+
* A formatter, not a validator — pass a selector that came from
|
|
94
|
+
* {@link parseKindSelector} or that you constructed from a known-good version.
|
|
95
|
+
*/
|
|
96
|
+
export function formatKindSelectorReference(k: {
|
|
97
|
+
name: string;
|
|
98
|
+
selector: KindSelector;
|
|
99
|
+
}): BlockKindSelectorReference {
|
|
100
|
+
return `${k.name}@${formatKindSelector(k.selector)}` as BlockKindSelectorReference;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Widen a resolved {@link BlockKindReference} to its `exact`-tier selector form.
|
|
105
|
+
*
|
|
106
|
+
* The export direction: a block implements exactly one kind version, so export
|
|
107
|
+
* always emits `{name}@X.Y.Z`. Validates on the way through, so a
|
|
108
|
+
* malformed stored reference fails at the boundary rather than in the file.
|
|
109
|
+
*/
|
|
110
|
+
export function kindReferenceToSelectorReference(
|
|
111
|
+
ref: BlockKindReference,
|
|
112
|
+
): BlockKindSelectorReference {
|
|
113
|
+
const { name, version } = parseKindRef(ref);
|
|
114
|
+
return formatKindSelectorReference({ name, selector: parseKindSelector(version) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether `value` is a well-formed `{name}@{selector}` string. */
|
|
118
|
+
export function isBlockKindSelectorReference(value: unknown): value is BlockKindSelectorReference {
|
|
119
|
+
if (typeof value !== "string") return false;
|
|
120
|
+
try {
|
|
121
|
+
parseKindSelectorReference(value as BlockKindSelectorReference);
|
|
122
|
+
return true;
|
|
123
|
+
} catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|