From 224da1725790705daa96633939276fce7892f51b Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 13 Apr 2026 20:03:38 +0000 Subject: [PATCH 1/4] Add custom regexp channel type support --- packages/cddl/src/utils.ts | 20 ++++++++- packages/cddl/tests/parser.test.ts | 25 +++++++++++ packages/cddl/tests/utils.test.ts | 15 +++++++ packages/cddl2py/src/index.ts | 39 +++++++++++++++- .../tests/transform_edge_cases.test.ts | 44 +++++++++++++++++++ packages/cddl2ts/src/index.ts | 31 +++++++++++++ .../tests/transform_edge_cases.test.ts | 32 ++++++++++++++ 7 files changed, 204 insertions(+), 2 deletions(-) diff --git a/packages/cddl/src/utils.ts b/packages/cddl/src/utils.ts index 6d36def..cfe8311 100644 --- a/packages/cddl/src/utils.ts +++ b/packages/cddl/src/utils.ts @@ -81,7 +81,25 @@ export function isPropertyReference (t: any): t is PropertyReference { } export function isNativeTypeWithOperator (t: any): t is NativeTypeWithOperator { - return t && typeof t.Type === 'object' && 'Operator' in t + return t && typeof t === 'object' && 'Type' in t && 'Operator' in t +} + +export function getRegexpPattern (t: any): string | undefined { + if (!isNativeTypeWithOperator(t)) { + return + } + + if (typeof t.Type !== 'string' || !['str', 'text', 'tstr'].includes(t.Type)) { + return + } + + if (t.Operator?.Type !== 'regexp' || !isLiteralWithValue(t.Operator.Value)) { + return + } + + return typeof t.Operator.Value.Value === 'string' + ? t.Operator.Value.Value + : undefined } export function isRange (t: any): boolean { diff --git a/packages/cddl/tests/parser.test.ts b/packages/cddl/tests/parser.test.ts index f6103ec..5cfe878 100644 --- a/packages/cddl/tests/parser.test.ts +++ b/packages/cddl/tests/parser.test.ts @@ -60,4 +60,29 @@ describe('parser', () => { vi.restoreAllMocks() }) + + it('parses RFC 9165 regexp operators on text strings', () => { + vi.spyOn(fs, 'readFileSync').mockReturnValue('channel = tstr .regexp "custom:.+"\n') + const p = new Parser('foo.cddl') + + expect(p.parse()).toEqual([{ + Type: 'variable', + Name: 'channel', + IsChoiceAddition: false, + PropertyType: [{ + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: { + Type: 'literal', + Value: 'custom:.+', + Unwrapped: false + } + } + }], + Comments: [] + }]) + + vi.restoreAllMocks() + }) }) diff --git a/packages/cddl/tests/utils.test.ts b/packages/cddl/tests/utils.test.ts index a99279a..554c86c 100644 --- a/packages/cddl/tests/utils.test.ts +++ b/packages/cddl/tests/utils.test.ts @@ -12,6 +12,7 @@ import type { } from '../src/ast.js' import { Tokens, type Token } from '../src/tokens.js' import { + getRegexpPattern, hasSpecialNumberCharacter, isAlphabeticCharacter, isCDDLArray, @@ -183,6 +184,17 @@ describe('utils', () => { Value: 'tstr' } } + const nativeStringTypeWithRegexp: NativeTypeWithOperator = { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: { + Type: 'literal', + Value: 'custom:.+', + Unwrapped: false + } + } + } const rangeReference: PropertyReference = { Type: 'range', Value: { @@ -194,7 +206,10 @@ describe('utils', () => { } expect(isNativeTypeWithOperator(nativeTypeWithOperator)).toBe(true) + expect(isNativeTypeWithOperator(nativeStringTypeWithRegexp)).toBe(true) expect(isNativeTypeWithOperator({ Type: 'tstr' })).toBe(false) + expect(getRegexpPattern(nativeStringTypeWithRegexp)).toBe('custom:.+') + expect(getRegexpPattern(nativeTypeWithOperator)).toBeUndefined() expect(isRange({ Type: rangeReference })).toBe(true) expect(isRange({ Type: 'range' })).toBe(false) diff --git a/packages/cddl2py/src/index.ts b/packages/cddl2py/src/index.ts index 9c903ed..d66f4e2 100644 --- a/packages/cddl2py/src/index.ts +++ b/packages/cddl2py/src/index.ts @@ -1,8 +1,9 @@ import { + getRegexpPattern, isCDDLArray, isGroup, isNamedGroupReference, isLiteralWithValue, isNativeTypeWithOperator, isUnNamedProperty, isPropertyReference, isRange, isVariable, pascalCase, - type Assignment, type PropertyType, type PropertyReference, + type Assignment, type NativeTypeWithOperator, type PropertyType, type PropertyReference, type Property, type Array as CDDLArray, type Operator, type Group, type Variable, type Comment, type Tag } from 'cddl' @@ -29,6 +30,7 @@ interface ResolveTypeOptions { } const STRING_RECORD_KEY_TYPES = new Set(['str', 'text', 'tstr']) +const CUSTOM_CHANNEL_REGEXP_PATTERNS = new Set(['custom:.+', '^custom:.+$']) export function transform (assignments: Assignment[], options?: TransformOptions): string { const ctx: Context = { @@ -516,6 +518,33 @@ function getExtraItemsType (props: Property[], ctx: Context): string | undefined return `Union[${uniqueTypes.join(', ')}]` } +function resolveNativeTypeWithOperator (t: NativeTypeWithOperator, ctx: Context): string | undefined { + if (typeof t.Type !== 'string') { + return + } + + const mapped = NATIVE_TYPE_MAP[t.Type] + if (!mapped) { + return + } + + const regexpPattern = getRegexpPattern(t) + if (!regexpPattern || !CUSTOM_CHANNEL_REGEXP_PATTERNS.has(regexpPattern)) { + if (mapped === 'Any') { + ctx.typingImports.add('Any') + } + return mapped + } + + ctx.typingImports.add('Annotated') + if (ctx.pydantic) { + ctx.pydanticImports.add('StringConstraints') + return `Annotated[${mapped}, StringConstraints(pattern=${JSON.stringify(regexpPattern)})]` + } + + return `Annotated[${mapped}, ${JSON.stringify(regexpPattern)}]` +} + // --------------------------------------------------------------------------- // Type resolution // --------------------------------------------------------------------------- @@ -532,6 +561,14 @@ function resolveType (t: PropertyType, ctx: Context, options: ResolveTypeOptions throw new Error(`Unknown native type: "${t}"`) } + if (isNativeTypeWithOperator(t) && typeof t.Type === 'string') { + const resolved = resolveNativeTypeWithOperator(t, ctx) + if (resolved) { + return resolved + } + throw new Error(`Unknown native type with operator: ${JSON.stringify(t)}`) + } + if ((t as any).Type && typeof (t as any).Type === 'string' && NATIVE_TYPE_MAP[(t as any).Type]) { const mapped = NATIVE_TYPE_MAP[(t as any).Type] if (mapped === 'Any') { diff --git a/packages/cddl2py/tests/transform_edge_cases.test.ts b/packages/cddl2py/tests/transform_edge_cases.test.ts index 6dd4943..7a0a1a9 100644 --- a/packages/cddl2py/tests/transform_edge_cases.test.ts +++ b/packages/cddl2py/tests/transform_edge_cases.test.ts @@ -151,6 +151,50 @@ describe('transform edge cases', () => { expect(output).toContain('Combined = Union[_CombinedVariant0, _CombinedVariant1]') }) + it('should preserve custom channel regexp strings in python-friendly types', () => { + const typedDictOutput = transform([ + variable('channel', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('custom:.+') + } + } as any), + group('event-envelope', [ + property('channel', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('custom:.+') + } + } as any) + ]), + variable('email-address', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('[^@]+@[^@]+') + } + } as any) + ]) + const pydanticOutput = transform([ + variable('channel', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('custom:.+') + } + } as any) + ], { pydantic: true }) + + expect(typedDictOutput).toContain('from typing import Annotated') + expect(typedDictOutput).toContain('Channel = Annotated[str, "custom:.+"]') + expect(typedDictOutput).toContain('channel: Annotated[str, "custom:.+"]') + expect(typedDictOutput).toContain('EmailAddress = str') + expect(pydanticOutput).toContain('from pydantic import StringConstraints') + expect(pydanticOutput).toContain('Channel = Annotated[str, StringConstraints(pattern="custom:.+")]') + }) + it('should collapse multiple union mixin groups into a single alias', () => { const output = transform([ group('combined', [ diff --git a/packages/cddl2ts/src/index.ts b/packages/cddl2ts/src/index.ts index 79c8f4b..22f8552 100644 --- a/packages/cddl2ts/src/index.ts +++ b/packages/cddl2ts/src/index.ts @@ -3,6 +3,7 @@ import { parse, print, types } from 'recast' import typescriptParser from 'recast/parsers/typescript.js' import { + getRegexpPattern, isCDDLArray, isGroup, isNamedGroupReference, @@ -14,6 +15,7 @@ import { isVariable, pascalCase, type Assignment, + type NativeTypeWithOperator, type PropertyType, type PropertyReference, type Property, @@ -51,6 +53,7 @@ const NATIVE_TYPES: Record = { nil: b.tsNullKeyword(), null: b.tsNullKeyword() } +const CUSTOM_CHANNEL_REGEXP_PATTERNS = new Set(['custom:.+', '^custom:.+$']) const RECORD_KEY_TYPES = new Set([ 'int', 'uint', 'nint', 'integer', 'unsigned', 'number', 'float', 'float16', 'float32', 'float64', 'float16-32', 'float32-64', @@ -610,12 +613,40 @@ function parseObjectType (props: Property[], options: TransformSettings): Object return propItems } +function parseTemplateLiteralType (template: string): TSTypeKind { + const ast = parse(`type __CDDLTemplate = ${template};`, { + parser: typescriptParser, + sourceFileName: 'cddl2Ts.ts', + sourceRoot: process.cwd() + }) satisfies types.namedTypes.File + return (ast.program.body[0] as types.namedTypes.TSTypeAliasDeclaration).typeAnnotation +} + +function parseNativeTypeWithOperator (t: NativeTypeWithOperator): TSTypeKind | undefined { + if (typeof t.Type !== 'string') { + return + } + + const regexpPattern = getRegexpPattern(t) + if (regexpPattern && CUSTOM_CHANNEL_REGEXP_PATTERNS.has(regexpPattern)) { + return parseTemplateLiteralType('`custom:${string}`') + } + + return NATIVE_TYPES[t.Type] +} + function parseUnionType (t: PropertyType | Assignment, options: TransformSettings): TSTypeKind { if (typeof t === 'string') { if (!NATIVE_TYPES[t]) { throw new Error(`Unknown native type: "${t}`) } return NATIVE_TYPES[t] + } else if (isNativeTypeWithOperator(t) && typeof t.Type === 'string') { + const nativeType = parseNativeTypeWithOperator(t) + if (!nativeType) { + throw new Error(`Unknown native type with operator: ${JSON.stringify(t)}`) + } + return nativeType } else if ((t as any).Type && typeof (t as any).Type === 'string' && NATIVE_TYPES[(t as any).Type]) { return NATIVE_TYPES[(t as any).Type] } else if (isNativeTypeWithOperator(t) && NATIVE_TYPES[(t.Type as any).Type]) { diff --git a/packages/cddl2ts/tests/transform_edge_cases.test.ts b/packages/cddl2ts/tests/transform_edge_cases.test.ts index f0cb3d0..d5bed96 100644 --- a/packages/cddl2ts/tests/transform_edge_cases.test.ts +++ b/packages/cddl2ts/tests/transform_edge_cases.test.ts @@ -123,6 +123,38 @@ describe('transform edge cases', () => { expect(output).toContain('export type MaybeValue = unknown;') }) + it('should map custom channel regexp strings to template literal types', () => { + const output = transform([ + variable('channel', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('custom:.+') + } + } as any), + group('event-envelope', [ + property('channel', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('custom:.+') + } + } as any) + ]), + variable('email-address', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('[^@]+@[^@]+') + } + } as any) + ]) + + expect(output).toContain('export type Channel = `custom:${string}`;') + expect(output).toContain('channel: `custom:${string}`;') + expect(output).toContain('export type EmailAddress = string;') + }) + it('should keep camelCase fields by default', () => { const output = transform([ group('session-capability-request', [ From f278e6e249b89840ad8c7093b9899c5ff967c48c Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 13 Apr 2026 20:06:31 +0000 Subject: [PATCH 2/4] Fix operator guard regression --- packages/cddl/src/utils.ts | 9 ++++++++- packages/cddl/tests/utils.test.ts | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/cddl/src/utils.ts b/packages/cddl/src/utils.ts index cfe8311..6e1a4c9 100644 --- a/packages/cddl/src/utils.ts +++ b/packages/cddl/src/utils.ts @@ -81,7 +81,14 @@ export function isPropertyReference (t: any): t is PropertyReference { } export function isNativeTypeWithOperator (t: any): t is NativeTypeWithOperator { - return t && typeof t === 'object' && 'Type' in t && 'Operator' in t + return Boolean( + t && + typeof t === 'object' && + 'Type' in t && + !('Value' in t) && + t.Operator && + typeof t.Operator === 'object' + ) } export function getRegexpPattern (t: any): string | undefined { diff --git a/packages/cddl/tests/utils.test.ts b/packages/cddl/tests/utils.test.ts index 554c86c..b431031 100644 --- a/packages/cddl/tests/utils.test.ts +++ b/packages/cddl/tests/utils.test.ts @@ -10,6 +10,7 @@ import type { PropertyReference, Variable } from '../src/ast.js' +import { Type } from '../src/ast.js' import { Tokens, type Token } from '../src/tokens.js' import { getRegexpPattern, @@ -185,7 +186,7 @@ describe('utils', () => { } } const nativeStringTypeWithRegexp: NativeTypeWithOperator = { - Type: 'tstr', + Type: Type.TSTR, Operator: { Type: 'regexp', Value: { From 62d797ba65b9707fcb8a846faa6f127be133fa3a Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 13 Apr 2026 20:16:19 +0000 Subject: [PATCH 3/4] Generalize regexp template inference --- packages/cddl2py/src/index.ts | 44 +++++++++++++++++-- .../tests/transform_edge_cases.test.ts | 30 +++++++++++-- packages/cddl2ts/src/index.ts | 35 +++++++++++++-- .../tests/transform_edge_cases.test.ts | 26 ++++++++++- 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/packages/cddl2py/src/index.ts b/packages/cddl2py/src/index.ts index d66f4e2..ffa1aef 100644 --- a/packages/cddl2py/src/index.ts +++ b/packages/cddl2py/src/index.ts @@ -30,7 +30,6 @@ interface ResolveTypeOptions { } const STRING_RECORD_KEY_TYPES = new Set(['str', 'text', 'tstr']) -const CUSTOM_CHANNEL_REGEXP_PATTERNS = new Set(['custom:.+', '^custom:.+$']) export function transform (assignments: Assignment[], options?: TransformOptions): string { const ctx: Context = { @@ -518,6 +517,37 @@ function getExtraItemsType (props: Property[], ctx: Context): string | undefined return `Union[${uniqueTypes.join(', ')}]` } +function stringifyPythonLiteral (value: string) { + return JSON.stringify(value) +} + +function getTemplateAnnotatedPattern (regexpPattern: string): string | undefined { + const wildcard = '.+' + if (!regexpPattern.includes(wildcard) || /[\\()[\]{}|?*^$]/.test(regexpPattern.replaceAll(wildcard, ''))) { + return + } + + const segments = regexpPattern.split(wildcard) + const parts: string[] = [] + + for (let i = 0; i < segments.length; i++) { + const segment = segments[i] + if (segment.length > 0) { + parts.push(stringifyPythonLiteral(segment)) + } + + if (i < segments.length - 1) { + parts.push('str') + } + } + + if (parts.length === 0 || !parts.includes('str')) { + return + } + + return `Annotated[str, ${parts.join(' + ')}]` +} + function resolveNativeTypeWithOperator (t: NativeTypeWithOperator, ctx: Context): string | undefined { if (typeof t.Type !== 'string') { return @@ -529,7 +559,15 @@ function resolveNativeTypeWithOperator (t: NativeTypeWithOperator, ctx: Context) } const regexpPattern = getRegexpPattern(t) - if (!regexpPattern || !CUSTOM_CHANNEL_REGEXP_PATTERNS.has(regexpPattern)) { + if (!regexpPattern) { + if (mapped === 'Any') { + ctx.typingImports.add('Any') + } + return mapped + } + + const templateAnnotatedPattern = getTemplateAnnotatedPattern(regexpPattern) + if (!templateAnnotatedPattern) { if (mapped === 'Any') { ctx.typingImports.add('Any') } @@ -542,7 +580,7 @@ function resolveNativeTypeWithOperator (t: NativeTypeWithOperator, ctx: Context) return `Annotated[${mapped}, StringConstraints(pattern=${JSON.stringify(regexpPattern)})]` } - return `Annotated[${mapped}, ${JSON.stringify(regexpPattern)}]` + return templateAnnotatedPattern } // --------------------------------------------------------------------------- diff --git a/packages/cddl2py/tests/transform_edge_cases.test.ts b/packages/cddl2py/tests/transform_edge_cases.test.ts index 7a0a1a9..5b7e3de 100644 --- a/packages/cddl2py/tests/transform_edge_cases.test.ts +++ b/packages/cddl2py/tests/transform_edge_cases.test.ts @@ -151,7 +151,7 @@ describe('transform edge cases', () => { expect(output).toContain('Combined = Union[_CombinedVariant0, _CombinedVariant1]') }) - it('should preserve custom channel regexp strings in python-friendly types', () => { + it('should preserve template-like regexp strings in python-friendly types', () => { const typedDictOutput = transform([ variable('channel', { Type: 'tstr', @@ -160,6 +160,27 @@ describe('transform edge cases', () => { Value: literal('custom:.+') } } as any), + variable('prefixed-name', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('foo_.+') + } + } as any), + variable('sandwiched-name', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('some_.+_name') + } + } as any), + variable('multi-slot-name', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('pre_.+_mid_.+_post') + } + } as any), group('event-envelope', [ property('channel', { Type: 'tstr', @@ -188,8 +209,11 @@ describe('transform edge cases', () => { ], { pydantic: true }) expect(typedDictOutput).toContain('from typing import Annotated') - expect(typedDictOutput).toContain('Channel = Annotated[str, "custom:.+"]') - expect(typedDictOutput).toContain('channel: Annotated[str, "custom:.+"]') + expect(typedDictOutput).toContain('Channel = Annotated[str, "custom:" + str]') + expect(typedDictOutput).toContain('PrefixedName = Annotated[str, "foo_" + str]') + expect(typedDictOutput).toContain('SandwichedName = Annotated[str, "some_" + str + "_name"]') + expect(typedDictOutput).toContain('MultiSlotName = Annotated[str, "pre_" + str + "_mid_" + str + "_post"]') + expect(typedDictOutput).toContain('channel: Annotated[str, "custom:" + str]') expect(typedDictOutput).toContain('EmailAddress = str') expect(pydanticOutput).toContain('from pydantic import StringConstraints') expect(pydanticOutput).toContain('Channel = Annotated[str, StringConstraints(pattern="custom:.+")]') diff --git a/packages/cddl2ts/src/index.ts b/packages/cddl2ts/src/index.ts index 22f8552..c8bdbb9 100644 --- a/packages/cddl2ts/src/index.ts +++ b/packages/cddl2ts/src/index.ts @@ -53,7 +53,6 @@ const NATIVE_TYPES: Record = { nil: b.tsNullKeyword(), null: b.tsNullKeyword() } -const CUSTOM_CHANNEL_REGEXP_PATTERNS = new Set(['custom:.+', '^custom:.+$']) const RECORD_KEY_TYPES = new Set([ 'int', 'uint', 'nint', 'integer', 'unsigned', 'number', 'float', 'float16', 'float32', 'float64', 'float16-32', 'float32-64', @@ -622,14 +621,44 @@ function parseTemplateLiteralType (template: string): TSTypeKind { return (ast.program.body[0] as types.namedTypes.TSTypeAliasDeclaration).typeAnnotation } +function escapeTemplateLiteralSegment (segment: string): string { + return segment + .replace(/\\/g, '\\\\') + .replace(/`/g, '\\`') + .replace(/\$\{/g, '\\${') +} + +function regexpPatternToTemplateLiteral (pattern: string): string | undefined { + const normalized = pattern.startsWith('^') && pattern.endsWith('$') + ? pattern.slice(1, -1) + : pattern + + if (!normalized.includes('.+')) { + return + } + + const wildcardOnlyPattern = normalized.replace(/(\.\+)+/g, '') + if (wildcardOnlyPattern.includes('(') || wildcardOnlyPattern.includes('[') || wildcardOnlyPattern.includes('\\')) { + return + } + + const segments = normalized.split(/(?:\.\+)+/g) + if (segments.length <= 1) { + return + } + + return `\`${segments.map(escapeTemplateLiteralSegment).join('${string}')}\`` +} + function parseNativeTypeWithOperator (t: NativeTypeWithOperator): TSTypeKind | undefined { if (typeof t.Type !== 'string') { return } const regexpPattern = getRegexpPattern(t) - if (regexpPattern && CUSTOM_CHANNEL_REGEXP_PATTERNS.has(regexpPattern)) { - return parseTemplateLiteralType('`custom:${string}`') + const templateLiteral = regexpPattern && regexpPatternToTemplateLiteral(regexpPattern) + if (templateLiteral) { + return parseTemplateLiteralType(templateLiteral) } return NATIVE_TYPES[t.Type] diff --git a/packages/cddl2ts/tests/transform_edge_cases.test.ts b/packages/cddl2ts/tests/transform_edge_cases.test.ts index d5bed96..6611cb2 100644 --- a/packages/cddl2ts/tests/transform_edge_cases.test.ts +++ b/packages/cddl2ts/tests/transform_edge_cases.test.ts @@ -123,7 +123,7 @@ describe('transform edge cases', () => { expect(output).toContain('export type MaybeValue = unknown;') }) - it('should map custom channel regexp strings to template literal types', () => { + it('should map simple wildcard regexp strings to template literal types', () => { const output = transform([ variable('channel', { Type: 'tstr', @@ -147,12 +147,36 @@ describe('transform edge cases', () => { Type: 'regexp', Value: literal('[^@]+@[^@]+') } + } as any), + variable('prefixed-name', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('foo_.+') + } + } as any), + variable('wrapped-name', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('some_.+_name') + } + } as any), + variable('double-wildcard', { + Type: 'tstr', + Operator: { + Type: 'regexp', + Value: literal('some_.+_middle_.+') + } } as any) ]) expect(output).toContain('export type Channel = `custom:${string}`;') expect(output).toContain('channel: `custom:${string}`;') expect(output).toContain('export type EmailAddress = string;') + expect(output).toContain('export type PrefixedName = `foo_${string}`;') + expect(output).toContain('export type WrappedName = `some_${string}_name`;') + expect(output).toContain('export type DoubleWildcard = `some_${string}_middle_${string}`;') }) it('should keep camelCase fields by default', () => { From 094128f0b17013f86ac3e7cc66f69ec4dd700bdc Mon Sep 17 00:00:00 2001 From: Christian Bromann Date: Mon, 13 Apr 2026 20:28:47 +0000 Subject: [PATCH 4/4] Add cddl coverage tests --- packages/cddl/tests/cli.test.ts | 51 +++++++++++++++++++++++++++++++ packages/cddl/tests/lexer.test.ts | 8 +++++ packages/cddl/tests/utils.test.ts | 15 +++++++++ 3 files changed, 74 insertions(+) create mode 100644 packages/cddl/tests/cli.test.ts diff --git a/packages/cddl/tests/cli.test.ts b/packages/cddl/tests/cli.test.ts new file mode 100644 index 0000000..ead72cd --- /dev/null +++ b/packages/cddl/tests/cli.test.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { CLI_EPILOGUE } from '../src/cli/constants.js' + +describe('cli entrypoint', () => { + afterEach(() => { + vi.resetModules() + vi.restoreAllMocks() + }) + + it('wires yargs commands and returns argv', async () => { + const command = vi.fn() + const example = vi.fn() + const epilogue = vi.fn() + const demandCommand = vi.fn() + const help = vi.fn() + const argvValue = { _: ['repl'] } + const chain = { + command, + example, + epilogue, + demandCommand, + help, + argv: argvValue + } + + command.mockReturnValue(chain) + example.mockReturnValue(chain) + epilogue.mockReturnValue(chain) + demandCommand.mockReturnValue(chain) + help.mockReturnValue(chain) + + const yargsMock = vi.fn().mockReturnValue(chain) + const hideBinMock = vi.fn().mockReturnValue(['repl']) + + vi.doMock('yargs/yargs', () => ({ default: yargsMock })) + vi.doMock('yargs/helpers', () => ({ hideBin: hideBinMock })) + + const { default: runCli } = await import('../src/cli/index.js') + const result = await runCli() + + expect(hideBinMock).toHaveBeenCalledWith(process.argv) + expect(yargsMock).toHaveBeenCalledWith(['repl']) + expect(command).toHaveBeenCalledTimes(2) + expect(example).toHaveBeenCalledWith('$0 repl', 'Start CDDL repl') + expect(epilogue).toHaveBeenCalledWith(CLI_EPILOGUE) + expect(demandCommand).toHaveBeenCalledTimes(1) + expect(help).toHaveBeenCalledTimes(1) + expect(result).toEqual(argvValue) + }) +}) diff --git a/packages/cddl/tests/lexer.test.ts b/packages/cddl/tests/lexer.test.ts index 7e332cb..6c41d09 100644 --- a/packages/cddl/tests/lexer.test.ts +++ b/packages/cddl/tests/lexer.test.ts @@ -60,4 +60,12 @@ describe('lexer', () => { expect(locEnd.line).toBe(0) expect(locEnd.position).toBe(0) }) + + it('should render caret location info for the current line', () => { + const l = new Lexer('foo\nbar') + l.nextToken() + + expect(l.getLine(1)).toBe('bar') + expect(l.getLocationInfo()).toBe('foo\n ^\n |\n') + }) }) diff --git a/packages/cddl/tests/utils.test.ts b/packages/cddl/tests/utils.test.ts index b431031..e499be5 100644 --- a/packages/cddl/tests/utils.test.ts +++ b/packages/cddl/tests/utils.test.ts @@ -209,8 +209,23 @@ describe('utils', () => { expect(isNativeTypeWithOperator(nativeTypeWithOperator)).toBe(true) expect(isNativeTypeWithOperator(nativeStringTypeWithRegexp)).toBe(true) expect(isNativeTypeWithOperator({ Type: 'tstr' })).toBe(false) + expect(isNativeTypeWithOperator({ + Type: Type.TSTR, + Operator: 'regexp' + })).toBe(false) expect(getRegexpPattern(nativeStringTypeWithRegexp)).toBe('custom:.+') expect(getRegexpPattern(nativeTypeWithOperator)).toBeUndefined() + expect(getRegexpPattern({ + Type: Type.TSTR, + Operator: { + Type: 'regexp', + Value: { + Type: 'literal', + Value: 42, + Unwrapped: false + } + } + })).toBeUndefined() expect(isRange({ Type: rangeReference })).toBe(true) expect(isRange({ Type: 'range' })).toBe(false)