@bcts/dcbor-parse 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/src/error.ts ADDED
@@ -0,0 +1,373 @@
1
+ /**
2
+ * @bcts/dcbor-parse - Error types
3
+ *
4
+ * This is a 1:1 TypeScript port of bc-dcbor-parse-rust error.rs
5
+ *
6
+ * @module dcbor-parse/error
7
+ */
8
+
9
+ import type { Token } from "./token";
10
+
11
+ /**
12
+ * Represents a span (range) in the source string.
13
+ *
14
+ * Corresponds to the Rust `logos::Span` type.
15
+ */
16
+ export interface Span {
17
+ readonly start: number;
18
+ readonly end: number;
19
+ }
20
+
21
+ /**
22
+ * Creates a span with the given start and end positions.
23
+ */
24
+ export function span(start: number, end: number): Span {
25
+ return { start, end };
26
+ }
27
+
28
+ /**
29
+ * Creates a default (empty) span.
30
+ */
31
+ export function defaultSpan(): Span {
32
+ return { start: 0, end: 0 };
33
+ }
34
+
35
+ /**
36
+ * Parse error types.
37
+ *
38
+ * Corresponds to the Rust `Error` enum in error.rs
39
+ */
40
+ export type ParseError =
41
+ | { readonly type: "EmptyInput" }
42
+ | { readonly type: "UnexpectedEndOfInput" }
43
+ | { readonly type: "ExtraData"; readonly span: Span }
44
+ | { readonly type: "UnexpectedToken"; readonly token: Token; readonly span: Span }
45
+ | { readonly type: "UnrecognizedToken"; readonly span: Span }
46
+ | { readonly type: "ExpectedComma"; readonly span: Span }
47
+ | { readonly type: "ExpectedColon"; readonly span: Span }
48
+ | { readonly type: "UnmatchedParentheses"; readonly span: Span }
49
+ | { readonly type: "UnmatchedBraces"; readonly span: Span }
50
+ | { readonly type: "ExpectedMapKey"; readonly span: Span }
51
+ | { readonly type: "InvalidTagValue"; readonly value: string; readonly span: Span }
52
+ | { readonly type: "UnknownTagName"; readonly name: string; readonly span: Span }
53
+ | { readonly type: "InvalidHexString"; readonly span: Span }
54
+ | { readonly type: "InvalidBase64String"; readonly span: Span }
55
+ | { readonly type: "UnknownUrType"; readonly urType: string; readonly span: Span }
56
+ | { readonly type: "InvalidUr"; readonly message: string; readonly span: Span }
57
+ | { readonly type: "InvalidKnownValue"; readonly value: string; readonly span: Span }
58
+ | { readonly type: "UnknownKnownValueName"; readonly name: string; readonly span: Span }
59
+ | { readonly type: "InvalidDateString"; readonly dateString: string; readonly span: Span }
60
+ | { readonly type: "DuplicateMapKey"; readonly span: Span };
61
+
62
+ // Error constructors (lowercase to differentiate from the type)
63
+ export const parseError = {
64
+ emptyInput(): ParseError {
65
+ return { type: "EmptyInput" };
66
+ },
67
+
68
+ unexpectedEndOfInput(): ParseError {
69
+ return { type: "UnexpectedEndOfInput" };
70
+ },
71
+
72
+ extraData(span: Span): ParseError {
73
+ return { type: "ExtraData", span };
74
+ },
75
+
76
+ unexpectedToken(token: Token, span: Span): ParseError {
77
+ return { type: "UnexpectedToken", token, span };
78
+ },
79
+
80
+ unrecognizedToken(span: Span): ParseError {
81
+ return { type: "UnrecognizedToken", span };
82
+ },
83
+
84
+ expectedComma(span: Span): ParseError {
85
+ return { type: "ExpectedComma", span };
86
+ },
87
+
88
+ expectedColon(span: Span): ParseError {
89
+ return { type: "ExpectedColon", span };
90
+ },
91
+
92
+ unmatchedParentheses(span: Span): ParseError {
93
+ return { type: "UnmatchedParentheses", span };
94
+ },
95
+
96
+ unmatchedBraces(span: Span): ParseError {
97
+ return { type: "UnmatchedBraces", span };
98
+ },
99
+
100
+ expectedMapKey(span: Span): ParseError {
101
+ return { type: "ExpectedMapKey", span };
102
+ },
103
+
104
+ invalidTagValue(value: string, span: Span): ParseError {
105
+ return { type: "InvalidTagValue", value, span };
106
+ },
107
+
108
+ unknownTagName(name: string, span: Span): ParseError {
109
+ return { type: "UnknownTagName", name, span };
110
+ },
111
+
112
+ invalidHexString(span: Span): ParseError {
113
+ return { type: "InvalidHexString", span };
114
+ },
115
+
116
+ invalidBase64String(span: Span): ParseError {
117
+ return { type: "InvalidBase64String", span };
118
+ },
119
+
120
+ unknownUrType(urType: string, span: Span): ParseError {
121
+ return { type: "UnknownUrType", urType, span };
122
+ },
123
+
124
+ invalidUr(message: string, span: Span): ParseError {
125
+ return { type: "InvalidUr", message, span };
126
+ },
127
+
128
+ invalidKnownValue(value: string, span: Span): ParseError {
129
+ return { type: "InvalidKnownValue", value, span };
130
+ },
131
+
132
+ unknownKnownValueName(name: string, span: Span): ParseError {
133
+ return { type: "UnknownKnownValueName", name, span };
134
+ },
135
+
136
+ invalidDateString(dateString: string, span: Span): ParseError {
137
+ return { type: "InvalidDateString", dateString, span };
138
+ },
139
+
140
+ duplicateMapKey(span: Span): ParseError {
141
+ return { type: "DuplicateMapKey", span };
142
+ },
143
+ };
144
+
145
+ /**
146
+ * Checks if an error is the default unrecognized token error.
147
+ *
148
+ * Corresponds to Rust `Error::is_default()`
149
+ */
150
+ export function isDefaultError(error: ParseError): boolean {
151
+ return error.type === "UnrecognizedToken";
152
+ }
153
+
154
+ /**
155
+ * Gets the error message for a parse error.
156
+ *
157
+ * Corresponds to Rust's `Display` implementation for `Error`
158
+ */
159
+ export function errorMessage(error: ParseError): string {
160
+ switch (error.type) {
161
+ case "EmptyInput":
162
+ return "Empty input";
163
+ case "UnexpectedEndOfInput":
164
+ return "Unexpected end of input";
165
+ case "ExtraData":
166
+ return "Extra data at end of input";
167
+ case "UnexpectedToken":
168
+ return `Unexpected token ${tokenDebugString(error.token)}`;
169
+ case "UnrecognizedToken":
170
+ return "Unrecognized token";
171
+ case "ExpectedComma":
172
+ return "Expected comma";
173
+ case "ExpectedColon":
174
+ return "Expected colon";
175
+ case "UnmatchedParentheses":
176
+ return "Unmatched parentheses";
177
+ case "UnmatchedBraces":
178
+ return "Unmatched braces";
179
+ case "ExpectedMapKey":
180
+ return "Expected map key";
181
+ case "InvalidTagValue":
182
+ return `Invalid tag value '${error.value}'`;
183
+ case "UnknownTagName":
184
+ return `Unknown tag name '${error.name}'`;
185
+ case "InvalidHexString":
186
+ return "Invalid hex string";
187
+ case "InvalidBase64String":
188
+ return "Invalid base64 string";
189
+ case "UnknownUrType":
190
+ return `Unknown UR type '${error.urType}'`;
191
+ case "InvalidUr":
192
+ return `Invalid UR '${error.message}'`;
193
+ case "InvalidKnownValue":
194
+ return `Invalid known value '${error.value}'`;
195
+ case "UnknownKnownValueName":
196
+ return `Unknown known value name '${error.name}'`;
197
+ case "InvalidDateString":
198
+ return `Invalid date string '${error.dateString}'`;
199
+ case "DuplicateMapKey":
200
+ return "Duplicate map key";
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Gets the span for a parse error, if applicable.
206
+ */
207
+ export function errorSpan(error: ParseError): Span | undefined {
208
+ switch (error.type) {
209
+ case "EmptyInput":
210
+ case "UnexpectedEndOfInput":
211
+ return undefined;
212
+ case "ExtraData":
213
+ case "UnexpectedToken":
214
+ case "UnrecognizedToken":
215
+ case "ExpectedComma":
216
+ case "ExpectedColon":
217
+ case "UnmatchedParentheses":
218
+ case "UnmatchedBraces":
219
+ case "ExpectedMapKey":
220
+ case "InvalidTagValue":
221
+ case "UnknownTagName":
222
+ case "InvalidHexString":
223
+ case "InvalidBase64String":
224
+ case "UnknownUrType":
225
+ case "InvalidUr":
226
+ case "InvalidKnownValue":
227
+ case "UnknownKnownValueName":
228
+ case "InvalidDateString":
229
+ case "DuplicateMapKey":
230
+ return error.span;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Formats an error message with source context, line number, and caret.
236
+ *
237
+ * Corresponds to Rust `Error::format_message()`
238
+ */
239
+ function formatMessage(message: string, source: string, range: Span): string {
240
+ const start = range.start;
241
+ const end = range.end;
242
+
243
+ // Walk through the characters up to `start` to find line number and line start offset
244
+ let lineNumber = 1;
245
+ let lineStart = 0;
246
+
247
+ for (let idx = 0; idx < source.length && idx < start; idx++) {
248
+ if (source[idx] === "\n") {
249
+ lineNumber++;
250
+ lineStart = idx + 1;
251
+ }
252
+ }
253
+
254
+ // Grab the exact line text (or empty if out of bounds)
255
+ const lines = source.split("\n");
256
+ const line = lines[lineNumber - 1] ?? "";
257
+
258
+ // Column is byte-offset into that line
259
+ const column = Math.max(0, start - lineStart);
260
+
261
+ // Underline at least one caret, even for zero-width spans
262
+ const underlineLen = Math.max(1, end - start);
263
+ const caret = " ".repeat(column) + "^".repeat(underlineLen);
264
+
265
+ return `line ${lineNumber}: ${message}\n${line}\n${caret}`;
266
+ }
267
+
268
+ /**
269
+ * Gets the full error message with source context.
270
+ *
271
+ * Corresponds to Rust `Error::full_message()`
272
+ */
273
+ export function fullErrorMessage(error: ParseError, source: string): string {
274
+ const message = errorMessage(error);
275
+
276
+ switch (error.type) {
277
+ case "EmptyInput":
278
+ return formatMessage(message, source, defaultSpan());
279
+ case "UnexpectedEndOfInput":
280
+ return formatMessage(message, source, span(source.length, source.length));
281
+ case "ExtraData":
282
+ case "UnexpectedToken":
283
+ case "UnrecognizedToken":
284
+ case "ExpectedComma":
285
+ case "ExpectedColon":
286
+ case "UnmatchedParentheses":
287
+ case "UnmatchedBraces":
288
+ case "ExpectedMapKey":
289
+ case "InvalidTagValue":
290
+ case "UnknownTagName":
291
+ case "InvalidHexString":
292
+ case "InvalidBase64String":
293
+ case "UnknownUrType":
294
+ case "InvalidUr":
295
+ case "InvalidKnownValue":
296
+ case "UnknownKnownValueName":
297
+ case "InvalidDateString":
298
+ case "DuplicateMapKey":
299
+ return formatMessage(message, source, error.span);
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Creates a default parse error (UnrecognizedToken with empty span).
305
+ *
306
+ * Corresponds to Rust `Error::default()`
307
+ */
308
+ export function defaultParseError(): ParseError {
309
+ return parseError.unrecognizedToken(defaultSpan());
310
+ }
311
+
312
+ /**
313
+ * Result type for parse operations.
314
+ *
315
+ * Corresponds to Rust `Result<T, Error>`
316
+ */
317
+ export type ParseResult<T> =
318
+ | { readonly ok: true; readonly value: T }
319
+ | { readonly ok: false; readonly error: ParseError };
320
+
321
+ /**
322
+ * Creates a successful result.
323
+ */
324
+ export function ok<T>(value: T): ParseResult<T> {
325
+ return { ok: true, value };
326
+ }
327
+
328
+ /**
329
+ * Creates an error result.
330
+ */
331
+ export function err<T>(error: ParseError): ParseResult<T> {
332
+ return { ok: false, error };
333
+ }
334
+
335
+ /**
336
+ * Checks if a result is successful.
337
+ */
338
+ export function isOk<T>(result: ParseResult<T>): result is { ok: true; value: T } {
339
+ return result.ok;
340
+ }
341
+
342
+ /**
343
+ * Checks if a result is an error.
344
+ */
345
+ export function isErr<T>(result: ParseResult<T>): result is { ok: false; error: ParseError } {
346
+ return !result.ok;
347
+ }
348
+
349
+ /**
350
+ * Unwraps a result, throwing if it's an error.
351
+ */
352
+ export function unwrap<T>(result: ParseResult<T>): T {
353
+ if (result.ok) {
354
+ return result.value;
355
+ }
356
+ throw new Error(errorMessage(result.error));
357
+ }
358
+
359
+ /**
360
+ * Unwraps a result error, throwing if it's successful.
361
+ */
362
+ export function unwrapErr<T>(result: ParseResult<T>): ParseError {
363
+ if (!result.ok) {
364
+ return result.error;
365
+ }
366
+ throw new Error("Called unwrapErr on an Ok result");
367
+ }
368
+
369
+ // Helper function to get debug string for a token (forward declaration resolved at runtime)
370
+ function tokenDebugString(token: Token): string {
371
+ // Simple debug representation
372
+ return JSON.stringify(token);
373
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * # dCBOR Diagnostic Parser and Composer
3
+ *
4
+ * This package provides tools for parsing and composing the [CBOR diagnostic
5
+ * notation](https://datatracker.ietf.org/doc/html/rfc8949#name-diagnostic-notation)
6
+ * into [dCBOR (deterministic CBOR)](https://datatracker.ietf.org/doc/draft-mcnally-deterministic-cbor/)
7
+ * data items.
8
+ *
9
+ * It is intended for use in testing, debugging, and other scenarios where a
10
+ * human-readable representation of dCBOR is useful. It is not optimized for
11
+ * performance and should not be used in production environments where binary
12
+ * dCBOR is expected.
13
+ *
14
+ * The primary functions provided are:
15
+ *
16
+ * - `parseDcborItem`: Parses a string in CBOR diagnostic notation into a `Cbor` object.
17
+ * - `composeDcborArray`: Composes a `Cbor` array from a slice of strings
18
+ * representing dCBOR items in diagnostic notation.
19
+ * - `composeDcborMap`: Composes a `Cbor` map from a slice of strings
20
+ * representing the key-value pairs in dCBOR diagnostic notation.
21
+ *
22
+ * ## Supported Types
23
+ *
24
+ * | Type | Example(s) |
25
+ * | ------------------- | ----------------------------------------------------------- |
26
+ * | Boolean | `true`, `false` |
27
+ * | Null | `null` |
28
+ * | Integers | `0`, `1`, `-1`, `42` |
29
+ * | Floats | `3.14`, `-2.5`, `Infinity`, `-Infinity`, `NaN` |
30
+ * | Strings | `"hello"`, `"🌎"` |
31
+ * | Date Literals | `2023-02-08`, `2023-02-08T15:30:45Z`, `1965-05-15` |
32
+ * | Hex Byte Strings | `h'68656c6c6f'` |
33
+ * | Base64 Byte Strings | `b64'AQIDBAUGBwgJCg=='` |
34
+ * | Tagged Values | `1234("hello")`, `5678(3.14)` |
35
+ * | Name-Tagged Values | `tag-name("hello")`, `tag-name(3.14)` |
36
+ * | Known Values | `'1'`, `'isA'` |
37
+ * | Unit Known Value | `Unit`, `''`, `'0'` |
38
+ * | URs | `ur:date/cyisdadmlasgtapttl` |
39
+ * | Arrays | `[1, 2, 3]`, `["hello", "world"]`, `[1, [2, 3]]` |
40
+ * | Maps | `{1: 2, 3: 4}`, `{"key": "value"}`, `{1: [2, 3], 4: 5}` |
41
+ *
42
+ * @module dcbor-parse
43
+ */
44
+
45
+ // Parse functions
46
+ export { parseDcborItem, parseDcborItemPartial } from "./parse";
47
+
48
+ // Token types
49
+ export { type Token, token, Lexer } from "./token";
50
+
51
+ // Error types
52
+ export {
53
+ type Span,
54
+ span,
55
+ defaultSpan,
56
+ type ParseError,
57
+ parseError,
58
+ type ParseResult,
59
+ ok,
60
+ err,
61
+ isOk,
62
+ isErr,
63
+ unwrap,
64
+ unwrapErr,
65
+ isDefaultError,
66
+ errorMessage,
67
+ errorSpan,
68
+ fullErrorMessage,
69
+ defaultParseError,
70
+ } from "./error";
71
+
72
+ // Compose functions
73
+ export {
74
+ type ComposeError,
75
+ composeError,
76
+ type ComposeResult,
77
+ composeOk,
78
+ composeErr,
79
+ composeErrorMessage,
80
+ composeDcborArray,
81
+ composeDcborMap,
82
+ } from "./compose";