@k8ts/metadata 0.7.3 → 0.10.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/src/meta.ts CHANGED
@@ -2,13 +2,12 @@ import { seq } from "doddle"
2
2
  import { MetadataError } from "./error"
3
3
  import type { InputMeta, MetaInputParts } from "./input/dict-input"
4
4
  import { parseKey, parseMetaInput } from "./key"
5
- import { pNameValue } from "./key/parse-key"
6
- import { checkMetaString, ValueKey, type SectionKey } from "./key/repr"
5
+ import { parseInnerKey, parseSectionKey, pNameValue } from "./key/parse-key"
6
+ import { checkMetaString, MetadataKey, type DomainPrefix } from "./key/repr"
7
7
  import { Key } from "./key/types"
8
8
  import { orderMetaKeyedObject } from "./order-meta-keyed-object"
9
9
  import { equalsMap, toJS } from "./util"
10
10
  export type Meta = Meta.Meta
11
- export type MutableMeta = Meta.MutableMeta
12
11
  const MetaMarker = Symbol("k8ts.org/metadata")
13
12
  export interface MetaLike {
14
13
  readonly [MetaMarker]: true
@@ -22,7 +21,7 @@ export namespace Meta {
22
21
  }
23
22
  export function _checkValue(key: string, v: string) {
24
23
  const parsed = parseKey(key)
25
- if (!(parsed instanceof ValueKey)) {
24
+ if (!(parsed instanceof MetadataKey)) {
26
25
  throw new MetadataError(`Expected value key, got section key for ${key}`)
27
26
  }
28
27
  if (parsed.metaType === "label") {
@@ -32,37 +31,151 @@ export namespace Meta {
32
31
  }
33
32
  }
34
33
  export type Input = InputMeta
35
- export class Meta implements Iterable<[ValueKey, string]>, MetaLike {
34
+ /**
35
+ * Mutable storage for k8s metadata. K8s metadata includes labels, annotations, and core fields.
36
+ * These are addressed using `CharPrefix`.
37
+ *
38
+ * - **Labels**: `%key` → `metadata.labels.key`
39
+ * - **Annotations**: `^key` → `metadata.annotations.key`
40
+ * - **Comments**: `#key` → Build-time metadata, not manifested in k8s objects.
41
+ * - **Core metadata**: `key` → `metadata.key` (e.g., `name`, `namespace`, etc.)
42
+ *
43
+ * In addition, you can address different metadata keys under the same domain using a
44
+ * `DomainKey`, of the form `example.com/`. When using a `DomainKey`, you use a `CharPrefix` on
45
+ * the inner keys.
46
+ *
47
+ * This lets you add different kinds of metadata under the same domain with ease.
48
+ *
49
+ * @example
50
+ * meta.add("%app", "my-app") // adds label 'app' with value 'my-app'
51
+ * meta.add("example.section/", {
52
+ * "%label1": "value1", // adds `%example.section/label1` with value 'value1'
53
+ * "^annotation1": "value2" // adds `^example.section/annotation1` with value 'value2'
54
+ * })
55
+ */
56
+ export class Meta implements Iterable<[MetadataKey, string]>, MetaLike {
36
57
  readonly [MetaMarker] = true
58
+ /**
59
+ * Constructs a new Meta instance from a map of key-value pairs. Validates all keys and
60
+ * values during construction.
61
+ *
62
+ * @param _dict Internal map storing metadata key-value pairs
63
+ * @throws {MetadataError} If any key or value is invalid
64
+ */
37
65
  constructor(private readonly _dict: Map<string, string>) {
38
66
  for (const [key, value] of _dict.entries()) {
39
67
  _checkValue(key, value)
40
68
  }
41
69
  }
42
70
 
71
+ /**
72
+ * Makes Meta instances iterable, yielding [ValueKey, string] pairs.
73
+ *
74
+ * @example
75
+ * for (const [key, value] of meta) {
76
+ * console.log(key.str, value)
77
+ * }
78
+ */
43
79
  *[Symbol.iterator]() {
44
80
  for (const entry of this._dict.entries()) {
45
- yield [parseKey(entry[0]) as ValueKey, entry[1]] as [ValueKey, string]
81
+ yield [parseKey(entry[0]) as MetadataKey, entry[1]] as [MetadataKey, string]
46
82
  }
47
83
  }
48
84
  protected _create(raw: Map<string, string>) {
49
85
  return new Meta(raw)
50
86
  }
87
+ /**
88
+ * Creates a deep clone of this object.
89
+ *
90
+ * @returns
91
+ */
51
92
  clone() {
52
93
  return this._create(new Map(this._dict))
53
94
  }
54
95
 
96
+ /**
97
+ * Deletes a single value key from the metadata.
98
+ *
99
+ * @example
100
+ * meta.delete("name") // deletes core metadata 'name'
101
+ * meta.delete("%app") // deletes label 'app'
102
+ *
103
+ * @param key The value key to delete
104
+ */
55
105
  delete(key: Key.Value): Meta
56
- delete(ns: Key.Section, key: string): Meta
57
- delete(ns: Key.Section): Meta
58
- delete(a: any, b?: any) {
59
- const parsed = parseKey(a)
60
- this._dict.delete(parsed.str)
106
+
107
+ /**
108
+ * Deletes specific keys under a domain prefix.
109
+ *
110
+ * @example
111
+ * meta.delete("example.com/", "key1", "key2") // deletes specific keys in section
112
+ *
113
+ * @param ns The section key namespace
114
+ * @param keys Specific value keys within the section to delete
115
+ */
116
+ delete(ns: Key.Domain, ...keys: Key.Value[]): Meta
117
+
118
+ /**
119
+ * Deletes all keys within a section namespace.
120
+ *
121
+ * @example
122
+ * meta.delete("example.com/") // deletes all keys in section
123
+ *
124
+ * @param ns The section key namespace to delete
125
+ */
126
+ delete(ns: Key.Domain): Meta
127
+ delete(a: any, ...rest: any[]) {
128
+ if (a.endsWith("/")) {
129
+ const sectionKey = parseSectionKey(a)
130
+ const onlyKeys = rest.map(parseInnerKey)
131
+ if (onlyKeys.length > 0) {
132
+ var deleteOnly = (key: MetadataKey) => onlyKeys.some(ok => ok.equals(key))
133
+ } else {
134
+ var deleteOnly = (_key: MetadataKey) => _key.domain().equals(sectionKey)
135
+ }
136
+ for (const k of this._keys) {
137
+ if (deleteOnly(k)) {
138
+ this._dict.delete(k.str)
139
+ }
140
+ }
141
+ } else {
142
+ this._dict.delete(a)
143
+ }
61
144
  return this
62
145
  }
63
146
 
147
+ /**
148
+ * Adds a single key-value pair to the metadata. Throws if the key already exists.
149
+ *
150
+ * @example
151
+ * meta.add("%app", "my-app") // adds label
152
+ * meta.add("name", "my-resource") // adds core metadata
153
+ *
154
+ * @param key The value key to add
155
+ * @param value The value to associate with the key
156
+ */
64
157
  add(key: Key.Value, value?: string): Meta
65
- add(key: Key.Section, value: MetaInputParts.Nested): Meta
158
+
159
+ /**
160
+ * Adds a nested object of key-value pairs within a section namespace. Throws if any key
161
+ * already exists.
162
+ *
163
+ * @example
164
+ * meta.add("example.com/", { "%label": "value", "^annotation": "data" })
165
+ *
166
+ * @param key The section key namespace
167
+ * @param value Nested object containing key-value pairs
168
+ */
169
+ add(key: Key.Domain, value: MetaInputParts.Nested): Meta
170
+
171
+ /**
172
+ * Adds multiple key-value pairs from an input object. Throws if any key already exists.
173
+ *
174
+ * @example
175
+ * meta.add({ "%app": "my-app", name: "my-resource" })
176
+ *
177
+ * @param input Object or map containing key-value pairs to add
178
+ */
66
179
  add(input: InputMeta): Meta
67
180
  add(a: any, b?: any) {
68
181
  const parsed = _pairToMap([a, b])
@@ -78,17 +191,47 @@ export namespace Meta {
78
191
  return this
79
192
  }
80
193
 
194
+ /**
195
+ * Compares this Meta instance to another for equality. Two instances are equal if they
196
+ * contain the same key-value pairs.
197
+ *
198
+ * @param other The other Meta instance or input to compare against
199
+ * @returns Whether the two Meta instances are equal
200
+ */
81
201
  equals(other: Meta.Input) {
82
202
  return equalsMap(this._dict, make(other)._dict)
83
203
  }
84
204
 
85
- section(key: string) {
86
- const newS = seq(this).map(([k, v]) => [k.section(key), v] as const)
87
- return parseMetaInput(newS)
88
- }
89
-
205
+ /**
206
+ * Overwrites a single key-value pair, replacing any existing value.
207
+ *
208
+ * @example
209
+ * meta.overwrite("%app", "new-app") // replaces existing label value
210
+ *
211
+ * @param key The value key to overwrite
212
+ * @param value The new value (undefined removes the key)
213
+ */
90
214
  overwrite(key: Key.Value, value: string | undefined): Meta
91
- overwrite(key: Key.Section, value: MetaInputParts.Nested): Meta
215
+
216
+ /**
217
+ * Overwrites key-value pairs within a section namespace.
218
+ *
219
+ * @example
220
+ * meta.overwrite("example.com/", { "%label": "new-value" })
221
+ *
222
+ * @param key The section key namespace
223
+ * @param value Nested object containing key-value pairs to overwrite
224
+ */
225
+ overwrite(key: Key.Domain, value: MetaInputParts.Nested): Meta
226
+
227
+ /**
228
+ * Overwrites multiple key-value pairs from an input object.
229
+ *
230
+ * @example
231
+ * meta.overwrite({ "%app": "new-app", name: "new-name" })
232
+ *
233
+ * @param input Object or map containing key-value pairs to overwrite
234
+ */
92
235
  overwrite(input?: InputMeta): Meta
93
236
  overwrite(a?: any, b?: any) {
94
237
  if (a === undefined) {
@@ -101,15 +244,38 @@ export namespace Meta {
101
244
  return this
102
245
  }
103
246
 
104
- has<X extends Key.Value>(key: X) {
247
+ /**
248
+ * Checks if a key with a given domain prefix exists in the metadata.
249
+ *
250
+ * @example
251
+ * meta.has("example.com/") // Checks for any key with this domain
252
+ * meta.has("%app") // Checks for the label 'app'
253
+ *
254
+ * @param domainPrefix The domain prefix to check for
255
+ * @returns True if any keys exist under the specified domain prefix, false otherwise
256
+ */
257
+ has(domainPrefix: Key.Domain): boolean
258
+ /** @param key */
259
+ has(key: Key.Value): boolean
260
+ has(key: any) {
105
261
  const parsed = parseKey(key)
106
- if (parsed instanceof ValueKey) {
262
+ if (parsed instanceof MetadataKey) {
107
263
  return this._dict.has(key)
108
264
  } else {
109
- return this._matchSectionKeys(parsed).size > 0
265
+ return this._matchDomainPrefixes(parsed).size > 0
110
266
  }
111
267
  }
112
268
 
269
+ /**
270
+ * Retrieves the value for the specified key. Throws if the key doesn't exist.
271
+ *
272
+ * @example
273
+ * const appName = meta.get("%app")
274
+ *
275
+ * @param key The value key to retrieve
276
+ * @returns The value associated with the key
277
+ * @throws {MetadataError} If the key is not found
278
+ */
113
279
  get(key: Key.Value) {
114
280
  const parsed = parseKey(key)
115
281
  const v = this._dict.get(key)
@@ -119,29 +285,50 @@ export namespace Meta {
119
285
  return v
120
286
  }
121
287
 
288
+ /**
289
+ * Attempts to retrieve the value for the specified key, returning a fallback if not found.
290
+ *
291
+ * @example
292
+ * const appName = meta.tryGet("%app", "default-app")
293
+ *
294
+ * @param key The value key to retrieve
295
+ * @param fallback Optional fallback value if key doesn't exist
296
+ * @returns The value associated with the key, or the fallback value
297
+ * @throws {MetadataError} If a domain key is provided instead of a value key
298
+ */
122
299
  tryGet(key: Key.Value, fallback?: string) {
123
300
  const parsed = parseKey(key)
124
- if (!(parsed instanceof ValueKey)) {
125
- throw new MetadataError("Unexpected section key!", { key })
301
+ if (!(parsed instanceof MetadataKey)) {
302
+ throw new MetadataError("Unexpected domain key!", { key })
126
303
  }
127
304
  return this._dict.get(key) ?? fallback
128
305
  }
129
306
 
130
- private _matchSectionKeys(key: SectionKey) {
307
+ private _matchDomainPrefixes(key: DomainPrefix) {
131
308
  return seq(this)
132
309
  .filter(([k, v]) => k.parent?.equals(key) ?? false)
133
310
  .toMap(x => [x[0].str, x[1]] as const)
134
311
  .pull()
135
312
  }
136
313
 
137
- pick(...keySpecs: Key.Key[]) {
314
+ /**
315
+ * Creates a new Meta instance containing only some of the keys. You can pass both entire
316
+ * keys and domain prefixes to include all keys under that domain.
317
+ *
318
+ * @example
319
+ * const subset = meta.pick("%app", "name", "example.com/")
320
+ *
321
+ * @param keySpecs Keys or domain prefixes to include in the result
322
+ * @returns A new Meta instance with only the picked keys
323
+ */
324
+ pick(...keySpecs: (Key.Domain | Key.Value)[]) {
138
325
  const parsed = keySpecs.map(parseKey)
139
326
  const keyStrSet = new Set<string>()
140
327
  for (const key of parsed) {
141
- if (key instanceof ValueKey) {
328
+ if (key instanceof MetadataKey) {
142
329
  keyStrSet.add(key.str)
143
330
  } else {
144
- const sectionKeys = this._matchSectionKeys(key)
331
+ const sectionKeys = this._matchDomainPrefixes(key)
145
332
  for (const k of sectionKeys.keys()) {
146
333
  keyStrSet.add(k)
147
334
  }
@@ -157,87 +344,131 @@ export namespace Meta {
157
344
  private _prefixed(prefix: string) {
158
345
  const out: { [k: string]: string } = {}
159
346
  for (const [k, v] of this) {
160
- if (k._prefix === prefix) {
347
+ if (k.prefix() === prefix) {
161
348
  out[k.suffix] = v
162
349
  }
163
350
  }
164
351
  return orderMetaKeyedObject(out)
165
352
  }
166
353
 
354
+ /**
355
+ * Returns all labels as a plain object that can be embedded into a k8s manifest, with keys
356
+ * in canonical order.
357
+ *
358
+ * @example
359
+ * const labels = Meta.make({
360
+ * "%app": "my-app",
361
+ * "%tier": "backend"
362
+ * }).labels
363
+ * // { app: "my-app", tier: "backend" }
364
+ */
167
365
  get labels() {
168
366
  return this._prefixed("%")
169
367
  }
170
368
 
369
+ /**
370
+ * Returns all annotations as a plain object that can be embedded into a k8s manifest, with
371
+ * keys in canonical order.
372
+ *
373
+ * @example
374
+ * const annotations = Meta.make({
375
+ * "^note": "This is important",
376
+ * "^description": "Detailed info"
377
+ * }).annotations
378
+ * // { note: "This is important", description: "Detailed info" }
379
+ */
171
380
  get annotations() {
172
381
  return this._prefixed("^")
173
382
  }
174
383
 
384
+ /**
385
+ * Returns all comments (build-time metadata) as a plain object, with keys in canonical
386
+ * order.
387
+ *
388
+ * @example
389
+ * const comments = Meta.make({
390
+ * "#note": "Internal use only"
391
+ * }).comments
392
+ * // { note: "Internal use only" }
393
+ */
175
394
  get comments() {
176
395
  return this._prefixed("#")
177
396
  }
397
+ /**
398
+ * Returns all core metadata fields (`name` and `namespace`) as a plain object that can be
399
+ * embedded into a k8s manifest, with keys in canonical order.
400
+ *
401
+ * @example
402
+ * const core = meta.core // { name: "my-resource", namespace: "default" }
403
+ */
404
+ get core() {
405
+ return this._prefixed("") as {
406
+ [key in Key.Special]?: string
407
+ }
408
+ }
178
409
 
410
+ /**
411
+ * Returns all metadata key-value pairs as a flat JavaScript object, with each key prefixed
412
+ * appropriately.
413
+ *
414
+ * @example
415
+ * const all = Meta.make({
416
+ * "%app": "my-app",
417
+ * "^note": "This is important",
418
+ * name: "my-resource"
419
+ * }).values
420
+ * // { "%app": "my-app", "^note": "This is important", "name": "my-resource" }
421
+ */
179
422
  get values() {
180
423
  return toJS(this._dict)
181
424
  }
182
425
 
183
- get keys(): ValueKey[] {
426
+ private get _keys(): MetadataKey[] {
184
427
  return seq(this)
185
428
  .map(([k, v]) => k)
186
429
  .toArray()
187
430
  .pull()
188
431
  }
189
-
190
- get core() {
191
- return this._prefixed("") as {
192
- [key in Key.Special]?: string
193
- }
194
- }
195
- remove(key: Key.Value): this
196
- remove(ns: Key.Section, key: string): this
197
- remove(ns: Key.Section): this
198
- remove(a: any, b?: any) {
199
- const parsed = parseKey(a)
200
- if (parsed instanceof ValueKey) {
201
- this._dict.delete(parsed.str)
202
- return this
203
- }
204
- if (b !== undefined) {
205
- // remove specific key from section
206
- for (const k of this.keys) {
207
- if (k.parent?.equals(parsed) && k.suffix === b) {
208
- this._dict.delete(k.str)
209
- }
210
- }
211
- return this
212
- }
213
- // remove entire section
214
- for (const k of this.keys) {
215
- if (k.parent?.equals(parsed)) this._dict.delete(k.str)
216
- }
217
- return this
218
- }
219
-
220
- expand() {
221
- const labels = this.labels
222
- const annotations = this.annotations
223
- const core = this.core
224
- return {
225
- ...core,
226
- labels,
227
- annotations
228
- }
229
- }
230
432
  }
231
433
 
434
+ /**
435
+ * Creates a Meta instance with a single key-value pair.
436
+ *
437
+ * @example
438
+ * const meta = Meta.make("%app", "my-app")
439
+ *
440
+ * @param key The value key
441
+ * @param value The value to associate with the key
442
+ */
232
443
  export function make(key: Key.Value, value: string): Meta
233
- export function make(key: Key.Section, value: MetaInputParts.Nested): Meta
444
+
445
+ /**
446
+ * Creates a Meta instance with key-value pairs within a section namespace.
447
+ *
448
+ * @example
449
+ * const meta = Meta.make("example.com/", { "%label": "value" })
450
+ *
451
+ * @param key The section key namespace
452
+ * @param value Nested object containing key-value pairs
453
+ */
454
+ export function make(key: Key.Domain, value: MetaInputParts.Nested): Meta
455
+
456
+ /**
457
+ * Creates a Meta instance from an input object or returns an empty Meta if no input provided.
458
+ *
459
+ * @example
460
+ * const meta = Meta.make({ "%app": "my-app", name: "resource" })
461
+ * const empty = Meta.make()
462
+ *
463
+ * @param input Object or map containing key-value pairs
464
+ */
234
465
  export function make(input?: InputMeta): Meta
235
466
  export function make(a?: any, b?: any) {
236
467
  return new Meta(_pairToMap([a, b]))
237
468
  }
238
- function _pairToObject(pair: [string | ValueKey, string | object] | [object]) {
469
+ function _pairToObject(pair: [string | MetadataKey, string | object] | [object]) {
239
470
  let [key, value] = pair
240
- key = key instanceof ValueKey ? key.str : key
471
+ key = key instanceof MetadataKey ? key.str : key
241
472
  if (typeof key === "string") {
242
473
  return {
243
474
  [key]: value as string
@@ -245,27 +476,7 @@ export namespace Meta {
245
476
  }
246
477
  return key
247
478
  }
248
- function _pairToMap(pair: [string | ValueKey, string | object] | [object]) {
479
+ function _pairToMap(pair: [string | MetadataKey, string | object] | [object]) {
249
480
  return parseMetaInput(_pairToObject(pair))
250
481
  }
251
-
252
- export function splat(...input: InputMeta[]) {
253
- return input.map(make).reduce((acc, meta) => acc.add(meta), make())
254
- }
255
-
256
- export function is(value: any): value is Meta {
257
- return value instanceof Meta
258
- }
259
-
260
- export class MutableMeta extends Meta {
261
- // MutableMeta is now just a direct extension of Meta so callers
262
- // that expect MutableMeta keep working. The class body is empty
263
- // because all mutation behavior lives on `Meta` itself.
264
- constructor(...args: any[]) {
265
- // Construct as a Meta instance. `Meta` constructor signature
266
- // expects a Map<ValueKey, string> which callers provide when
267
- // creating mutable instances.
268
- super(args[0])
269
- }
270
- }
271
482
  }