@bcts/dcbor 1.0.0-alpha.12 → 1.0.0-alpha.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bcts/dcbor",
3
- "version": "1.0.0-alpha.12",
3
+ "version": "1.0.0-alpha.13",
4
4
  "type": "module",
5
5
  "description": "Blockchain Commons Deterministic CBOR (dCBOR) for TypeScript",
6
6
  "license": "BSD-2-Clause-Patent",
@@ -47,8 +47,6 @@
47
47
  "dev": "tsdown --watch",
48
48
  "test": "vitest run",
49
49
  "test:watch": "vitest",
50
- "test:cli": "vitest run tests/cli.test.ts",
51
- "test:examples": "npm run build && node scripts/test-examples.js",
52
50
  "lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
53
51
  "lint:fix": "eslint 'src/**/*.ts' 'tests/**/*.ts' --fix",
54
52
  "typecheck": "tsc --noEmit",
package/src/cbor.ts CHANGED
@@ -250,7 +250,7 @@ export interface CborMethods {
250
250
  * @param initialState - Initial state for the visitor
251
251
  * @param visitor - Visitor function called for each element
252
252
  */
253
- walk<State>(initialState: State, visitor: Visitor<State>): State;
253
+ walk<State>(initialState: State, visitor: Visitor<State>): void;
254
254
  /**
255
255
  * Validate that value has one of the expected tags.
256
256
  * @param expectedTags - Array of expected tag values
@@ -774,8 +774,8 @@ export const attachMethods = <T extends Omit<Cbor, keyof CborMethods>>(obj: T):
774
774
  },
775
775
 
776
776
  // Advanced operations
777
- walk<State>(this: Cbor, initialState: State, visitor: Visitor<State>): State {
778
- return walk(this, initialState, visitor);
777
+ walk<State>(this: Cbor, initialState: State, visitor: Visitor<State>): void {
778
+ walk(this, initialState, visitor);
779
779
  },
780
780
  validateTag(this: Cbor, expectedTags: Tag[]): Tag {
781
781
  if (this.type !== MajorType.Tagged) {
package/src/diag.ts CHANGED
@@ -376,14 +376,29 @@ function formatMap(map: CborMap, opts: DiagFormatOpts): string {
376
376
 
377
377
  /**
378
378
  * Format tagged value.
379
+ *
380
+ * Matches Rust's diag_item() for Tagged case.
379
381
  */
380
382
  function formatTagged(tag: number | bigint, content: Cbor, opts: DiagFormatOpts): string {
381
- // Check for summarizer first
383
+ // Check for summarizer first (matches Rust's summarization logic)
382
384
  if (opts.summarize === true) {
383
385
  const store = resolveTagsStore(opts.tags);
384
386
  const summarizer = store?.summarizer(tag);
385
387
  if (summarizer !== undefined) {
386
- return summarizer(content, opts.flat ?? false);
388
+ // Call summarizer and handle Result type (matching Rust's match on Result)
389
+ const result = summarizer(content, opts.flat ?? false);
390
+ if (result.ok) {
391
+ return result.value;
392
+ } else {
393
+ // Match Rust's error format: "<error: {}>"
394
+ const errorMsg =
395
+ result.error.type === "Custom"
396
+ ? result.error.message
397
+ : result.error.type === "WrongTag"
398
+ ? `expected CBOR tag ${result.error.expected.value}, but got ${result.error.actual.value}`
399
+ : result.error.type;
400
+ return `<error: ${errorMsg}>`;
401
+ }
387
402
  }
388
403
  }
389
404
 
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export {
52
52
  TagsStore,
53
53
  type TagsStoreTrait,
54
54
  type CborSummarizer,
55
+ type SummarizerResult,
55
56
  getGlobalTagsStore,
56
57
  } from "./tags-store";
57
58
  export * from "./tags";
@@ -72,15 +73,10 @@ export {
72
73
  type EdgeTypeVariant,
73
74
  type WalkElement,
74
75
  type Visitor,
76
+ walk,
75
77
  asSingle,
76
78
  asKeyValue,
77
79
  edgeLabel,
78
- // Helper functions
79
- countElements,
80
- collectAtLevel,
81
- findFirst,
82
- collectAllText,
83
- maxDepth,
84
80
  } from "./walk";
85
81
 
86
82
  // Codable interfaces
package/src/tags-store.ts CHANGED
@@ -9,17 +9,26 @@
9
9
 
10
10
  import type { Cbor, CborNumber } from "./cbor";
11
11
  import type { Tag } from "./tag";
12
+ import type { Error as CborErrorType } from "./error";
13
+
14
+ /**
15
+ * Result type for summarizer functions, matching Rust's Result<String, Error>.
16
+ */
17
+ export type SummarizerResult =
18
+ | { readonly ok: true; readonly value: string }
19
+ | { readonly ok: false; readonly error: CborErrorType };
12
20
 
13
21
  /**
14
22
  * Function type for custom CBOR value summarizers.
15
23
  *
16
24
  * Summarizers provide custom string representations for tagged values.
25
+ * Returns a Result type matching Rust's `Result<String, Error>`.
17
26
  *
18
27
  * @param cbor - The CBOR value to summarize
19
28
  * @param flat - If true, produce single-line output
20
- * @returns String summary of the value
29
+ * @returns Result with summary string on success, or error on failure
21
30
  */
22
- export type CborSummarizer = (cbor: Cbor, flat: boolean) => string;
31
+ export type CborSummarizer = (cbor: Cbor, flat: boolean) => SummarizerResult;
23
32
 
24
33
  /**
25
34
  * Interface for tag store operations.
@@ -92,7 +101,13 @@ export class TagsStore implements TagsStoreTrait {
92
101
  /**
93
102
  * Insert a tag into the registry.
94
103
  *
95
- * @param tag - The tag to register
104
+ * Matches Rust's TagsStore::insert() behavior:
105
+ * - Throws if the tag name is undefined or empty
106
+ * - Throws if a tag with the same value exists with a different name
107
+ * - Allows re-registering the same tag value with the same name
108
+ *
109
+ * @param tag - The tag to register (must have a non-empty name)
110
+ * @throws Error if tag has no name, empty name, or conflicts with existing registration
96
111
  *
97
112
  * @example
98
113
  * ```typescript
@@ -101,11 +116,25 @@ export class TagsStore implements TagsStoreTrait {
101
116
  * ```
102
117
  */
103
118
  insert(tag: Tag): void {
119
+ const name = tag.name;
120
+
121
+ // Rust: let name = tag.name().unwrap(); assert!(!name.is_empty());
122
+ if (name === undefined || name === "") {
123
+ throw new Error(`Tag ${tag.value} must have a non-empty name`);
124
+ }
125
+
104
126
  const key = this.#valueKey(tag.value);
105
- this.#tagsByValue.set(key, tag);
106
- if (tag.name !== undefined) {
107
- this.#tagsByName.set(tag.name, tag);
127
+ const existing = this.#tagsByValue.get(key);
128
+
129
+ // Rust: if old_name != name { panic!(...) }
130
+ if (existing?.name !== undefined && existing.name !== name) {
131
+ throw new Error(
132
+ `Attempt to register tag: ${tag.value} '${existing.name}' with different name: '${name}'`,
133
+ );
108
134
  }
135
+
136
+ this.#tagsByValue.set(key, tag);
137
+ this.#tagsByName.set(name, tag);
109
138
  }
110
139
 
111
140
  /**
@@ -148,27 +177,6 @@ export class TagsStore implements TagsStoreTrait {
148
177
  this.#summarizers.set(key, summarizer);
149
178
  }
150
179
 
151
- /**
152
- * Remove a tag from the registry.
153
- *
154
- * @param tagValue - The numeric tag value to remove
155
- * @returns true if a tag was removed, false otherwise
156
- */
157
- remove(tagValue: CborNumber): boolean {
158
- const key = this.#valueKey(tagValue);
159
- const tag = this.#tagsByValue.get(key);
160
- if (tag === undefined) {
161
- return false;
162
- }
163
-
164
- this.#tagsByValue.delete(key);
165
- if (tag.name !== undefined) {
166
- this.#tagsByName.delete(tag.name);
167
- }
168
- this.#summarizers.delete(key);
169
- return true;
170
- }
171
-
172
180
  assignedNameForTag(tag: Tag): string | undefined {
173
181
  const key = this.#valueKey(tag.value);
174
182
  const stored = this.#tagsByValue.get(key);
@@ -198,33 +206,6 @@ export class TagsStore implements TagsStoreTrait {
198
206
  return this.#summarizers.get(key);
199
207
  }
200
208
 
201
- /**
202
- * Get all registered tags.
203
- *
204
- * @returns Array of all registered tags
205
- */
206
- getAllTags(): Tag[] {
207
- return Array.from(this.#tagsByValue.values());
208
- }
209
-
210
- /**
211
- * Clear all registered tags and summarizers.
212
- */
213
- clear(): void {
214
- this.#tagsByValue.clear();
215
- this.#tagsByName.clear();
216
- this.#summarizers.clear();
217
- }
218
-
219
- /**
220
- * Get the number of registered tags.
221
- *
222
- * @returns Number of tags in the registry
223
- */
224
- get size(): number {
225
- return this.#tagsByValue.size;
226
- }
227
-
228
209
  /**
229
210
  * Create a string key for a numeric tag value.
230
211
  * Handles both number and bigint types.
package/src/tags.ts CHANGED
@@ -148,11 +148,10 @@ export const TAG_SELF_DESCRIBE_CBOR = 55799;
148
148
  // Matches Rust's register_tags() functionality
149
149
  // ============================================================================
150
150
 
151
- import type { TagsStore } from "./tags-store";
151
+ import type { TagsStore, SummarizerResult } from "./tags-store";
152
152
  import { getGlobalTagsStore } from "./tags-store";
153
153
  import { CborDate } from "./date";
154
154
  import type { Cbor } from "./cbor";
155
- import { diagnostic } from "./diag";
156
155
 
157
156
  // Tag constants matching Rust
158
157
  export const TAG_DATE = 1;
@@ -169,11 +168,13 @@ export const registerTagsIn = (tagsStore: TagsStore): void => {
169
168
  tagsStore.insertAll(tags);
170
169
 
171
170
  // Set summarizer for date tag
172
- tagsStore.setSummarizer(TAG_DATE, (untaggedCbor: Cbor, _flat: boolean): string => {
171
+ tagsStore.setSummarizer(TAG_DATE, (untaggedCbor: Cbor, _flat: boolean): SummarizerResult => {
173
172
  try {
174
- return CborDate.fromUntaggedCbor(untaggedCbor).toString();
175
- } catch {
176
- return diagnostic(untaggedCbor);
173
+ return { ok: true, value: CborDate.fromUntaggedCbor(untaggedCbor).toString() };
174
+ } catch (e) {
175
+ // On error, return error result matching Rust's Result::Err
176
+ const message = e instanceof Error ? e.message : String(e);
177
+ return { ok: false, error: { type: "Custom", message } };
177
178
  }
178
179
  });
179
180
  };
package/src/walk.ts CHANGED
@@ -158,21 +158,20 @@ export type Visitor<State> = (
158
158
  * @param cbor - The CBOR value to traverse
159
159
  * @param initialState - Initial state value
160
160
  * @param visitor - Function to call for each element
161
- * @returns Final state after traversal
162
161
  *
163
162
  * @example
164
163
  * ```typescript
165
- * // Count all text strings in a structure
166
- * interface CountState { count: number }
164
+ * // Count all text strings in a structure using RefCell-like pattern
165
+ * const count = { value: 0 };
167
166
  *
168
167
  * const structure = cbor({ name: 'Alice', tags: ['urgent', 'draft'] });
169
- * const result = walk(structure, { count: 0 }, (element, level, edge, state) => {
168
+ * walk(structure, null, (element, level, edge, state) => {
170
169
  * if (element.type === 'single' && element.cbor.type === MajorType.Text) {
171
- * return [{ count: state.count + 1 }, false];
170
+ * count.value++;
172
171
  * }
173
172
  * return [state, false];
174
173
  * });
175
- * console.log(result.count); // 3 (name, urgent, draft)
174
+ * console.log(count.value); // 3 (name, urgent, draft)
176
175
  * ```
177
176
  *
178
177
  * @example
@@ -181,7 +180,7 @@ export type Visitor<State> = (
181
180
  * const structure = cbor([1, 2, 3, 'found', 5, 6]);
182
181
  * let found = false;
183
182
  *
184
- * walk(structure, null, (element, level, edge) => {
183
+ * walk(structure, null, (element, level, edge, state) => {
185
184
  * if (element.type === 'single' &&
186
185
  * element.cbor.type === MajorType.Text &&
187
186
  * element.cbor.value === 'found') {
@@ -192,8 +191,8 @@ export type Visitor<State> = (
192
191
  * });
193
192
  * ```
194
193
  */
195
- export const walk = <State>(cbor: Cbor, initialState: State, visitor: Visitor<State>): State => {
196
- return walkInternal(cbor, 0, { type: EdgeType.None }, initialState, visitor);
194
+ export const walk = <State>(cbor: Cbor, initialState: State, visitor: Visitor<State>): void => {
195
+ walkInternal(cbor, 0, { type: EdgeType.None }, initialState, visitor);
197
196
  };
198
197
 
199
198
  /**
@@ -344,173 +343,3 @@ function walkTagged<State>(
344
343
  ): State {
345
344
  return walkInternal(cbor.value, level + 1, { type: EdgeType.TaggedContent }, state, visitor);
346
345
  }
347
-
348
- /**
349
- * Helper: Count all elements in a CBOR tree.
350
- *
351
- * @param cbor - The CBOR value to count
352
- * @returns Total number of elements visited
353
- *
354
- * @example
355
- * ```typescript
356
- * const structure = cbor([1, 2, [3, 4]]);
357
- * const count = countElements(structure);
358
- * console.log(count); // 6 (array, 1, 2, inner array, 3, 4)
359
- * ```
360
- */
361
- export const countElements = (cbor: Cbor): number => {
362
- interface CountState {
363
- count: number;
364
- }
365
-
366
- const result = walk<CountState>(cbor, { count: 0 }, (_element, _level, _edge, state) => {
367
- return [{ count: state.count + 1 }, false];
368
- });
369
-
370
- return result.count;
371
- };
372
-
373
- /**
374
- * Helper: Collect all elements at a specific depth level.
375
- *
376
- * @param cbor - The CBOR value to traverse
377
- * @param targetLevel - The depth level to collect (0 = root)
378
- * @returns Array of CBOR values at the target level
379
- *
380
- * @example
381
- * ```typescript
382
- * const structure = cbor([[1, 2], [3, 4]]);
383
- * const level1 = collectAtLevel(structure, 1);
384
- * // Returns: [[1, 2], [3, 4]]
385
- * const level2 = collectAtLevel(structure, 2);
386
- * // Returns: [1, 2, 3, 4]
387
- * ```
388
- */
389
- export const collectAtLevel = (cbor: Cbor, targetLevel: number): Cbor[] => {
390
- interface CollectState {
391
- items: Cbor[];
392
- }
393
-
394
- const result = walk<CollectState>(cbor, { items: [] }, (element, level, _edge, state) => {
395
- if (level === targetLevel && element.type === "single") {
396
- return [{ items: [...state.items, element.cbor] }, false];
397
- }
398
- return [state, false];
399
- });
400
-
401
- return result.items;
402
- };
403
-
404
- /**
405
- * Helper: Find first element matching a predicate.
406
- *
407
- * @template T - Type of extracted value
408
- * @param cbor - The CBOR value to search
409
- * @param predicate - Function to test each element
410
- * @returns First matching element, or undefined if not found
411
- *
412
- * @example
413
- * ```typescript
414
- * const structure = cbor({ users: [
415
- * { name: 'Alice', age: 30 },
416
- * { name: 'Bob', age: 25 }
417
- * ]});
418
- *
419
- * const bob = findFirst(structure, (element) => {
420
- * if (element.type === 'single' &&
421
- * element.cbor.type === MajorType.Text &&
422
- * element.cbor.value === 'Bob') {
423
- * return true;
424
- * }
425
- * return false;
426
- * });
427
- * ```
428
- */
429
- export const findFirst = (
430
- cbor: Cbor,
431
- predicate: (element: WalkElement) => boolean,
432
- ): Cbor | undefined => {
433
- interface FindState {
434
- found?: Cbor;
435
- }
436
-
437
- const result = walk<FindState>(cbor, {}, (element, _level, _edge, state) => {
438
- if (state.found !== undefined) {
439
- // Already found, stop descending
440
- return [state, true];
441
- }
442
-
443
- if (predicate(element)) {
444
- if (element.type === "single") {
445
- return [{ found: element.cbor }, true]; // Stop after finding
446
- }
447
- // Matched but not a single element, stop anyway
448
- return [state, true];
449
- }
450
-
451
- return [state, false];
452
- });
453
-
454
- return result.found;
455
- };
456
-
457
- /**
458
- * Helper: Collect all text strings in a CBOR tree.
459
- *
460
- * @param cbor - The CBOR value to traverse
461
- * @returns Array of all text string values found
462
- *
463
- * @example
464
- * ```typescript
465
- * const doc = cbor({
466
- * title: 'Document',
467
- * tags: ['urgent', 'draft'],
468
- * author: { name: 'Alice' }
469
- * });
470
- *
471
- * const texts = collectAllText(doc);
472
- * // Returns: ['Document', 'urgent', 'draft', 'Alice']
473
- * ```
474
- */
475
- export const collectAllText = (cbor: Cbor): string[] => {
476
- interface TextState {
477
- texts: string[];
478
- }
479
-
480
- const result = walk<TextState>(cbor, { texts: [] }, (element, _level, _edge, state) => {
481
- if (element.type === "single" && element.cbor.type === MajorType.Text) {
482
- return [{ texts: [...state.texts, element.cbor.value] }, false];
483
- }
484
- return [state, false];
485
- });
486
-
487
- return result.texts;
488
- };
489
-
490
- /**
491
- * Helper: Get the maximum depth of a CBOR tree.
492
- *
493
- * @param cbor - The CBOR value to measure
494
- * @returns Maximum depth (0 for leaf values, 1+ for containers)
495
- *
496
- * @example
497
- * ```typescript
498
- * const flat = cbor([1, 2, 3]);
499
- * console.log(maxDepth(flat)); // 1
500
- *
501
- * const nested = cbor([[[1]]]);
502
- * console.log(maxDepth(nested)); // 3
503
- * ```
504
- */
505
- export const maxDepth = (cbor: Cbor): number => {
506
- interface DepthState {
507
- maxDepth: number;
508
- }
509
-
510
- const result = walk<DepthState>(cbor, { maxDepth: 0 }, (_element, level, _edge, state) => {
511
- const newMaxDepth = Math.max(state.maxDepth, level);
512
- return [{ maxDepth: newMaxDepth }, false];
513
- });
514
-
515
- return result.maxDepth;
516
- };