From 36fa5afe548092eb477c53a424ae3c717717094b Mon Sep 17 00:00:00 2001 From: dprevost-perso Date: Tue, 22 Sep 2026 19:14:49 -0400 Subject: [PATCH 1/3] feat(cddl): add .cbor and .cborseq control operators (#89) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 8610 §3.8.4: a .cbor control on a byte string asserts the byte string carries a CBOR-encoded data item matching the given type; .cborseq asserts it carries a sequence of CBOR items matching the given (array) type. This is exactly how COSE (RFC 9052) describes a COSE_Sign1's protected header or payload field. Verified the semantics directly against the RFC text before porting, not just the PR description. Parser (packages/cddl): no new parsing logic needed - the operator's value is a plain type reference, which parseOperator already handles generically for every other operator (bits, and, within, eq, ne, lt, le, gt, ge). Just the two OPERATORS/OPERATORS_EXPECTING_VALUES table entries plus the OperatorType union member. Also updated docs: examples/commons/operators.cddl (the shared "one example per operator" fixture already covered every other operator, so .cbor/.cborseq were a visible gap by omission) and packages/cddl/docs/operators.md, whose hardcoded OperatorType union, numbered operator list, and worked per-operator examples were all stale in the exact same way - checked every doc file that enumerates operators, not just the obvious one. Ported from Mearman/cddl#1 (stacked on webdriverio/cddl#88, already merged upstream, so applies directly to main). One correction made along the way: that PR's .cbor test asserted Occurrence: { n: 0, m: Infinity } for a `?`-marked member, but this repo's parser correctly computes { n: 0, m: 1 } for `?` (zero-or-one, matching every other optional-member test in this file) - m: Infinity is `*`, not `?`. Fixed the ported test's expectation rather than carry over what looks like a fork-specific parser divergence or copy-paste error. Also added a .cborseq parser test - the ported PR only covered .cbor. Generators: checked all five, not just cddl. cddl2ts/cddl2py/ cddl2swift/cddl2kotlin already ignore any operator they don't specifically special-case (only "default" is ever checked), so .cbor/ .cborseq fall through generically to the base bstr type - verified via their actual transform() output, not assumed, and added a test to each. cddl2java was the exception, and needed real code changes: its parseType() throws "Unknown operator" for any operator whose base type isn't bool/range/group - .cbor/.cborseq (attached to plain bstr) hit that path and crashed. Digging into why also surfaced a second, pre-existing bug: cddl2java had no bstr/bytes -> byte[] mapping at all (bare `bstr` alone resolved to "Unknown"), unlike every other generator. Latent until now since bstr never appears in the real webdriver-bidi spec cddl2java is tested against - but .cbor/.cborseq are RFC-defined to apply specifically to byte strings, so properly supporting this port in cddl2java requires both fixes, not just catching the crash. Fixed both: added the missing byte[] mapping, and made the operator-fallthrough resolve the base type instead of throwing for any operator it doesn't specifically recognize (the same generic behavior the other four generators already have). Deliberately did not add .cbor/.cborseq to examples/commons/test.cddl, the realistic BiDi-shaped fixture 4 of the 5 generators snapshot-test against - unlike operators.cddl (a catalog, one example per feature), test.cddl reads as an actual mini protocol spec, and a CBOR/COSE property doesn't fit that narrative. Touching it would also mean regenerating snapshots across 4 packages for coverage the explicit per-generator tests above already give more precisely. Full checks:all passes (464 tests, coverage thresholds met). Co-Authored-By: Claude Sonnet 5 --- examples/commons/operators.cddl | 4 ++ packages/cddl/docs/operators.md | 49 +++++++++++++++- packages/cddl/src/ast.ts | 2 +- packages/cddl/src/parser.ts | 4 +- .../tests/__snapshots__/parser.test.ts.snap | 58 +++++++++++++++++++ packages/cddl/tests/parser.test.ts | 44 ++++++++++++++ packages/cddl2java/src/index.ts | 11 ++++ packages/cddl2java/tests/parseType.test.ts | 31 ++++++++++ packages/cddl2kotlin/tests/transform.test.ts | 20 +++++++ .../tests/transform_edge_cases.test.ts | 13 +++++ packages/cddl2swift/tests/transform.test.ts | 20 +++++++ packages/cddl2ts/tests/transform.test.ts | 13 +++++ 12 files changed, 266 insertions(+), 3 deletions(-) diff --git a/examples/commons/operators.cddl b/examples/commons/operators.cddl index e54df91..7b3e64b 100644 --- a/examples/commons/operators.cddl +++ b/examples/commons/operators.cddl @@ -3,6 +3,10 @@ label = bstr .size (1..63) nai = tstr .regexp "[A-Za-z0-9]+@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)+" rwxbits = uint .bits rwx +coseHeader = { alg: int } +cosePayload = bstr .cbor coseHeader +coseSeq = bstr .cborseq coseHeader + foo = ip4 .and nai fooWithin = ip4 .within nai bar = foo .ne fooWithin diff --git a/packages/cddl/docs/operators.md b/packages/cddl/docs/operators.md index 9a900c6..e29508a 100644 --- a/packages/cddl/docs/operators.md +++ b/packages/cddl/docs/operators.md @@ -7,7 +7,7 @@ Operators in CDDL modify or constrain types, providing additional semantics beyo In the AST, an operator is represented by the following structure: ```typescript -export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' +export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'cbor' | 'cborseq' export interface Operator { Type: OperatorType @@ -35,6 +35,8 @@ CDDL supports the following operators, represented in the AST: 10. `le`: Less than or equal comparison 11. `gt`: Greater than comparison 12. `ge`: Greater than or equal comparison +13. `cbor`: Asserts a byte string, decoded as CBOR, matches the given type (RFC 8610 §3.8.4) +14. `cborseq`: Asserts a byte string, decoded as a sequence of CBOR items, matches the given (array) type (RFC 8610 §3.8.4) ## Operator Location in the AST @@ -242,6 +244,51 @@ AST representation: } ``` +### CBOR Operators + +The `.cbor` operator asserts that a byte string, once decoded as CBOR, matches +the given type - the standard way to describe a byte string containing nested +CBOR-encoded data, e.g. how COSE (RFC 9052) describes a `COSE_Sign1`'s +protected header or payload field. `.cborseq` is the same idea for a byte +string containing a *sequence* of CBOR items, matched against an array type +(RFC 8610 §3.8.4): + +```cddl +coseHeader = { alg: int } +cosePayload = bstr .cbor coseHeader +``` + +AST representation: + +```json +{ + "Type": "variable", + "Name": "cosePayload", + "IsChoiceAddition": false, + "PropertyType": [ + { + "Type": "bstr", + "Operator": { + "Type": "cbor", + "Value": { + "Type": "group", + "Value": "coseHeader", + "Unwrapped": false + } + } + } + ], + "Comments": [] +} +``` + +`.cborseq` produces the identical shape with `"Type": "cborseq"` in place of +`"cbor"`. Like `.size`/`.regexp`/the comparison operators, this is a +validation constraint on the value, not a structural type change - the +`cddl2*` generators all resolve `bstr .cbor X` to their native byte-array +type (`Uint8Array`, `bytes`, `[UInt8]`, `ByteArray`, `byte[]`), ignoring the +operator and its referenced type. + ### Comparison Operators The `.ge`, `.gt`, `.le`, `.lt`, `.eq`, and `.ne` operators specify numerical constraints: diff --git a/packages/cddl/src/ast.ts b/packages/cddl/src/ast.ts index 7794b03..6fca0ed 100644 --- a/packages/cddl/src/ast.ts +++ b/packages/cddl/src/ast.ts @@ -156,7 +156,7 @@ export type Range = { Inclusive: boolean } -export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' +export type OperatorType = 'default' | 'size' | 'regexp' | 'bits' | 'and' | 'within' | 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge' | 'cbor' | 'cborseq' export interface Operator { Type: OperatorType Value: PropertyType diff --git a/packages/cddl/src/parser.ts b/packages/cddl/src/parser.ts index d851979..c7362c2 100644 --- a/packages/cddl/src/parser.ts +++ b/packages/cddl/src/parser.ts @@ -13,7 +13,7 @@ import { const NIL_TOKEN: Token = { Type: Tokens.ILLEGAL, Literal: '' } const DEFAULT_OCCURRENCE: Occurrence = { n: 1, m: 1 } // exactly one time -const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge'] +const OPERATORS: OperatorType[] = ['default', 'size', 'regexp', 'bits', 'and', 'within', 'eq', 'ne', 'lt', 'le', 'gt', 'ge', 'cbor', 'cborseq'] const OPERATORS_EXPECTING_VALUES: Record = { default: undefined, size: ['literal', 'range'], @@ -27,6 +27,8 @@ const OPERATORS_EXPECTING_VALUES: Record can parse operators 1`] = ` ], "Type": "variable", }, + { + "Comments": [], + "IsChoiceAddition": false, + "Name": "coseHeader", + "Properties": [ + { + "Comments": [], + "HasCut": true, + "Name": "alg", + "Occurrence": { + "m": 1, + "n": 1, + }, + "Type": [ + "int", + ], + }, + ], + "Type": "group", + }, + { + "Comments": [], + "IsChoiceAddition": false, + "Name": "cosePayload", + "PropertyType": [ + { + "Operator": { + "Type": "cbor", + "Value": { + "Type": "group", + "Unwrapped": false, + "Value": "coseHeader", + }, + }, + "Type": "bstr", + }, + ], + "Type": "variable", + }, + { + "Comments": [], + "IsChoiceAddition": false, + "Name": "coseSeq", + "PropertyType": [ + { + "Operator": { + "Type": "cborseq", + "Value": { + "Type": "group", + "Unwrapped": false, + "Value": "coseHeader", + }, + }, + "Type": "bstr", + }, + ], + "Type": "variable", + }, { "Comments": [], "IsChoiceAddition": false, diff --git a/packages/cddl/tests/parser.test.ts b/packages/cddl/tests/parser.test.ts index 228055a..abbd292 100644 --- a/packages/cddl/tests/parser.test.ts +++ b/packages/cddl/tests/parser.test.ts @@ -269,4 +269,48 @@ describe('parser', () => { vi.restoreAllMocks() }) + + it('parses the .cbor operator on a map member, referencing another rule as the embedded type', () => { + vi.spyOn(fs, 'readFileSync').mockReturnValue('inner = { a: int }\nouter = { ? parent: bstr .cbor inner }\n') + const p = new Parser('foo.cddl') + + const [, outer] = p.parse() as Group[] + expect(outer.Properties).toEqual([{ + HasCut: true, + Occurrence: { n: 0, m: 1 }, + Name: 'parent', + Type: [{ + Type: 'bstr', + Operator: { + Type: 'cbor', + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + }], + Comments: [] + }]) + + vi.restoreAllMocks() + }) + + it('parses the .cborseq operator the same way, on a required member', () => { + vi.spyOn(fs, 'readFileSync').mockReturnValue('inner = { a: int }\nouter = { payload: bstr .cborseq inner }\n') + const p = new Parser('foo.cddl') + + const [, outer] = p.parse() as Group[] + expect(outer.Properties).toEqual([{ + HasCut: true, + Occurrence: { n: 1, m: 1 }, + Name: 'payload', + Type: [{ + Type: 'bstr', + Operator: { + Type: 'cborseq', + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + }], + Comments: [] + }]) + + vi.restoreAllMocks() + }) }) diff --git a/packages/cddl2java/src/index.ts b/packages/cddl2java/src/index.ts index 411e64a..fe44d7e 100644 --- a/packages/cddl2java/src/index.ts +++ b/packages/cddl2java/src/index.ts @@ -477,6 +477,8 @@ export function parseType (specType: any): { type: string, isLiteral: boolean } return { type: 'Float', isLiteral: false }; } else if ((specType[0] === 'string' || specType[0] === 'text')) { return { type: 'String', isLiteral: false }; + } else if (specType[0] === 'bstr' || specType[0] === 'bytes') { + return { type: 'byte[]', isLiteral: false }; } else if (specType[0] === 'null' || specType[0] === 'any') { return { type: 'Object', isLiteral: false }; // null/any represented as Object in Java } else if (typeof specType[0] === 'object') { @@ -571,6 +573,15 @@ export function parseType (specType: any): { type: string, isLiteral: boolean } } } } + + // Any other operator (.cbor, .cborseq, .size, .regexp, .bits, .and, .within, + // .eq, .ne, .lt, .le, .gt, .ge, ...) is a validation constraint on the value, + // not a structural type change - resolve the underlying type and ignore it, + // the same way the other generators already treat these operators. + if (typeof specType[0].Type === 'string') { + return parseType([specType[0].Type]); + } + /* c8 ignore next */ throw new Error(`Unknown operator: ${JSON.stringify(specType[0].Operator)}`) } } diff --git a/packages/cddl2java/tests/parseType.test.ts b/packages/cddl2java/tests/parseType.test.ts index 93b4f21..26dbf22 100644 --- a/packages/cddl2java/tests/parseType.test.ts +++ b/packages/cddl2java/tests/parseType.test.ts @@ -84,4 +84,35 @@ describe('parseType', () => { expect(parseType(specType)).toEqual({ type: 'Integer', isLiteral: false }) }) }) + + describe('byte strings', () => { + it('resolves bare bstr/bytes to byte[]', () => { + // was "Unknown" before the fix - bstr/bytes had no primitive-type mapping at all + expect(parseType(['bstr'])).toEqual({ type: 'byte[]', isLiteral: false }) + expect(parseType(['bytes'])).toEqual({ type: 'byte[]', isLiteral: false }) + }) + + it('resolves a bstr with an unrecognized operator by ignoring the operator, not throwing', () => { + // .cbor/.cborseq (RFC 8610 §3.8.4) are validation constraints on a byte string, + // not a structural type change - same as every other generator already treats + // .size/.and/.within/etc. Before the fix this threw "Unknown operator". + const cborSpecType = [{ + Type: 'bstr', + Operator: { + Type: 'cbor', + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + }] + expect(parseType(cborSpecType)).toEqual({ type: 'byte[]', isLiteral: false }) + + const cborseqSpecType = [{ + Type: 'bstr', + Operator: { + Type: 'cborseq', + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + }] + expect(parseType(cborseqSpecType)).toEqual({ type: 'byte[]', isLiteral: false }) + }) + }) }) diff --git a/packages/cddl2kotlin/tests/transform.test.ts b/packages/cddl2kotlin/tests/transform.test.ts index bfbdeda..31987bb 100644 --- a/packages/cddl2kotlin/tests/transform.test.ts +++ b/packages/cddl2kotlin/tests/transform.test.ts @@ -22,6 +22,26 @@ describe('transform', () => { expect(output).toContain('typealias DeviceAddress = String') }) + it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to ByteArray', (operatorType) => { + // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, + // not a structural type change - same as every other operator (.size, .and, ...) + const assignment: Variable = { + Type: 'variable', + Name: 'payload', + PropertyType: { + Type: 'bstr', + Operator: { + Type: operatorType, + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + } as any, + Comments: [], + IsChoiceAddition: false + } + const output = transform([assignment]) + expect(output).toContain('typealias Payload = ByteArray') + }) + it('should transform a string literal union into an enum class', () => { const assignment: Variable = { Type: 'variable', diff --git a/packages/cddl2py/tests/transform_edge_cases.test.ts b/packages/cddl2py/tests/transform_edge_cases.test.ts index 47065e2..6e46fda 100644 --- a/packages/cddl2py/tests/transform_edge_cases.test.ts +++ b/packages/cddl2py/tests/transform_edge_cases.test.ts @@ -656,4 +656,17 @@ describe('transform edge cases', () => { expect(output).toContain('# leading docs') expect(output).toContain('Commented = str') }) + + it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to bytes', (operatorType) => { + // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, + // not a structural type change - same as every other operator (.size, .and, ...) + const output = transform([variable('payload', { + Type: 'bstr', + Operator: { + Type: operatorType, + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + } as any)]) + expect(output).toContain('Payload = bytes') + }) }) diff --git a/packages/cddl2swift/tests/transform.test.ts b/packages/cddl2swift/tests/transform.test.ts index 5f77093..7813c9e 100644 --- a/packages/cddl2swift/tests/transform.test.ts +++ b/packages/cddl2swift/tests/transform.test.ts @@ -22,6 +22,26 @@ describe('transform', () => { expect(output).toContain('public typealias DeviceAddress = String') }) + it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to [UInt8]', (operatorType) => { + // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, + // not a structural type change - same as every other operator (.size, .and, ...) + const assignment: Variable = { + Type: 'variable', + Name: 'payload', + PropertyType: { + Type: 'bstr', + Operator: { + Type: operatorType, + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + } as any, + Comments: [], + IsChoiceAddition: false + } + const output = transform([assignment]) + expect(output).toContain('public typealias Payload = [UInt8]') + }) + it('should transform a string literal union into a raw value enum', () => { const assignment: Variable = { Type: 'variable', diff --git a/packages/cddl2ts/tests/transform.test.ts b/packages/cddl2ts/tests/transform.test.ts index 0084d2d..dd7026e 100644 --- a/packages/cddl2ts/tests/transform.test.ts +++ b/packages/cddl2ts/tests/transform.test.ts @@ -66,6 +66,19 @@ describe('literal transformation direct', () => { expect(output).toContain(`export type ${name.split('-').map((part) => `${part[0]!.toUpperCase()}${part.slice(1)}`).join('')} = ${expectedType};`) }) + it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to Uint8Array', (operatorType) => { + // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, + // not a structural type change - same as every other operator (.size, .and, ...) + const output = transform([variable('payload', { + Type: 'bstr', + Operator: { + Type: operatorType, + Value: { Type: 'group', Value: 'inner', Unwrapped: false } + } + } as any)]) + expect(output).toContain('export type Payload = Uint8Array;') + }) + it('should keep bytes fields as object properties instead of record aliases', () => { const output = transform([ group('network-get-data-result', [ From e9cda9ecb64e46ef289dcc01d15a4017d4b18d01 Mon Sep 17 00:00:00 2001 From: dprevost-perso Date: Tue, 22 Sep 2026 19:52:14 -0400 Subject: [PATCH 2/3] fix: .cborseq examples used a map controller type, not an array MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 8610 §3.8.4: .cborseq matches the decoded sequence, taken as an array, against its controller type - unlike .cbor, which matches a single decoded item and can legitimately be a map. Both examples/commons/operators.cddl and the .cborseq parser test reused the same map-typed coseHeader/inner rule .cbor already (correctly) used, silently modeling invalid CDDL. Fixed by giving .cborseq its own array-typed controller: coseHeaders = [coseHeader] in the fixture, innerList = [int] in the parser test. Verified both corrected AST shapes by actually running the parser before writing the assertions, not just adjusting the type name. operators.md's prose already said "array type" without repeating the invalid example, so it didn't need a change. Co-Authored-By: Claude Sonnet 5 --- examples/commons/operators.cddl | 4 ++- .../tests/__snapshots__/parser.test.ts.snap | 25 ++++++++++++++++++- packages/cddl/tests/parser.test.ts | 7 ++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/examples/commons/operators.cddl b/examples/commons/operators.cddl index 7b3e64b..3732f65 100644 --- a/examples/commons/operators.cddl +++ b/examples/commons/operators.cddl @@ -5,7 +5,9 @@ rwxbits = uint .bits rwx coseHeader = { alg: int } cosePayload = bstr .cbor coseHeader -coseSeq = bstr .cborseq coseHeader + +coseHeaders = [coseHeader] +coseSeq = bstr .cborseq coseHeaders foo = ip4 .and nai fooWithin = ip4 .within nai diff --git a/packages/cddl/tests/__snapshots__/parser.test.ts.snap b/packages/cddl/tests/__snapshots__/parser.test.ts.snap index bad2730..eb14de6 100644 --- a/packages/cddl/tests/__snapshots__/parser.test.ts.snap +++ b/packages/cddl/tests/__snapshots__/parser.test.ts.snap @@ -1819,6 +1819,29 @@ exports[`parser > can parse operators 1`] = ` ], "Type": "variable", }, + { + "Comments": [], + "Name": "coseHeaders", + "Type": "array", + "Values": [ + { + "Comments": [], + "HasCut": false, + "Name": "", + "Occurrence": { + "m": 1, + "n": 1, + }, + "Type": [ + { + "Type": "group", + "Unwrapped": false, + "Value": "coseHeader", + }, + ], + }, + ], + }, { "Comments": [], "IsChoiceAddition": false, @@ -1830,7 +1853,7 @@ exports[`parser > can parse operators 1`] = ` "Value": { "Type": "group", "Unwrapped": false, - "Value": "coseHeader", + "Value": "coseHeaders", }, }, "Type": "bstr", diff --git a/packages/cddl/tests/parser.test.ts b/packages/cddl/tests/parser.test.ts index abbd292..447c9bd 100644 --- a/packages/cddl/tests/parser.test.ts +++ b/packages/cddl/tests/parser.test.ts @@ -293,7 +293,10 @@ describe('parser', () => { }) it('parses the .cborseq operator the same way, on a required member', () => { - vi.spyOn(fs, 'readFileSync').mockReturnValue('inner = { a: int }\nouter = { payload: bstr .cborseq inner }\n') + // .cborseq's argument must be an array type (RFC 8610 §3.8.4: the decoded + // sequence, taken as an array, is matched against it) - unlike .cbor's inner, + // which matches a single decoded item and can legitimately be a map. + vi.spyOn(fs, 'readFileSync').mockReturnValue('innerList = [int]\nouter = { payload: bstr .cborseq innerList }\n') const p = new Parser('foo.cddl') const [, outer] = p.parse() as Group[] @@ -305,7 +308,7 @@ describe('parser', () => { Type: 'bstr', Operator: { Type: 'cborseq', - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: 'innerList', Unwrapped: false } } }], Comments: [] From 2409fd72c05d2c583b72fa8a65144d5965c4bb14 Mon Sep 17 00:00:00 2001 From: dprevost-perso Date: Tue, 22 Sep 2026 19:55:57 -0400 Subject: [PATCH 3/3] test: give .cborseq its own array-suggestive reference name in all 5 generators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not a functional bug - none of these tests ever define a concrete shape for the referenced type, so the RFC 8610 §3.8.4 map-vs-array distinction fixed at the parser level (1e16479) couldn't actually be violated here; every generator ignores the operator's value entirely regardless. But all five reused the same bare "inner" name for both .cbor and .cborseq, which reads as an inconsistency right next to the parser-level fix distinguishing coseHeader (map) from coseHeaders/innerList (array). Split each shared it.each into [operator, referencedType] pairs so the naming matches what the RFC actually requires, even though it makes no difference to what's being tested. Co-Authored-By: Claude Sonnet 5 --- packages/cddl2java/tests/parseType.test.ts | 7 +++++-- packages/cddl2kotlin/tests/transform.test.ts | 12 ++++++++---- packages/cddl2py/tests/transform_edge_cases.test.ts | 12 ++++++++---- packages/cddl2swift/tests/transform.test.ts | 12 ++++++++---- packages/cddl2ts/tests/transform.test.ts | 12 ++++++++---- 5 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/cddl2java/tests/parseType.test.ts b/packages/cddl2java/tests/parseType.test.ts index 26dbf22..e98f15c 100644 --- a/packages/cddl2java/tests/parseType.test.ts +++ b/packages/cddl2java/tests/parseType.test.ts @@ -95,7 +95,10 @@ describe('parseType', () => { it('resolves a bstr with an unrecognized operator by ignoring the operator, not throwing', () => { // .cbor/.cborseq (RFC 8610 §3.8.4) are validation constraints on a byte string, // not a structural type change - same as every other generator already treats - // .size/.and/.within/etc. Before the fix this threw "Unknown operator". + // .size/.and/.within/etc. Before the fix this threw "Unknown operator". The + // generator never looks at what the reference resolves to, but the names below + // still follow the RFC: .cbor's argument matches a single decoded item (a map + // is fine), .cborseq's matches the sequence taken as an array. const cborSpecType = [{ Type: 'bstr', Operator: { @@ -109,7 +112,7 @@ describe('parseType', () => { Type: 'bstr', Operator: { Type: 'cborseq', - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: 'innerList', Unwrapped: false } } }] expect(parseType(cborseqSpecType)).toEqual({ type: 'byte[]', isLiteral: false }) diff --git a/packages/cddl2kotlin/tests/transform.test.ts b/packages/cddl2kotlin/tests/transform.test.ts index 31987bb..70ba6fb 100644 --- a/packages/cddl2kotlin/tests/transform.test.ts +++ b/packages/cddl2kotlin/tests/transform.test.ts @@ -22,9 +22,13 @@ describe('transform', () => { expect(output).toContain('typealias DeviceAddress = String') }) - it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to ByteArray', (operatorType) => { - // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, - // not a structural type change - same as every other operator (.size, .and, ...) + it.each([ + ['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine + ['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4) + ] as const)('should ignore the .%s operator and still map bstr to ByteArray', (operatorType, referencedType) => { + // Either way this is a validation constraint on a byte string, not a structural + // type change - same as every other operator (.size, .and, ...); the generator + // never looks at what the reference resolves to. const assignment: Variable = { Type: 'variable', Name: 'payload', @@ -32,7 +36,7 @@ describe('transform', () => { Type: 'bstr', Operator: { Type: operatorType, - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: referencedType, Unwrapped: false } } } as any, Comments: [], diff --git a/packages/cddl2py/tests/transform_edge_cases.test.ts b/packages/cddl2py/tests/transform_edge_cases.test.ts index 6e46fda..bea6e68 100644 --- a/packages/cddl2py/tests/transform_edge_cases.test.ts +++ b/packages/cddl2py/tests/transform_edge_cases.test.ts @@ -657,14 +657,18 @@ describe('transform edge cases', () => { expect(output).toContain('Commented = str') }) - it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to bytes', (operatorType) => { - // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, - // not a structural type change - same as every other operator (.size, .and, ...) + it.each([ + ['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine + ['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4) + ] as const)('should ignore the .%s operator and still map bstr to bytes', (operatorType, referencedType) => { + // Either way this is a validation constraint on a byte string, not a structural + // type change - same as every other operator (.size, .and, ...); the generator + // never looks at what the reference resolves to. const output = transform([variable('payload', { Type: 'bstr', Operator: { Type: operatorType, - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: referencedType, Unwrapped: false } } } as any)]) expect(output).toContain('Payload = bytes') diff --git a/packages/cddl2swift/tests/transform.test.ts b/packages/cddl2swift/tests/transform.test.ts index 7813c9e..60c4480 100644 --- a/packages/cddl2swift/tests/transform.test.ts +++ b/packages/cddl2swift/tests/transform.test.ts @@ -22,9 +22,13 @@ describe('transform', () => { expect(output).toContain('public typealias DeviceAddress = String') }) - it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to [UInt8]', (operatorType) => { - // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, - // not a structural type change - same as every other operator (.size, .and, ...) + it.each([ + ['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine + ['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4) + ] as const)('should ignore the .%s operator and still map bstr to [UInt8]', (operatorType, referencedType) => { + // Either way this is a validation constraint on a byte string, not a structural + // type change - same as every other operator (.size, .and, ...); the generator + // never looks at what the reference resolves to. const assignment: Variable = { Type: 'variable', Name: 'payload', @@ -32,7 +36,7 @@ describe('transform', () => { Type: 'bstr', Operator: { Type: operatorType, - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: referencedType, Unwrapped: false } } } as any, Comments: [], diff --git a/packages/cddl2ts/tests/transform.test.ts b/packages/cddl2ts/tests/transform.test.ts index dd7026e..c6efdb2 100644 --- a/packages/cddl2ts/tests/transform.test.ts +++ b/packages/cddl2ts/tests/transform.test.ts @@ -66,14 +66,18 @@ describe('literal transformation direct', () => { expect(output).toContain(`export type ${name.split('-').map((part) => `${part[0]!.toUpperCase()}${part.slice(1)}`).join('')} = ${expectedType};`) }) - it.each(['cbor', 'cborseq'] as const)('should ignore the .%s operator and still map bstr to Uint8Array', (operatorType) => { - // RFC 8610 §3.8.4: .cbor/.cborseq are validation constraints on a byte string, - // not a structural type change - same as every other operator (.size, .and, ...) + it.each([ + ['cbor', 'inner'], // .cbor's argument matches a single decoded item - a map is fine + ['cborseq', 'innerList'] // .cborseq's argument matches the sequence taken as an array (RFC 8610 §3.8.4) + ] as const)('should ignore the .%s operator and still map bstr to Uint8Array', (operatorType, referencedType) => { + // Either way this is a validation constraint on a byte string, not a structural + // type change - same as every other operator (.size, .and, ...); the generator + // never looks at what the reference resolves to. const output = transform([variable('payload', { Type: 'bstr', Operator: { Type: operatorType, - Value: { Type: 'group', Value: 'inner', Unwrapped: false } + Value: { Type: 'group', Value: referencedType, Unwrapped: false } } } as any)]) expect(output).toContain('export type Payload = Uint8Array;')