diff --git a/src/compiler/deserialization/bigint.ucrx.class.ts b/src/compiler/deserialization/bigint.ucrx.class.ts index 97891b92..42b8a1c9 100644 --- a/src/compiler/deserialization/bigint.ucrx.class.ts +++ b/src/compiler/deserialization/bigint.ucrx.class.ts @@ -33,13 +33,14 @@ export class BigIntUcrxClass extends UcrxClass) { super({ + lib: baseClass.lib, schema: baseClass.schema, baseClass, typeName: `${baseClass.typeName}$Entry`, diff --git a/src/compiler/deserialization/mod.ts b/src/compiler/deserialization/mod.ts index b54daec9..19e486bb 100644 --- a/src/compiler/deserialization/mod.ts +++ b/src/compiler/deserialization/mod.ts @@ -13,6 +13,7 @@ export * from './ucd-handler-feature.js'; export * from './ucd-lib.js'; export * from './ucd-models.js'; export * from './ucd-support-defaults.js'; +export * from './ucd-support-inset.js'; export * from './ucd-support-non-finite.js'; export * from './ucd-support-primitives.js'; export * from './unknown.ucrx.class.js'; diff --git a/src/compiler/deserialization/number.ucrx.class.ts b/src/compiler/deserialization/number.ucrx.class.ts index f4e70374..7da34bd8 100644 --- a/src/compiler/deserialization/number.ucrx.class.ts +++ b/src/compiler/deserialization/number.ucrx.class.ts @@ -32,14 +32,11 @@ export class NumberUcrxClass extends UcrxClass { it('enables feature in object form', async () => { const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', Number], + readTimestamp: { model: Number, mode: 'sync' }, }, features: [ { @@ -67,7 +67,7 @@ describe('UcdCompiler', () => { }; const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', schema], + readTimestamp: { model: schema, mode: 'sync' }, }, }); @@ -88,7 +88,7 @@ describe('UcdCompiler', () => { }; const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', schema], + readTimestamp: { model: schema, mode: 'sync' }, }, }); @@ -109,7 +109,7 @@ describe('UcdCompiler', () => { }; const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', schema], + readTimestamp: { model: schema, mode: 'sync' }, }, }); @@ -130,8 +130,8 @@ describe('UcdCompiler', () => { }; await expect( - new UcdCompiler<{ readTimestamp: UcModel }>({ - models: { readTimestamp: schema }, + new UcdCompiler({ + models: { readTimestamp: { model: schema } }, }).bootstrap(), ).rejects.toThrow( new ReferenceError( @@ -151,8 +151,8 @@ describe('UcdCompiler', () => { }; await expect( - new UcdCompiler<{ readTimestamp: UcModel }>({ - models: { readTimestamp: schema }, + new UcdCompiler({ + models: { readTimestamp: { model: schema } }, }).bootstrap(), ).rejects.toThrow( new ReferenceError( @@ -166,7 +166,7 @@ describe('UcdCompiler', () => { describe('ES2015', () => { it('compiles async module', async () => { const compiler = new UcdCompiler({ - models: { readValue: ['async', Number] }, + models: { readValue: { model: Number, mode: 'async' } }, }); const text = await compiler.generate(); @@ -191,7 +191,7 @@ export async function readValue( }); it('compiles sync module', async () => { const compiler = new UcdCompiler({ - models: { readValue: ['sync', Number] }, + models: { readValue: { model: Number, mode: 'sync' } }, }); const text = await compiler.generate(); @@ -215,7 +215,7 @@ export function readValue( }); it('compiles universal module', async () => { const compiler = new UcdCompiler({ - models: { readValue: Number }, + models: { readValue: { model: Number } }, }); const text = await compiler.generate(); @@ -243,7 +243,7 @@ export function readValue( describe('IIFE', () => { it('creates async factory', async () => { const compiler = new UcdCompiler({ - models: { readValue: ['async', Number] }, + models: { readValue: { model: Number, mode: 'async' } }, }); const text = await compiler.generate({ format: EsBundleFormat.IIFE }); @@ -268,7 +268,7 @@ export function readValue( }); it('creates sync factory', async () => { const compiler = new UcdCompiler({ - models: { readValue: ['sync', Number] }, + models: { readValue: { model: Number, mode: 'sync' } }, }); const text = await compiler.generate({ format: EsBundleFormat.IIFE }); @@ -293,7 +293,7 @@ export function readValue( }); it('creates universal factory', async () => { const compiler = new UcdCompiler({ - models: { readValue: Number }, + models: { readValue: { model: Number } }, }); const text = await compiler.generate({ format: EsBundleFormat.IIFE }); diff --git a/src/compiler/deserialization/ucd-compiler.ts b/src/compiler/deserialization/ucd-compiler.ts index f5a4b7d6..d92b50b2 100644 --- a/src/compiler/deserialization/ucd-compiler.ts +++ b/src/compiler/deserialization/ucd-compiler.ts @@ -15,6 +15,7 @@ import { esline, } from 'esgen'; import { capitalize } from 'httongue'; +import { UcPresentationName } from '../../schema/uc-presentations.js'; import { UcSchema } from '../../schema/uc-schema.js'; import { UcdHandlerRegistry } from '../impl/ucd-handler-registry.js'; import { UccConfig } from '../processor/ucc-config.js'; @@ -22,10 +23,9 @@ import { UccFeature } from '../processor/ucc-feature.js'; import { UcrxLib } from '../rx/ucrx-lib.js'; import { UcrxProcessor } from '../rx/ucrx-processor.js'; import { UcrxClass, UcrxSignature } from '../rx/ucrx.class.js'; -import { UcdFunction } from './ucd-function.js'; import { UcdHandlerFeature } from './ucd-handler-feature.js'; import { UcdLib } from './ucd-lib.js'; -import { UcdExports, UcdModels, isUcdModelConfig } from './ucd-models.js'; +import { UcdExports, UcdModels } from './ucd-models.js'; import { ucdSupportDefaults } from './ucd-support-defaults.js'; /** @@ -53,11 +53,12 @@ export class UcdCompiler< * @param options - Compiler options. */ constructor(options: UcdCompiler.Options) { - const { models, validate = true, features } = options; + const { models, presentations, validate = true, features } = options; super({ - names: validate ? ['validator', 'deserializer'] : ['deserializer'], - models: Object.values(models).map(entry => (isUcdModelConfig(entry) ? entry[1] : entry)), + processors: validate ? ['validator', 'deserializer'] : ['deserializer'], + presentations, + models: Object.values(models).map(({ model }) => model), features, }); @@ -92,7 +93,7 @@ export class UcdCompiler< // Stop registering default handlers. // Start registering custom ones. - defaultConfig.configure(); + defaultConfig.configure(undefined, {}); this.#entities.makeDefault(); this.#formats.makeDefault(); @@ -343,19 +344,15 @@ export class UcdCompiler< export namespace UcdCompiler { export type Any = UcdCompiler; - export interface Options extends Omit { + export interface Options + extends Omit { readonly models: TModels; + readonly presentations?: UcPresentationName | UcPresentationName[] | undefined; readonly validate?: boolean | undefined; readonly features?: | UccFeature | readonly UccFeature[] | undefined; - readonly inset?: EsSnippet | undefined; readonly exportDefaults?: boolean | undefined; - - createDeserializer?>( - this: void, - options: UcdFunction.Options, - ): UcdFunction; } } diff --git a/src/compiler/deserialization/ucd-function.ts b/src/compiler/deserialization/ucd-function.ts index b142e45e..6a5ae6ba 100644 --- a/src/compiler/deserialization/ucd-function.ts +++ b/src/compiler/deserialization/ucd-function.ts @@ -1,40 +1,34 @@ -import { EsCode, EsFunction, EsSnippet, EsSymbol, EsVarKind, EsVarSymbol, esline } from 'esgen'; -import { UcDeserializer } from '../../schema/uc-deserializer.js'; +import { + EsCallable, + EsCode, + EsFunction, + EsSnippet, + EsSymbol, + EsVarKind, + EsVarSymbol, + esline, +} from 'esgen'; import { ucModelName } from '../../schema/uc-model-name.js'; import { UcSchema } from '../../schema/uc-schema.js'; import { UnsupportedUcSchemaError } from '../common/unsupported-uc-schema.error.js'; -import { UC_MODULE_DESERIALIZER } from '../impl/uc-modules.js'; +import { UC_MODULE_CHURI, UC_MODULE_DESERIALIZER } from '../impl/uc-modules.js'; import { ucSchemaTypeSymbol } from '../impl/uc-schema-symbol.js'; +import { UcrxInsetSignature } from '../rx/ucrx-inset-method.js'; import { UcrxClass } from '../rx/ucrx.class.js'; import { UcdExportSignature } from './ucd-export.signature.js'; import { UcdLib } from './ucd-lib.js'; +import { UcdModels } from './ucd-models.js'; export class UcdFunction = UcSchema> { readonly #lib: UcdLib.Any; readonly #schema: TSchema; #ucrxClass?: UcrxClass; - readonly #createAsyncReader: Exclude< - UcdFunction.Options['createAsyncReader'], - undefined - >; - - readonly #createSyncReader: Exclude< - UcdFunction.Options['createSyncReader'], - undefined - >; constructor(options: UcdFunction.Options); - constructor({ - lib, - schema, - createAsyncReader: createReader = UcdFunction$createReader, - createSyncReader = UcdFunction$createSyncReader, - }: UcdFunction.Options) { + constructor({ lib, schema }: UcdFunction.Options) { this.#lib = lib; this.#schema = schema; - this.#createAsyncReader = createReader; - this.#createSyncReader = createSyncReader; } get lib(): UcdLib.Any { @@ -64,8 +58,11 @@ export class UcdFunction = UcSc return this.#ucrxClass; } - exportFn(externalName: string, mode: UcDeserializer.Mode): EsFunction { - const { opaqueUcrx, defaultEntities, defaultFormats, onMeta, inset } = this.lib; + exportFn( + externalName: string, + { mode = 'universal', lexer, inset }: UcdModels.SchemaEntry, + ): EsFunction { + const { opaqueUcrx, defaultEntities, defaultFormats, onMeta } = this.lib; const stream = new EsSymbol('stream'); const options = (code: EsCode): void => { code.multiLine(code => { @@ -78,7 +75,15 @@ export class UcdFunction = UcSc 'formats,', 'onMeta,', opaqueUcrx ? esline`opaqueRx: ${opaqueUcrx.instantiate()},` : EsCode.none, - inset ? esline`inset: ${inset},` : EsCode.none, + inset + ? code => { + code.line( + 'inset: ', + new EsCallable(UcrxInsetSignature).lambda(({ args }) => inset(args)), + ',', + ); + } + : EsCode.none, ) .write('}'); }); @@ -94,8 +99,8 @@ export class UcdFunction = UcSc code.write( mode === 'universal' - ? this.#universalBody({ input, options }) - : this.#nonUniversalBody(mode, { input, options }), + ? this.#universalBody(lexer, { input, options }) + : this.#nonUniversalBody(mode, lexer, { input, options }), ); }, args: { @@ -128,7 +133,11 @@ export class UcdFunction = UcSc }); } - #nonUniversalBody(mode: 'sync' | 'async', args: UcdExportSignature.AllValues): EsSnippet { + #nonUniversalBody( + mode: 'sync' | 'async', + lexer: UcdModels.Entry['lexer'], + args: UcdExportSignature.AllValues, + ): EsSnippet { return code => { const result = new EsVarSymbol('result'); const reader = new EsVarSymbol('reader'); @@ -138,8 +147,8 @@ export class UcdFunction = UcSc .write( reader.declare({ value: () => mode === 'async' - ? this.#createAsyncReader(args, this) - : this.#createSyncReader(args, this), + ? this.#createAsyncReader(lexer, args) + : this.#createSyncReader(lexer, args), }), ) .write(`try {`) @@ -155,7 +164,7 @@ export class UcdFunction = UcSc }; } - #universalBody(args: UcdExportSignature.AllValues): EsSnippet { + #universalBody(lexer: UcdModels.Entry['lexer'], args: UcdExportSignature.AllValues): EsSnippet { return code => { const result = new EsVarSymbol('result'); const syncReader = new EsVarSymbol('syncReader'); @@ -164,7 +173,7 @@ export class UcdFunction = UcSc code .write(result.declare({ as: EsVarKind.Let })) - .write(syncReader.declare({ value: () => this.#createSyncReader(args, this) })) + .write(syncReader.declare({ value: () => this.#createSyncReader(lexer, args) })) .write(esline`if (${syncReader}) {`) .indent(code => { code @@ -180,7 +189,7 @@ export class UcdFunction = UcSc .write('return result;'); }) .write(`}`) - .write(reader.declare({ value: () => this.#createAsyncReader(args, this) })) + .write(reader.declare({ value: () => this.#createAsyncReader(lexer, args) })) .write( esline`return ${reader}.read(${this.ucrxClass.instantiate({ set, @@ -190,24 +199,48 @@ export class UcdFunction = UcSc }; } + #createAsyncReader( + lexer: UcdModels.Entry['lexer'], + { input, options }: UcdExportSignature.AllValues, + ): EsSnippet { + let stream: EsSnippet; + + if (lexer) { + const UcLexerStream = UC_MODULE_CHURI.import('UcLexerStream'); + const createLexer = new EsCallable({ emit: {} }).lambda(({ args }) => lexer(args)); + + stream = esline`${input}.pipeThrough(new ${UcLexerStream}(${createLexer}))`; + } else { + stream = input; + } + + const AsyncUcdReader = UC_MODULE_DESERIALIZER.import('AsyncUcdReader'); + + return esline`new ${AsyncUcdReader}(${stream}, ${options})`; + } + + #createSyncReader( + lexer: UcdModels.Entry['lexer'], + { input, options }: UcdExportSignature.AllValues, + ): EsSnippet { + if (lexer) { + const createSyncUcdLexer = UC_MODULE_DESERIALIZER.import('createSyncUcdLexer'); + const createLexer = new EsCallable({ emit: {} }).lambda(({ args }) => lexer(args)); + + return esline`${createSyncUcdLexer}(${input}, ${createLexer}, ${options})`; + } + + const createSyncUcdReader = UC_MODULE_DESERIALIZER.import('createSyncUcdReader'); + + return esline`${createSyncUcdReader}(${input}, ${options})`; + } + } export namespace UcdFunction { export interface Options> { readonly lib: UcdLib.Any; readonly schema: TSchema; - - createAsyncReader?( - this: void, - args: UcdExportSignature.AllValues, - deserializer: UcdFunction, - ): EsSnippet; - - createSyncReader?( - this: void, - args: UcdExportSignature.AllValues, - deserializer: UcdFunction, - ): EsSnippet; } export interface Args { @@ -261,15 +294,3 @@ export namespace UcdFunction { readonly suffix: string; } } - -function UcdFunction$createReader({ input, options }: UcdExportSignature.AllValues): EsSnippet { - const AsyncUcdReader = UC_MODULE_DESERIALIZER.import('AsyncUcdReader'); - - return esline`new ${AsyncUcdReader}(${input}, ${options})`; -} - -function UcdFunction$createSyncReader({ input, options }: UcdExportSignature.AllValues): EsSnippet { - const createSyncUcdReader = UC_MODULE_DESERIALIZER.import('createSyncUcdReader'); - - return esline`${createSyncUcdReader}(${input}, ${options})`; -} diff --git a/src/compiler/deserialization/ucd-lib.ts b/src/compiler/deserialization/ucd-lib.ts index d769c659..aecf9dd7 100644 --- a/src/compiler/deserialization/ucd-lib.ts +++ b/src/compiler/deserialization/ucd-lib.ts @@ -1,12 +1,11 @@ import { EsBundle, EsCallable, EsNamespace, EsSnippet, esline } from 'esgen'; -import { UcDeserializer } from '../../schema/uc-deserializer.js'; -import { UcSchema, ucSchema } from '../../schema/uc-schema.js'; +import { UcModel, UcSchema, ucSchema } from '../../schema/uc-schema.js'; import { UC_MODULE_DESERIALIZER_META } from '../impl/uc-modules.js'; import { UccSchemaIndex } from '../processor/ucc-schema-index.js'; import { UcrxLib } from '../rx/ucrx-lib.js'; import { UcrxClass, UcrxSignature } from '../rx/ucrx.class.js'; import { UcdFunction } from './ucd-function.js'; -import { UcdModels, isUcdModelConfig } from './ucd-models.js'; +import { UcdModels } from './ucd-models.js'; /** * Deserializer library allocated by {@link UcdCompiler#bootstrap compiler}. @@ -16,10 +15,9 @@ import { UcdModels, isUcdModelConfig } from './ucd-models.js'; export class UcdLib extends UcrxLib { readonly #schemaIndex: UccSchemaIndex; - readonly #models: UcdModelConfigs; + readonly #models: UcdSchemaConfigs; readonly #options: UcdLib.Options; - readonly #createDeserializer: Exclude['createDeserializer'], undefined>; readonly #deserializers = new Map(); readonly #defaultEntities: EsSnippet; @@ -29,22 +27,13 @@ export class UcdLib extends UcrxLib { constructor(bundle: EsBundle, options: UcdLib.Options); constructor({ ns }: EsBundle, options: UcdLib.Options) { - const { - schemaIndex, - models, - exportDefaults, - entities, - formats, - meta, - createDeserializer = options => new UcdFunction(options), - } = options; + const { schemaIndex, models, exportDefaults, entities, formats, meta } = options; super(options); this.#schemaIndex = schemaIndex; this.#options = options; this.#models = this.#createModels(models); - this.#createDeserializer = createDeserializer; const exportNs = exportDefaults ? ns : undefined; @@ -55,22 +44,23 @@ export class UcdLib extends UcrxLib { this.#declareDeserializers(ns); } - #createModels(models: TModels): UcdModelConfigs { + #createModels(models: TModels): UcdSchemaConfigs { return Object.fromEntries( Object.entries(models).map(([externalName, entry]) => [ externalName, - isUcdModelConfig(entry) - ? { schema: ucSchema(entry[1]), mode: entry[0] } - : { schema: ucSchema(entry), mode: 'universal' }, + { + ...entry, + model: ucSchema(entry.model), + }, ]), - ) as UcdModelConfigs; + ) as UcdSchemaConfigs; } #declareDeserializers(ns: EsNamespace): void { - for (const [externalName, { schema, mode }] of Object.entries(this.#models)) { - const fn = this.deserializerFor(schema); + for (const [externalName, config] of Object.entries(this.#models)) { + const fn = this.deserializerFor(config.model); - ns.refer(fn.exportFn(externalName, mode)); + ns.refer(fn.exportFn(externalName, config)); } for (const { schema, whenCompiled } of this.#options.internalModels) { @@ -114,10 +104,6 @@ export class UcdLib extends UcrxLib { return this.#onMeta; } - get inset(): EsSnippet | undefined { - return this.#options.inset; - } - deserializerFor = UcSchema>( schema: TSchema, ): UcdFunction { @@ -125,7 +111,7 @@ export class UcdLib extends UcrxLib { let deserializer = this.#deserializers.get(schemaId) as UcdFunction | undefined; if (!deserializer) { - deserializer = this.#createDeserializer({ + deserializer = new UcdFunction({ lib: this as UcdLib, schema, }); @@ -152,13 +138,7 @@ export namespace UcdLib { formats(this: void, exportNs?: EsNamespace): EsSnippet; meta(this: void, exportNs?: EsNamespace): EsSnippet; onMeta?: EsSnippet | undefined; - readonly inset?: EsSnippet | undefined; readonly exportDefaults?: boolean | undefined; - - createDeserializer?>( - this: void, - options: UcdFunction.Options, - ): UcdFunction; } export interface InternalModel = UcSchema> { @@ -167,17 +147,10 @@ export namespace UcdLib { } } -type UcdModelConfigs = { - readonly [externalName in keyof TModels]: UcdModelConfig< - UcSchema.Of>, - UcdModels.ModeOf +type UcdSchemaConfigs = { + readonly [externalName in keyof TModels]: UcdSchemaConfig< + UcdModels.ModelOf >; }; -interface UcdModelConfig< - out TSchema extends UcSchema = UcSchema, - out TMode extends UcDeserializer.Mode = UcDeserializer.Mode, -> { - readonly schema: TSchema; - readonly mode: TMode; -} +type UcdSchemaConfig = UcdModels.Entry>; diff --git a/src/compiler/deserialization/ucd-models.ts b/src/compiler/deserialization/ucd-models.ts index d9280f65..4a3bb9f5 100644 --- a/src/compiler/deserialization/ucd-models.ts +++ b/src/compiler/deserialization/ucd-models.ts @@ -1,37 +1,64 @@ +import { EsSnippet } from 'esgen'; import { UcDeserializer } from '../../schema/uc-deserializer.js'; -import { UcInfer, UcModel } from '../../schema/uc-schema.js'; +import { UcInfer, UcModel, UcSchema } from '../../schema/uc-schema.js'; +import { UcrxInsetSignature } from '../rx/ucrx-inset-method.js'; export interface UcdModels { readonly [reader: string]: UcdModels.Entry; } -export function isUcdModelConfig( - entry: UcdModels.Entry, -): entry is UcdModels.ModelConfig { - return Array.isArray(entry); -} - export namespace UcdModels { - export type Entry = TModel | ModelConfig; + export interface BaseEntry { + readonly model: TModel; + readonly mode?: UcDeserializer.Mode | undefined; + readonly inset?: ((this: void, args: UcrxInsetSignature.Values) => EsSnippet) | undefined; + } + + export type Entry = + | SyncEntry + | AsyncEntry + | UniversalEntry + | AsyncLexerEntry + | LexerEntry; + + export interface SyncEntry extends BaseEntry { + readonly mode: 'sync'; + readonly lexer?: ((this: void, args: { emit: EsSnippet }) => EsSnippet) | undefined; + } + + export interface AsyncEntry extends BaseEntry { + readonly mode: 'async'; + readonly lexer?: undefined; + } + + export interface AsyncLexerEntry extends BaseEntry { + readonly mode: 'async'; + readonly lexer: (this: void, args: { emit: EsSnippet }) => EsSnippet; + } + + export interface UniversalEntry extends BaseEntry { + readonly mode?: 'universal' | undefined; + readonly lexer?: undefined; + } - export type ModelConfig< - TModel extends UcModel = UcModel, - TMode extends UcDeserializer.Mode = UcDeserializer.Mode, - > = readonly [TMode, TModel]; + export interface LexerEntry extends BaseEntry { + readonly mode?: 'universal' | undefined; + readonly lexer: (this: void, args: { emit: EsSnippet }) => EsSnippet; + } - export type ModeOf = TEntry extends ModelConfig - ? TMode - : 'universal'; + export type ModelOf = TEntry extends Entry ? TModel : never; - export type ModelOf = TEntry extends ModelConfig - ? TModel - : TEntry; + export type SchemaEntry = UcSchema> = Entry; } export type UcdExports = { - readonly [reader in keyof TModels]: UcdModels.ModeOf extends 'sync' + readonly [reader in keyof TModels]: TModels[reader] extends UcdModels.SyncEntry ? UcDeserializer.Sync>> - : UcdModels.ModeOf extends 'async' + : TModels[reader] extends UcdModels.AsyncEntry + ? UcDeserializer.AsyncByTokens>> + : TModels[reader] extends UcdModels.AsyncLexerEntry ? UcDeserializer.Async>> + : TModels[reader] extends UcdModels.UniversalEntry + ? UcDeserializer.ByTokens>> : UcDeserializer>>; }; diff --git a/src/compiler/deserialization/ucd-support-inset.ts b/src/compiler/deserialization/ucd-support-inset.ts new file mode 100644 index 00000000..3fde1df4 --- /dev/null +++ b/src/compiler/deserialization/ucd-support-inset.ts @@ -0,0 +1,58 @@ +import { esImport, esMemberAccessor, esline } from 'esgen'; +import { UcPresentationName } from '../../schema/uc-presentations.js'; +import { UcSchema } from '../../schema/uc-schema.js'; +import { UC_TOKEN_INSET_URI_PARAM } from '../../syntax/uc-token.js'; +import { UcrxCore$stubBody } from '../impl/ucrx-core.stub.js'; +import { UccConfig } from '../processor/ucc-config.js'; +import { UcrxCore } from '../rx/ucrx-core.js'; +import { UcdCompiler } from './ucd-compiler.js'; + +export function ucdSupportInset( + compiler: UcdCompiler.Any, + schema: UcSchema, +): UccConfig { + return { + configure({ lexer, from, method, args }, { within }) { + compiler + .modifyUcrxClass(schema, { + applyTo(ucrxClass) { + if (!ucrxClass.findMember(UcrxCore.ins)?.declared) { + UcrxCore.ins.overrideIn(ucrxClass, { + body: UcrxCore$stubBody, + }); + } + }, + }) + .modifyUcrxMethod(schema, UcrxCore.ins, { + inset: { + insetId: within && (UC_PRESENTATION_INSET_ID[within] ?? within), + createLexer({ + member: { + args: { emit }, + }, + }) { + const UcLexer = esImport(from, lexer); + const extraArgs = args?.length ? ', ' + args.join(', ') : ''; + + return method + ? esline`${UcLexer}${esMemberAccessor(method).accessor}(${emit}${extraArgs})` + : esline`new ${UcLexer}(${emit}${extraArgs})`; + }, + }, + }); + }, + }; +} + +export interface UcdInsetOptions { + readonly lexer: string; + readonly from: string; + readonly method?: string | undefined; + readonly args?: readonly string[] | undefined; +} + +const UC_PRESENTATION_INSET_ID: { + readonly [presentation in UcPresentationName]?: number | undefined; +} = { + uriParam: UC_TOKEN_INSET_URI_PARAM, +}; diff --git a/src/compiler/deserialization/unknown.ucrx.class.ts b/src/compiler/deserialization/unknown.ucrx.class.ts index aba1fc77..6abb85d2 100644 --- a/src/compiler/deserialization/unknown.ucrx.class.ts +++ b/src/compiler/deserialization/unknown.ucrx.class.ts @@ -52,6 +52,7 @@ export class UnknownUcrxClass extends UcrxClass { constructor(lib: UcrxLib, schema: UcSchema) { super({ + lib, typeName: schema.nullable ? 'Any' : 'NonNull', schema, baseClass: lib.baseUcrx, diff --git a/src/compiler/impl/uc-value.compiler.ts b/src/compiler/impl/uc-value.compiler.ts index c9543aa6..50f6c46b 100644 --- a/src/compiler/impl/uc-value.compiler.ts +++ b/src/compiler/impl/uc-value.compiler.ts @@ -2,30 +2,42 @@ import { EsFunction, esline } from 'esgen'; import { UcUnknown, ucUnknown } from '../../schema/unknown/uc-unknown.js'; import { UcdCompiler } from '../deserialization/ucd-compiler.js'; import { UcdLib } from '../deserialization/ucd-lib.js'; +import { UcdModels } from '../deserialization/ucd-models.js'; import { UcrxLib } from '../rx/ucrx-lib.js'; -export class UcValueCompiler extends UcdCompiler<{ parseUcValue: ['sync', UcUnknown.Schema] }> { +export class UcValueCompiler extends UcdCompiler<{ + parseUcValue: UcdModels.SyncEntry; +}> { constructor() { super({ - models: { parseUcValue: ['sync', ucUnknown()] }, + models: { + parseUcValue: { + model: ucUnknown(), + mode: 'sync', + }, + }, }); } override async bootstrapOptions(): Promise< - UcdLib.Options<{ parseUcValue: ['sync', UcUnknown.Schema] }> + UcdLib.Options<{ parseUcValue: UcdModels.SyncEntry }> > { const options = await super.bootstrapOptions(); const onMeta = new EsFunction( 'onMeta$byDefault', - { cx: {}, rx: {}, attr: {} }, + { + cx: {}, + rx: {}, + attr: {}, + }, { declare: { at: 'exports', body: ({ args: { cx, attr } }) => (code, scope) => { const ucrxLib = scope.get(UcrxLib); - const ucrxClass = ucrxLib.ucrxClassFor(options.models.parseUcValue[1]); + const ucrxClass = ucrxLib.ucrxClassFor(options.models.parseUcValue.model); code.write(esline`return new ${ucrxClass}($ => ${cx}.meta.add(${attr}, $));`); }, diff --git a/src/compiler/impl/uri-charge.compiler.ts b/src/compiler/impl/uri-charge.compiler.ts index 9729b20a..ce49b96b 100644 --- a/src/compiler/impl/uri-charge.compiler.ts +++ b/src/compiler/impl/uri-charge.compiler.ts @@ -9,6 +9,7 @@ import { ListUcrxClass } from '../deserialization/list.ucrx.class.js'; import { MapUcrxClass, MapUcrxStore } from '../deserialization/map.ucrx.class.js'; import { UcdCompiler } from '../deserialization/ucd-compiler.js'; import { UcdLib } from '../deserialization/ucd-lib.js'; +import { UcdModels } from '../deserialization/ucd-models.js'; import { ucdSupportDefaults } from '../deserialization/ucd-support-defaults.js'; import { UnknownUcrxClass } from '../deserialization/unknown.ucrx.class.js'; import { UcrxCore } from '../rx/ucrx-core.js'; @@ -23,12 +24,12 @@ import { } from './uc-modules.js'; export class URIChargeCompiler extends UcdCompiler<{ - parseURICharge: ['sync', UcSchema]; + parseURICharge: UcdModels.SyncEntry>; }> { constructor() { super({ - models: { parseURICharge: ['sync', URICharge$Schema] }, + models: { parseURICharge: { model: URICharge$Schema, mode: 'sync' } }, features(compiler) { return { configure: () => { @@ -50,7 +51,7 @@ export class URIChargeCompiler extends UcdCompiler<{ } override async bootstrapOptions(): Promise< - UcdLib.Options<{ parseURICharge: ['sync', UcSchema] }> + UcdLib.Options<{ parseURICharge: UcdModels.SyncEntry> }> > { const options = await super.bootstrapOptions(); diff --git a/src/compiler/processor/ucc-config.ts b/src/compiler/processor/ucc-config.ts index 46d68365..0ee38a38 100644 --- a/src/compiler/processor/ucc-config.ts +++ b/src/compiler/processor/ucc-config.ts @@ -1,3 +1,5 @@ +import { UcPresentationName } from '../../schema/uc-presentations.js'; + /** * Schema processing configuration. * @@ -12,6 +14,17 @@ export interface UccConfig { * May be called multiple times. * * @param options - Configuration options. + * @param context - Configuration context. */ - configure(options: TOptions): void; + configure(options: TOptions, context: UccConfigContext): void; +} + +/** + * Schema processing configuration context. + */ +export interface UccConfigContext { + /** + * Presentation name the feature is applied in. + */ + readonly within?: UcPresentationName | undefined; } diff --git a/src/compiler/processor/ucc-processor.ts b/src/compiler/processor/ucc-processor.ts index 3a35af64..795c5f04 100644 --- a/src/compiler/processor/ucc-processor.ts +++ b/src/compiler/processor/ucc-processor.ts @@ -1,8 +1,13 @@ import { asArray, lazyValue, mayHaveProperties } from '@proc7ts/primitives'; import { esQuoteKey, esStringLiteral } from 'esgen'; -import { UcFeatureConstraint, UcProcessorName } from '../../schema/uc-constraints.js'; +import { + UcConstraints, + UcFeatureConstraint, + UcProcessorName, +} from '../../schema/uc-constraints.js'; +import { UcPresentationName } from '../../schema/uc-presentations.js'; import { UcModel, UcSchema, ucSchema } from '../../schema/uc-schema.js'; -import { UccConfig } from './ucc-config.js'; +import { UccConfig, UccConfigContext } from './ucc-config.js'; import { UccFeature } from './ucc-feature.js'; import { UccSchemaFeature } from './ucc-schema-feature.js'; import { UccSchemaIndex } from './ucc-schema-index.js'; @@ -29,8 +34,11 @@ export abstract class UccProcessor); - constructor({ names, models, features }: UccProcessorInit) { - this.#schemaIndex = new UccSchemaIndex(asArray(names)); + constructor({ processors, presentations = [], models, features }: UccProcessorInit) { + this.#schemaIndex = new UccSchemaIndex( + asArray(processors), + asArray(presentations), + ); this.#models = models; this.#features = features && asArray(features); } @@ -39,13 +47,6 @@ export abstract class UccProcessor(model: UcModel): this { const schema = ucSchema(model); - for (const name of this.names) { - asArray(schema.where?.[name]).forEach(useFeature => this.#useFeature(schema, useFeature)); + this.#applyConstraints(schema, schema.where, {}); + for (const within of this.schemaIndex.listPresentations(schema.within)) { + this.#applyConstraints(schema, schema.within![within], { within }); } return this; } + #applyConstraints( + schema: UcSchema, + constraints: UcConstraints | undefined, + context: UccConfigContext, + ): void { + for (const processorName of this.schemaIndex.processors) { + asArray(constraints?.[processorName]).forEach(feature => this.#useFeature(schema, feature, context)); + } + } + #useFeature( schema: UcSchema, { use: feature, from, with: options }: UcFeatureConstraint, + context: UccConfigContext, ): void { const useId = `${this.schemaIndex.schemaId(schema)}::${from}::${feature}`; let use = this.#uses.get(useId) as UccProcessor$FeatureUse | undefined; @@ -133,7 +146,7 @@ export abstract class UccProcessor { @@ -180,7 +193,14 @@ export interface UccProcessorInit> { /** * Processor names within {@link churi!UcConstraints schema constraints}. */ - readonly names: UcProcessorName | readonly UcProcessorName[]; + readonly processors: UcProcessorName | readonly UcProcessorName[]; + + /** + * Schema instance presentation names within {@link churi!UcPresentations presentation constraints}. + * + * All presentations enabled when missing or empty. + */ + readonly presentations?: UcPresentationName | readonly UcPresentationName[] | undefined; /** * Models with constraints to extract processing instructions from. @@ -201,7 +221,7 @@ class UccProcessor$FeatureUse, TO readonly #schema: UcSchema; readonly #from: string; readonly #name: string; - readonly #options: TOptions[] = []; + readonly #options: [TOptions, UccConfigContext][] = []; #enabled = false; constructor(schema: UcSchema, from: string, name: string) { @@ -210,8 +230,8 @@ class UccProcessor$FeatureUse, TO this.#name = name; } - configure(options: TOptions): void { - this.#options.push(options); + configure(options: TOptions, context: UccConfigContext): void { + this.#options.push([options, context]); } async enableIn(processor: TProcessor): Promise { @@ -259,8 +279,8 @@ class UccProcessor$FeatureUse, TO } #configure(config: UccConfig): void { - for (const options of this.#options) { - config.configure(options); + for (const [options, context] of this.#options) { + config.configure(options, context); } } diff --git a/src/compiler/processor/ucc-schema-index.spec.ts b/src/compiler/processor/ucc-schema-index.spec.ts index fcf5c2fb..7dd1bd1e 100644 --- a/src/compiler/processor/ucc-schema-index.spec.ts +++ b/src/compiler/processor/ucc-schema-index.spec.ts @@ -1,4 +1,5 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; +import { ucItHasMaxChars } from 'churi'; import { ucBoolean } from '../../schema/boolean/uc-boolean.js'; import { ucBigInt } from '../../schema/numeric/uc-bigint.js'; import { ucNumber } from '../../schema/numeric/uc-number.js'; @@ -12,7 +13,7 @@ describe('UccSchemaIndex', () => { let index: UccSchemaIndex; beforeEach(() => { - index = new UccSchemaIndex(['deserializer', 'validator']); + index = new UccSchemaIndex(['deserializer', 'validator'], []); }); it('uses stable IDs for primitives', () => { @@ -34,4 +35,10 @@ describe('UccSchemaIndex', () => { it('provides different IDs for optional and required types', () => { expect(index.schemaId(ucOptional(ucNumber()))).not.toBe(index.schemaId(ucNumber())); }); + it('distinguishes between presentations', () => { + const index2 = new UccSchemaIndex(['deserializer', 'validator'], ['uriParam']); + const schema = ucString({ within: { charge: ucItHasMaxChars(13) } }); + + expect(index.schemaId(schema)).not.toBe(index2.schemaId(schema)); + }); }); diff --git a/src/compiler/processor/ucc-schema-index.ts b/src/compiler/processor/ucc-schema-index.ts index c048a996..e919b525 100644 --- a/src/compiler/processor/ucc-schema-index.ts +++ b/src/compiler/processor/ucc-schema-index.ts @@ -1,24 +1,48 @@ import { asArray } from '@proc7ts/primitives'; -import { UcFeatureConstraint, UcProcessorName } from '../../schema/uc-constraints.js'; +import { + UcConstraints, + UcFeatureConstraint, + UcProcessorName, +} from '../../schema/uc-constraints.js'; +import { UcPresentationName, UcPresentations } from '../../schema/uc-presentations.js'; import { UcDataType, UcSchema } from '../../schema/uc-schema.js'; import { ucSchemaVariant } from '../impl/uc-schema-variant.js'; export class UccSchemaIndex { readonly #processors: readonly UcProcessorName[]; + readonly #presentations: readonly UcPresentationName[]; readonly #types = new Map(); readonly #typesByPrefix = new Map(); #typeCounter = 0; readonly #schemaIds = new WeakMap(); - constructor(processors: readonly UcProcessorName[]) { + constructor( + processors: readonly UcProcessorName[], + presentations: readonly UcPresentationName[], + ) { this.#processors = processors; + this.#presentations = presentations; } get processors(): readonly UcProcessorName[] { return this.#processors; } + get presentations(): readonly UcPresentationName[] { + return this.#presentations; + } + + listPresentations(presentations: UcPresentations | undefined): readonly UcPresentationName[] { + if (!presentations) { + return []; + } + + const names = this.presentations; + + return names.length ? names : (Object.keys(presentations) as UcPresentationName[]); + } + schemaId(schema: UcSchema): string { let schemaId = this.#schemaIds.get(schema); @@ -38,24 +62,36 @@ export class UccSchemaIndex { fullId += `,${variant}`; } - const { where = {} } = schema; + const { where, within } = schema; + + return `${fullId}${this.#constraintsId(schema, where)}${this.#presentationsId(schema, within)}`; + } + + #presentationsId(schema: UcSchema, presentations?: UcPresentations): string { + return this.listPresentations(presentations) + .map(presentationName => { + const constraintsId = this.#constraintsId(schema, presentations![presentationName]); - return this.processors.reduce((fullId, processorName) => { - const constraints = where[processorName]; + return constraintsId ? `,~${presentationName}(${constraintsId.slice(1)})` : ''; + }) + .join(''); + } - if (!constraints) { - return fullId; - } + #constraintsId(schema: UcSchema, constraints: UcConstraints = {}): string { + return this.processors + .map(processorName => asArray(constraints[processorName]) + .map(feature => this.#featureConstraintId(schema, feature)) + .join('')) + .join(''); + } - return asArray(constraints).reduce((fullId, constraint): string => { - const { use, from } = constraint; - const id = constraint.id - ? constraint.id(schema, schema => this.schemaId(schema)) - : UcsSchemaIndex$defaultConstraintId(constraint); + #featureConstraintId(schema: UcSchema, feature: UcFeatureConstraint): string { + const { use, from } = feature; + const id = feature.id + ? feature.id(schema, schema => this.schemaId(schema)) + : UcsSchemaIndex$defaultConstraintId(feature); - return fullId + `,${use}@${from}` + (id ? `(${id})` : ''); - }, fullId); - }, fullId); + return `,${use}@${from}` + (id ? `(${id})` : ''); } #typeEntry({ type }: UcSchema): UccSchemaIndex$TypeEntry { diff --git a/src/compiler/rx/mod.ts b/src/compiler/rx/mod.ts index 07393d5a..ce99f109 100644 --- a/src/compiler/rx/mod.ts +++ b/src/compiler/rx/mod.ts @@ -2,6 +2,7 @@ export * from './ucrx-attr-setter.js'; export * from './ucrx-core.js'; export * from './ucrx-entity-setter.js'; export * from './ucrx-formatted-setter.js'; +export * from './ucrx-inset-method.js'; export * from './ucrx-lib.js'; export * from './ucrx-method.js'; export * from './ucrx-processor.js'; diff --git a/src/compiler/rx/ucrx-core.ts b/src/compiler/rx/ucrx-core.ts index 5a857203..c0648cd3 100644 --- a/src/compiler/rx/ucrx-core.ts +++ b/src/compiler/rx/ucrx-core.ts @@ -3,6 +3,7 @@ import { UcrxCore$stub, UcrxCore$stubBody } from '../impl/ucrx-core.stub.js'; import { UcrxAttrSetter, UcrxAttrSetterSignature } from './ucrx-attr-setter.js'; import { UcrxEntitySetter } from './ucrx-entity-setter.js'; import { UcrxFormattedSetter } from './ucrx-formatted-setter.js'; +import { UcrxInsetMethod } from './ucrx-inset-method.js'; import { UcrxMethod } from './ucrx-method.js'; import { UcrxProperty } from './ucrx-property.js'; import { UcrxSetter } from './ucrx-setter.js'; @@ -14,7 +15,7 @@ export type UcrxCore = { readonly big: UcrxSetter; readonly ent: UcrxEntitySetter; readonly fmt: UcrxFormattedSetter; - readonly ins: UcrxMethod<{ emit: EsArg; cx: EsArg }>; + readonly ins: UcrxInsetMethod; readonly nls: UcrxMethod<{ cx: EsArg }>; readonly nul: UcrxMethod<{ cx: EsArg }>; readonly num: UcrxSetter; @@ -38,10 +39,7 @@ export const UcrxCore: UcrxCore = { big: /*#__PURE__*/ new UcrxSetter('big', { typeName: 'bigint', stub: UcrxCore$stub }), ent: /*#__PURE__*/ new UcrxEntitySetter('ent'), fmt: /*#__PURE__*/ new UcrxFormattedSetter('fmt'), - ins: /*#__PURE__*/ new UcrxMethod('ins', { - args: { emit: {}, cx: {} }, - stub: UcrxCore$stub, - }), + ins: /*#__PURE__*/ new UcrxInsetMethod('ins'), nls: /*#__PURE__*/ new UcrxMethod<{ cx: EsArg }>('nls', { args: { cx: {} }, stub: UcrxCore$stub, diff --git a/src/compiler/rx/ucrx-inset-method.ts b/src/compiler/rx/ucrx-inset-method.ts new file mode 100644 index 00000000..b27fcdd0 --- /dev/null +++ b/src/compiler/rx/ucrx-inset-method.ts @@ -0,0 +1,108 @@ +import { isDefined } from '@proc7ts/primitives'; +import { + EsArg, + EsMemberRef, + EsMethodDeclaration, + EsMethodHandle, + EsSignature, + EsSnippet, + esStringLiteral, + esline, +} from 'esgen'; +import { UcrxCore$stub } from '../impl/ucrx-core.stub.js'; +import { UcrxBeforeMod, UcrxMethod, UcrxMethodInit } from './ucrx-method.js'; +import { UcrxClass } from './ucrx.class.js'; + +export class UcrxInsetMethod extends UcrxMethod { + + constructor(requestedName: string, init: UcrxInsetMethodInit = {}) { + const { stub = UcrxCore$stub } = init; + + super(requestedName, { ...init, args: UcrxInsetSignature, stub }); + } + + override overrideIn( + ucrxClass: UcrxClass.Any, + declaration: EsMethodDeclaration, + ): EsMethodHandle { + return super.overrideIn(ucrxClass, { + ...declaration, + body: (member, hostClass) => code => { + const mods = ucrxClass.methodModifiersOf(this); + const insets = mods + .map(({ inset }) => inset && ([inset.insetId, inset] as const)) + .filter(isDefined); + + if (insets.length) { + const { + member: { args }, + } = member; + const { id } = args; + + code + .write(esline`switch (${id}) {`) + .indent(code => { + let defaultInset: UcrxInsetMod['inset']; + + for (const [insetId, inset] of insets) { + if (insetId != null) { + code.line( + `case ${ + typeof insetId === 'string' ? esStringLiteral(insetId) : insetId + }: return `, + inset.createLexer(member as EsMemberRef, ucrxClass), + ';', + ); + } else { + defaultInset = inset; + } + } + + if (defaultInset) { + code.line( + 'default: return ', + defaultInset.createLexer(member as EsMemberRef, ucrxClass), + ';', + ); + } + }) + .write('}'); + } + + code.write(declaration.body(member, hostClass)); + }, + }); + } + +} + +export interface UcrxInsetMod extends UcrxBeforeMod { + readonly inset?: + | { + readonly insetId?: number | string | undefined; + createLexer( + member: EsMemberRef>, + ucrxClass: UcrxClass.Any, + ): EsSnippet; + } + | undefined; +} + +export interface UcrxInsetMethodInit + extends Omit, 'args' | 'stub' | 'typeName'> { + readonly stub?: EsMethodDeclaration | undefined; +} + +export const UcrxInsetSignature = /*#__PURE__*/ new EsSignature({ id: {}, emit: {}, cx: {} }); + +export type UcrxInsetSignature = EsSignature; + +export namespace UcrxInsetSignature { + export type Args = { + readonly id: EsArg; + readonly emit: EsArg; + readonly cx: EsArg; + }; + + export type Values = EsSignature.ValuesOf; +} diff --git a/src/compiler/rx/ucrx-method.ts b/src/compiler/rx/ucrx-method.ts index daa2b6ca..fba3434e 100644 --- a/src/compiler/rx/ucrx-method.ts +++ b/src/compiler/rx/ucrx-method.ts @@ -48,10 +48,15 @@ export class UcrxMethod< body: (member, hostClass) => code => { const mods = ucrxClass.methodModifiersOf(this); - for (const { before } of mods) { - code.write( - before(member as EsMemberRef, EsMethodHandle>, ucrxClass), + for (const mod of mods) { + const before = mod.before?.( + member as EsMemberRef, EsMethodHandle>, + ucrxClass, ); + + if (before) { + code.write(before); + } } code.write(declaration.body(member, hostClass)); @@ -76,7 +81,7 @@ export interface UcrxMethodInit { - before( + before?( member: EsMemberRef, EsMethodHandle>, ucrxClass: UcrxClass.Any, ): EsSnippet; diff --git a/src/compiler/rx/ucrx-processor.ts b/src/compiler/rx/ucrx-processor.ts index 946a90cc..37372b3f 100644 --- a/src/compiler/rx/ucrx-processor.ts +++ b/src/compiler/rx/ucrx-processor.ts @@ -4,7 +4,7 @@ import { UccProcessor } from '../processor/ucc-processor.js'; import { UccSchemaIndex } from '../processor/ucc-schema-index.js'; import { UcrxLib } from './ucrx-lib.js'; import { UcrxBeforeMod, UcrxMethod } from './ucrx-method.js'; -import { UcrxClass, UcrxProto } from './ucrx.class.js'; +import { UcrxClass, UcrxClassMod, UcrxProto, UcrxSignature } from './ucrx.class.js'; /** * Schema processor utilizing {@link churi!Ucrx charge receiver} code generation. @@ -43,6 +43,15 @@ export abstract class UcrxProcessor< return this; } + modifyUcrxClass = UcSchema>( + schema: TSchema, + mod: UcrxClassMod, + ): this { + this.#typeEntryFor(schema.type).modifyClass(schema, mod); + + return this; + } + /** * Declares `method` to present in all {@link churi!Ucrx charge receiver} implementations. * @@ -99,7 +108,7 @@ export abstract class UcrxProcessor< if (!typeEntry) { typeEntry = new UcrxTypeEntry(this.schemaIndex); - this.#perType.set(type, typeEntry); + this.#perType.set(type, typeEntry as UcrxTypeEntry); } return typeEntry; @@ -111,7 +120,7 @@ export namespace UcrxProcessor { export type Any = UcrxProcessor; } -class UcrxTypeEntry = UcSchema> { +class UcrxTypeEntry = UcSchema> { readonly #schemaIndex: UccSchemaIndex; #proto: UcrxProto | undefined; @@ -133,6 +142,10 @@ class UcrxTypeEntry = UcSchema< return this.#schemaEntryFor(schema).proto(this.#proto); } + modifyClass(schema: TSchema, mod: UcrxClassMod): void { + this.#schemaEntryFor(schema).modifyClass(mod); + } + modifyMethodOf>( schema: TSchema, method: UcrxMethod, @@ -155,9 +168,9 @@ class UcrxTypeEntry = UcSchema< } -class UcrxSchemaEntry> { +class UcrxSchemaEntry> { - readonly #mods: ((ucrxClass: UcrxClass.Any) => void)[] = []; + readonly #mods: ((ucrxClass: UcrxClass) => void)[] = []; #explicitProto: UcrxProto | undefined; #proto: UcrxProto | undefined; @@ -180,15 +193,19 @@ class UcrxSchemaEntry> { const ucrxClass = baseProto(lib, schema); if (ucrxClass) { - this.#modifyUcrxClass(ucrxClass); + this.#modifyUcrxClass(ucrxClass as UcrxClass); - ucrxClass.initUcrx(lib); + ucrxClass.initUcrx(); } return ucrxClass; }; } + modifyClass(mod: UcrxClassMod): void { + this.#mods.push(ucrxClass => mod.applyTo(ucrxClass)); + } + modifyMethod>( method: UcrxMethod, mod: TMod, @@ -196,7 +213,7 @@ class UcrxSchemaEntry> { this.#mods.push(ucrxClass => ucrxClass.modifyMethod(method, mod)); } - #modifyUcrxClass(ucrxClass: UcrxClass.Any): void { + #modifyUcrxClass(ucrxClass: UcrxClass): void { this.#mods.forEach(mod => mod(ucrxClass)); } diff --git a/src/compiler/rx/ucrx-setter.spec.ts b/src/compiler/rx/ucrx-setter.spec.ts index 184976b8..0c3b289a 100644 --- a/src/compiler/rx/ucrx-setter.spec.ts +++ b/src/compiler/rx/ucrx-setter.spec.ts @@ -9,7 +9,7 @@ describe('UcrxSetter', () => { beforeEach(() => { compiler = new UcdCompiler({ models: { - readValue: Number, + readValue: { model: Number }, }, features: [ ucdSupportDefaults, diff --git a/src/compiler/rx/ucrx.class.spec.ts b/src/compiler/rx/ucrx.class.spec.ts index 407f9bcf..d800db22 100644 --- a/src/compiler/rx/ucrx.class.spec.ts +++ b/src/compiler/rx/ucrx.class.spec.ts @@ -17,7 +17,7 @@ describe('UcrxClass', () => { }); }); - describe('isMEmberOverridden', () => { + describe('isMemberOverridden', () => { it('returns false for non-declared member', () => { const member = new EsField('test'); @@ -30,6 +30,7 @@ class TestClass extends UcrxClass { constructor() { super({ + lib: null!, typeName: 'Test', schema: ucMap({}), baseClass: VoidUcrxClass.instance, @@ -43,6 +44,7 @@ class TestClass2 extends UcrxClass { constructor() { super({ + lib: null!, typeName: 'Test2', schema: ucMap({}), baseClass: new TestClass(), diff --git a/src/compiler/rx/ucrx.class.ts b/src/compiler/rx/ucrx.class.ts index 4c856e23..8efc7ecd 100644 --- a/src/compiler/rx/ucrx.class.ts +++ b/src/compiler/rx/ucrx.class.ts @@ -16,12 +16,14 @@ export abstract class UcrxClass< readonly #methodMods = new Map, unknown[]>(); #supportedTypes?: ReadonlySet; #associations?: Map, unknown>; + #lib: UcrxLib; constructor(init: UcrxClass.Init) { - const { schema, typeName = ucSchemaTypeSymbol(schema), declare = { at: 'bundle' } } = init; + const { lib, schema, typeName = ucSchemaTypeSymbol(schema), declare = { at: 'bundle' } } = init; super(`${typeName}Ucrx`, { ...init, declare }); + this.#lib = lib; this.#schema = schema; this.#typeName = typeName; } @@ -32,6 +34,10 @@ export abstract class UcrxClass< return baseClass instanceof UcrxClass ? baseClass : undefined; } + get lib(): UcrxLib { + return this.#lib; + } + get schema(): TSchema { return this.#schema; } @@ -120,8 +126,7 @@ export abstract class UcrxClass< this.baseUcrx?.discoverTypes(types); } - initUcrx(lib: UcrxLib): void; - initUcrx(_lib: UcrxLib): void { + initUcrx(): void { this.#declareTypes(); } @@ -141,6 +146,7 @@ export namespace UcrxClass { T = unknown, TSchema extends UcSchema = UcSchema, > = EsClassInit & { + readonly lib: UcrxLib; readonly schema: TSchema; readonly typeName?: string | undefined; }; @@ -162,6 +168,10 @@ export const UcrxSignature: UcrxSignature = /*#__PURE__*/ new EsSignature({ set: {}, }); +export interface UcrxClassMod = UcSchema> { + applyTo(ucrxClass: UcrxClass): void; +} + export type UcrxSignature = EsSignature; export namespace UcrxSignature { diff --git a/src/compiler/serialization/ucs-compiler.ts b/src/compiler/serialization/ucs-compiler.ts index dcd91193..b819769e 100644 --- a/src/compiler/serialization/ucs-compiler.ts +++ b/src/compiler/serialization/ucs-compiler.ts @@ -39,7 +39,7 @@ export class UcsCompiler extends UccProce const { models, features } = options; super({ - names: 'serializer', + processors: 'serializer', models: Object.values(models), features, }); diff --git a/src/deserializer/impl/ucd-read-value.sync.ts b/src/deserializer/impl/ucd-read-value.sync.ts index 89b7d4ff..8ee0b478 100644 --- a/src/deserializer/impl/ucd-read-value.sync.ts +++ b/src/deserializer/impl/ucd-read-value.sync.ts @@ -23,8 +23,8 @@ import { UC_TOKEN_COMMA, UC_TOKEN_DOLLAR_SIGN, UC_TOKEN_EXCLAMATION_MARK, - UC_TOKEN_INSET, UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_INSET, UcToken, } from '../../syntax/uc-token.js'; import { SyncUcdReader } from '../sync-ucd-reader.js'; @@ -41,62 +41,75 @@ export function ucdReadValueSync( const firstToken = reader.current(); let hasValue = false; - if (!firstToken) { + if (firstToken === undefined) { // End of input. // Decode as empty string. rx.emptyStr(); return; } - if (firstToken === UC_TOKEN_EXCLAMATION_MARK) { - ucdReadMetaOrEntityOrTrueSync(reader, rx); + if (typeof firstToken === 'number') { + const prefix = firstToken & 0xff; - if (single) { - return; - } + switch (prefix) { + case UC_TOKEN_EXCLAMATION_MARK: + ucdReadMetaOrEntityOrTrueSync(reader, rx); - hasValue = true; - } else if (firstToken === UC_TOKEN_APOSTROPHE) { - reader.skip(); // Skip apostrophe. + if (single) { + return; + } - rx.str(printUcTokens(ucdReadTokensSync(reader, rx))); + hasValue = true; - if (single) { - return; - } + break; + case UC_TOKEN_APOSTROPHE: + reader.skip(); // Skip apostrophe. - hasValue = true; - } else if (firstToken === UC_TOKEN_DOLLAR_SIGN) { - reader.skip(); // Skip dollar prefix. + rx.str(printUcTokens(ucdReadTokensSync(reader, rx))); - const bound = ucdFindAnyBoundSync(reader, rx); - const key = printUcTokens(trimUcTokensTail(reader.consumePrev())); + if (single) { + return; + } - if (bound === UC_TOKEN_OPENING_PARENTHESIS) { - ucdReadMapSync(reader, rx, key); - } else if (!key) { - // End of input and no key. - // Empty map. - rx.emptyMap(); - } else { - // End of input. - // Map containing single key with empty value. - rx.onlySuffix(key); - } + hasValue = true; - if (single) { - return; - } + break; + case UC_TOKEN_DOLLAR_SIGN: + reader.skip(); // Skip dollar prefix. + + { + const bound = ucdFindAnyBoundSync(reader, rx); + const key = printUcTokens(trimUcTokensTail(reader.consumePrev())); + + if (bound === UC_TOKEN_OPENING_PARENTHESIS) { + ucdReadMapSync(reader, rx, key); + } else if (!key) { + // End of input and no key. + // Empty map. + rx.emptyMap(); + } else { + // End of input. + // Map containing single key with empty value. + rx.onlySuffix(key); + } + } + + if (single) { + return; + } + + hasValue = true; - hasValue = true; - } else if (firstToken === UC_TOKEN_INSET) { - reader.readInset(rx.rx, emit => rx.ins(emit), single); + break; + case UC_TOKEN_PREFIX_INSET: + reader.readInset(rx.rx, emit => rx.ins(firstToken, emit), single); - if (single) { - return; - } + if (single) { + return; + } - hasValue = true; + hasValue = true; + } } if (reader.current() === UC_TOKEN_OPENING_PARENTHESIS) { diff --git a/src/deserializer/impl/ucd-read-value.ts b/src/deserializer/impl/ucd-read-value.ts index cab8859a..06b8d44e 100644 --- a/src/deserializer/impl/ucd-read-value.ts +++ b/src/deserializer/impl/ucd-read-value.ts @@ -15,8 +15,8 @@ import { UC_TOKEN_COMMA, UC_TOKEN_DOLLAR_SIGN, UC_TOKEN_EXCLAMATION_MARK, - UC_TOKEN_INSET, UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_INSET, UcToken, } from '../../syntax/uc-token.js'; import { AsyncUcdReader } from '../async-ucd-reader.js'; @@ -33,62 +33,75 @@ export async function ucdReadValue( const firstToken = reader.current(); let hasValue = false; - if (!firstToken) { + if (firstToken === undefined) { // End of input. // Decode as empty string. rx.emptyStr(); return; } - if (firstToken === UC_TOKEN_EXCLAMATION_MARK) { - await ucdReadMetaOrEntityOrTrue(reader, rx); + if (typeof firstToken === 'number') { + const prefix = firstToken & 0xff; - if (single) { - return; - } + switch (prefix) { + case UC_TOKEN_EXCLAMATION_MARK: + await ucdReadMetaOrEntityOrTrue(reader, rx); - hasValue = true; - } else if (firstToken === UC_TOKEN_APOSTROPHE) { - reader.skip(); // Skip apostrophe. + if (single) { + return; + } - rx.str(printUcTokens(await ucdReadTokens(reader, rx))); + hasValue = true; - if (single) { - return; - } + break; + case UC_TOKEN_APOSTROPHE: + reader.skip(); // Skip apostrophe. - hasValue = true; - } else if (firstToken === UC_TOKEN_DOLLAR_SIGN) { - reader.skip(); // Skip dollar prefix. + rx.str(printUcTokens(await ucdReadTokens(reader, rx))); - const bound = await ucdFindAnyBound(reader, rx); - const key = printUcTokens(trimUcTokensTail(reader.consumePrev())); + if (single) { + return; + } - if (bound === UC_TOKEN_OPENING_PARENTHESIS) { - await ucdReadMap(reader, rx, key); - } else if (!key) { - // End of input and no key. - // Empty map. - rx.emptyMap(); - } else { - // End of input. - // Map containing single key with empty value. - rx.onlySuffix(key); - } + hasValue = true; - if (single) { - return; - } + break; + case UC_TOKEN_DOLLAR_SIGN: + reader.skip(); // Skip dollar prefix. + + { + const bound = await ucdFindAnyBound(reader, rx); + const key = printUcTokens(trimUcTokensTail(reader.consumePrev())); + + if (bound === UC_TOKEN_OPENING_PARENTHESIS) { + await ucdReadMap(reader, rx, key); + } else if (!key) { + // End of input and no key. + // Empty map. + rx.emptyMap(); + } else { + // End of input. + // Map containing single key with empty value. + rx.onlySuffix(key); + } + } + + if (single) { + return; + } + + hasValue = true; - hasValue = true; - } else if (firstToken === UC_TOKEN_INSET) { - await reader.readInset(rx.rx, emit => rx.ins(emit), single); + break; + case UC_TOKEN_PREFIX_INSET: + await reader.readInset(rx.rx, emit => rx.ins(firstToken, emit), single); - if (single) { - return; - } + if (single) { + return; + } - hasValue = true; + hasValue = true; + } } if (reader.current() === UC_TOKEN_OPENING_PARENTHESIS) { diff --git a/src/deserializer/impl/ucrx-handle.ts b/src/deserializer/impl/ucrx-handle.ts index a2ba46eb..6d2db76f 100644 --- a/src/deserializer/impl/ucrx-handle.ts +++ b/src/deserializer/impl/ucrx-handle.ts @@ -5,7 +5,7 @@ import { Ucrx } from '../../rx/ucrx.js'; import { UcMeta } from '../../schema/meta/uc-meta.js'; import { UcRejection } from '../../schema/uc-error.js'; import type { URIChargePath } from '../../schema/uri-charge/uri-charge-path.js'; -import { ucOpaqueLexer } from '../../syntax/uc-input-lexer.js'; +import { ucOpaqueLexer } from '../../syntax/lexers/uc-opaque.lexer.js'; import { UcToken } from '../../syntax/uc-token.js'; import { UcdReader } from '../ucd-reader.js'; @@ -102,8 +102,8 @@ export class UcrxHandle implements UcrxContext { } } - ins(emit: (token: UcToken) => void): UcrxInsetLexer { - const lexer = this.#rx.ins(emit, this) ?? this.#reader.inset(emit, this); + ins(id: number | string, emit: (token: UcToken) => void): UcrxInsetLexer { + const lexer = this.#rx.ins(id, emit, this) ?? this.#reader.inset(id, emit, this); if (lexer) { return lexer; @@ -111,6 +111,9 @@ export class UcrxHandle implements UcrxContext { this.#reject({ code: 'unexpectedInset', + details: { + insetId: id, + }, message: 'Unrecognized inset', }); diff --git a/src/deserializer/sync-ucd-reader.spec.ts b/src/deserializer/sync-ucd-reader.spec.ts index acf72cb8..2317f3b9 100644 --- a/src/deserializer/sync-ucd-reader.spec.ts +++ b/src/deserializer/sync-ucd-reader.spec.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; -import { parseTokens } from '../spec/read-chunks.js'; +import { UcChargeLexer } from '../syntax/lexers/uc-charge.lexer.js'; import { UC_TOKEN_CRLF, UC_TOKEN_LF } from '../syntax/uc-token.js'; import { SyncUcdReader } from './sync-ucd-reader.js'; @@ -172,6 +172,6 @@ describe('SyncUcdReader', () => { }); function readChunks(...chunks: string[]): SyncUcdReader { - return new SyncUcdReader(parseTokens(...chunks)); + return new SyncUcdReader(UcChargeLexer.scan(...chunks)); } }); diff --git a/src/deserializer/sync-ucd-reader.ts b/src/deserializer/sync-ucd-reader.ts index a837452b..4ae15098 100644 --- a/src/deserializer/sync-ucd-reader.ts +++ b/src/deserializer/sync-ucd-reader.ts @@ -1,5 +1,7 @@ import { UcrxInsetLexer } from '../rx/ucrx-inset-syntax.js'; import { Ucrx } from '../rx/ucrx.js'; +import { UcChargeLexer } from '../syntax/lexers/uc-charge.lexer.js'; +import { scanUcTokens } from '../syntax/scan-uc-tokens.js'; import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { ucdReadValueSync } from './impl/ucd-read-value.sync.js'; @@ -163,7 +165,34 @@ export function createSyncUcdReader( options?: UcdReader.Options, ): SyncUcdReader | undefined { if (typeof input === 'string') { - return new SyncUcdReader(UcLexer.scan(input), options); + return new SyncUcdReader(UcChargeLexer.scan(input), options); + } + if (Array.isArray(input)) { + return new SyncUcdReader(input, options); + } + + return; +} + +export function createSyncUcdLexer( + input: string | readonly UcToken[], + createLexer: (emit: (token: UcToken) => void) => UcLexer, + options?: UcdReader.Options, +): SyncUcdReader; + +export function createSyncUcdLexer( + input: string | readonly UcToken[] | unknown, + createLexer: (emit: (token: UcToken) => void) => UcLexer, + options?: UcdReader.Options, +): SyncUcdReader | undefined; + +export function createSyncUcdLexer( + input: string | readonly UcToken[] | unknown, + createLexer: (emit: (token: UcToken) => void) => UcLexer, + options?: UcdReader.Options, +): SyncUcdReader | undefined { + if (typeof input === 'string') { + return new SyncUcdReader(scanUcTokens(createLexer, input), options); } if (Array.isArray(input)) { return new SyncUcdReader(input, options); diff --git a/src/deserializer/ucd-reader.spec.ts b/src/deserializer/ucd-reader.spec.ts index e90a2297..089daeca 100644 --- a/src/deserializer/ucd-reader.spec.ts +++ b/src/deserializer/ucd-reader.spec.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, it } from '@jest/globals'; import { Readable } from 'node:stream'; import { UcError } from '../schema/uc-error.js'; -import { readTokens } from '../spec/read-chunks.js'; +import { parseTokens } from '../spec/read-chunks.js'; import { UC_TOKEN_CRLF, UC_TOKEN_LF } from '../syntax/uc-token.js'; import { AsyncUcdReader } from './async-ucd-reader.js'; @@ -168,6 +168,6 @@ describe('UcdReader', () => { }); function readChunks(...chunks: string[]): AsyncUcdReader { - return new AsyncUcdReader(readTokens(...chunks)); + return new AsyncUcdReader(parseTokens(...chunks)); } }); diff --git a/src/deserializer/ucd-reader.ts b/src/deserializer/ucd-reader.ts index 41f690e8..e972512e 100644 --- a/src/deserializer/ucd-reader.ts +++ b/src/deserializer/ucd-reader.ts @@ -45,8 +45,12 @@ export abstract class UcdReader { return this.#opaqueRx; } - inset(emit: (token: UcToken) => void, cx: UcrxContext): UcrxInsetLexer | undefined { - return this.#inset?.(emit, cx); + inset( + id: number | string, + emit: (token: UcToken) => void, + cx: UcrxContext, + ): UcrxInsetLexer | undefined { + return this.#inset?.(id, emit, cx); } abstract hasNext(): boolean; diff --git a/src/impl/module-names.ts b/src/impl/module-names.ts index 6d51ac96..34ffeda6 100644 --- a/src/impl/module-names.ts +++ b/src/impl/module-names.ts @@ -1,2 +1,3 @@ +export const CHURI_MODULE = 'churi'; export const COMPILER_MODULE = 'churi/compiler.js'; export const SPEC_MODULE = '#churi/spec.js'; diff --git a/src/rx/all.ucrx.ts b/src/rx/all.ucrx.ts index 52f85ffb..b4cb5a39 100644 --- a/src/rx/all.ucrx.ts +++ b/src/rx/all.ucrx.ts @@ -1,4 +1,4 @@ -import { UcInputLexer } from '../syntax/uc-input-lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { Ucrx } from './ucrx.js'; @@ -11,7 +11,7 @@ export interface AllUcrx extends Ucrx { big(value: bigint): 1; ent(name: string): 1; fmt(format: string, data: readonly UcToken[]): 1; - ins(emit: (token: UcToken) => void): UcInputLexer; + ins(id: number | string, emit: (token: UcToken) => void): UcLexer; nls(): AllUcrx; nul(): 1; num(value: number): 1; diff --git a/src/rx/opaque.ucrx.spec.ts b/src/rx/opaque.ucrx.spec.ts index 2f7c7a08..f2d615df 100644 --- a/src/rx/opaque.ucrx.spec.ts +++ b/src/rx/opaque.ucrx.spec.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from '@jest/globals'; import { noop } from '@proc7ts/primitives'; -import { ucOpaqueLexer } from '../syntax/uc-input-lexer.js'; +import { ucOpaqueLexer } from '../syntax/lexers/uc-opaque.lexer.js'; +import { UC_TOKEN_INSET_URI_PARAM } from '../syntax/uc-token.js'; import { OpaqueUcrx } from './opaque.ucrx.js'; describe('OpaqueUcrx', () => { @@ -28,7 +29,7 @@ describe('OpaqueUcrx', () => { describe('emb', () => { it('returns ucOpaqueLexer', () => { - expect(new OpaqueUcrx().ins(noop)).toBe(ucOpaqueLexer); + expect(new OpaqueUcrx().ins(UC_TOKEN_INSET_URI_PARAM, noop)).toBe(ucOpaqueLexer); }); }); diff --git a/src/rx/opaque.ucrx.ts b/src/rx/opaque.ucrx.ts index 2e025e27..d808f894 100644 --- a/src/rx/opaque.ucrx.ts +++ b/src/rx/opaque.ucrx.ts @@ -1,4 +1,5 @@ -import { UcInputLexer, ucOpaqueLexer } from '../syntax/uc-input-lexer.js'; +import { ucOpaqueLexer } from '../syntax/lexers/uc-opaque.lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { AllUcrx } from './all.ucrx.js'; import { VoidUcrx } from './void.ucrx.js'; @@ -18,8 +19,8 @@ export class OpaqueUcrx extends VoidUcrx implements AllUcrx { // Ignore metadata. } - override ins(emit: (token: UcToken) => void): UcInputLexer; - override ins(_emit: (token: UcToken) => void): UcInputLexer { + override ins(id: number | string, emit: (token: UcToken) => void): UcLexer; + override ins(_id: number | string, _emit: (token: UcToken) => void): UcLexer { return ucOpaqueLexer; } diff --git a/src/rx/token.ucrx.spec.ts b/src/rx/token.ucrx.spec.ts index 8f2640be..babbedd8 100644 --- a/src/rx/token.ucrx.spec.ts +++ b/src/rx/token.ucrx.spec.ts @@ -1,8 +1,9 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; import { noop } from '@proc7ts/primitives'; -import { ucOpaqueLexer } from '../syntax/uc-input-lexer.js'; +import { ucOpaqueLexer } from '../syntax/lexers/uc-opaque.lexer.js'; import { UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, UC_TOKEN_OPENING_PARENTHESIS, UcToken, } from '../syntax/uc-token.js'; @@ -24,7 +25,7 @@ describe('TokenUcrx', () => { describe('emb', () => { it('returns ucOpaqueLexer', () => { - expect(new TokenUcrx(noop).ins(noop)).toBe(ucOpaqueLexer); + expect(new TokenUcrx(noop).ins(UC_TOKEN_INSET_URI_PARAM, noop)).toBe(ucOpaqueLexer); }); }); diff --git a/src/rx/token.ucrx.ts b/src/rx/token.ucrx.ts index 17bda940..6a082d37 100644 --- a/src/rx/token.ucrx.ts +++ b/src/rx/token.ucrx.ts @@ -1,7 +1,8 @@ import { encodeURIPart } from 'httongue'; import { UC_KEY_ESCAPED, isEscapedUcString } from '../impl/uc-string-escapes.js'; +import { ucOpaqueLexer } from '../syntax/lexers/uc-opaque.lexer.js'; import { printUcToken } from '../syntax/print-uc-token.js'; -import { UcInputLexer, ucOpaqueLexer } from '../syntax/uc-input-lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UC_TOKEN_APOSTROPHE, UC_TOKEN_CLOSING_PARENTHESIS, @@ -82,8 +83,8 @@ export class TokenUcrx implements AllUcrx { return 1; } - ins(emit: (token: UcToken) => void): UcInputLexer; - ins(_emit: (token: UcToken) => void): UcInputLexer { + ins(id: number | string, emit: (token: UcToken) => void): UcLexer; + ins(_id: number | string, _emit: (token: UcToken) => void): UcLexer { return ucOpaqueLexer; } diff --git a/src/rx/ucrx-inset-syntax.ts b/src/rx/ucrx-inset-syntax.ts index c1ca7978..69e9044e 100644 --- a/src/rx/ucrx-inset-syntax.ts +++ b/src/rx/ucrx-inset-syntax.ts @@ -1,23 +1,26 @@ -import { UcInputLexer } from '../syntax/uc-input-lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { UcrxContext } from './ucrx-context.js'; -export type UcrxInsetLexer = UcInputLexer; +export type UcrxInsetLexer = UcLexer; /** - * Inset syntax is a function that creates a {@link UcrxInsetLexer lexer} for _inset_. I.e. the input chunks enclosed - * into {@link churi!UC_TOKEN_INSET inset bounds}. + * Inset syntax is a function that creates a {@link UcrxInsetLexer lexer} for _inset_. I.e. for the input chunks + * enclosed into {@link churi!UC_TOKEN_PREFIX_INSET inset bounds}. * * Once an inset is encountered, the deserializer would try to use the lexer defined by {@link Ucrx#ins * charge receiver}. If the latter is not defined, it will try to use the one created by this method. * If that fails, an error will be reported. * + * @param id - Inset format identifier. * @param emit - Emitter function called each time a token is found. * @param cx - Charge processing context. * * @returns Either input lexer factory, or `undefined` if an inset is not expected. */ export type UcrxInsetSyntax = ( + this: void, + id: number | string, emit: (token: UcToken) => void, cx: UcrxContext, ) => UcrxInsetLexer | undefined; diff --git a/src/rx/ucrx.ts b/src/rx/ucrx.ts index 49cbaeff..ce52568f 100644 --- a/src/rx/ucrx.ts +++ b/src/rx/ucrx.ts @@ -1,4 +1,4 @@ -import { UcInputLexer } from '../syntax/uc-input-lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { UcrxContext } from './ucrx-context.js'; @@ -80,12 +80,13 @@ export interface Ucrx { /** * Called to start inset tokenization. * + * @param id - Inset format identifier. * @param emit - Emitter function called each time a token is found. * @param cx - Charge processing context. * * @returns Either input lexer, or `undefined` if inset is not expected.. */ - ins(emit: (token: UcToken) => void, cx: UcrxContext): UcInputLexer | undefined; + ins(id: number | string, emit: (token: UcToken) => void, cx: UcrxContext): UcLexer | undefined; /** * Charges nested list. diff --git a/src/rx/void.ucrx.ts b/src/rx/void.ucrx.ts index 5eb52ce7..cb538bca 100644 --- a/src/rx/void.ucrx.ts +++ b/src/rx/void.ucrx.ts @@ -6,7 +6,7 @@ import { } from '../impl/ucrx-decode-raw.js'; import { UcEntity } from '../schema/entity/uc-entity.js'; import { UcFormatted } from '../schema/entity/uc-formatted.js'; -import { UcInputLexer } from '../syntax/uc-input-lexer.js'; +import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; import { UcrxContext } from './ucrx-context.js'; import { @@ -50,8 +50,8 @@ export class VoidUcrx implements Ucrx { return this.any(new UcFormatted(format, data)) || cx.reject(ucrxRejectFormat(format, data)); } - ins(emit: (token: UcToken) => void, cx: UcrxContext): UcInputLexer | undefined; - ins(_emit: (token: UcToken) => void, _cx: UcrxContext): undefined { + ins(id: number | string, emit: (token: UcToken) => void, cx: UcrxContext): UcLexer | undefined; + ins(_id: number | string, _emit: (token: UcToken) => void, _cx: UcrxContext): undefined { // Inset is not expected by default. } diff --git a/src/schema/boolean/uc-boolean.deserializer.spec.ts b/src/schema/boolean/uc-boolean.deserializer.spec.ts index 3a1ca0db..3002448b 100644 --- a/src/schema/boolean/uc-boolean.deserializer.spec.ts +++ b/src/schema/boolean/uc-boolean.deserializer.spec.ts @@ -1,6 +1,7 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { UcdModels } from '../../compiler/deserialization/ucd-models.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcErrorInfo } from '../uc-error.js'; import { ucNullable } from '../uc-nullable.js'; @@ -16,12 +17,12 @@ describe('UcBoolean deserializer', () => { errors = []; }); - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcModel }>({ + const compiler = new UcdCompiler<{ readValue: UcdModels.UniversalEntry> }>({ models: { - readValue: Boolean, + readValue: { model: Boolean }, }, }); @@ -29,13 +30,13 @@ describe('UcBoolean deserializer', () => { }); it('deserializes boolean', async () => { - await expect(readValue(readTokens('!'))).resolves.toBe(true); - await expect(readValue(readTokens(' ! '))).resolves.toBe(true); - await expect(readValue(readTokens('-'))).resolves.toBe(false); - await expect(readValue(readTokens(' - '))).resolves.toBe(false); + await expect(readValue(parseTokens('!'))).resolves.toBe(true); + await expect(readValue(parseTokens(' ! '))).resolves.toBe(true); + await expect(readValue(parseTokens('-'))).resolves.toBe(false); + await expect(readValue(parseTokens(' - '))).resolves.toBe(false); }); it('rejects null', async () => { - await expect(readValue(readTokens('--'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('--'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -52,7 +53,7 @@ describe('UcBoolean deserializer', () => { ]); }); it('rejects nested list', async () => { - await expect(readValue(readTokens('()'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('()'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -73,7 +74,7 @@ describe('UcBoolean deserializer', () => { ]); }); it('rejects second item', async () => { - await expect(readValue(readTokens('!,-'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('!,-'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -91,12 +92,14 @@ describe('UcBoolean deserializer', () => { }); describe('nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcModel }>({ + const compiler = new UcdCompiler<{ + readValue: UcdModels.UniversalEntry>; + }>({ models: { - readValue: ucNullable(Boolean), + readValue: { model: ucNullable(Boolean) }, }, }); @@ -104,18 +107,18 @@ describe('UcBoolean deserializer', () => { }); it('deserializes boolean', async () => { - await expect(readValue(readTokens('!'))).resolves.toBe(true); - await expect(readValue(readTokens(' ! '))).resolves.toBe(true); - await expect(readValue(readTokens('-'))).resolves.toBe(false); - await expect(readValue(readTokens(' - '))).resolves.toBe(false); + await expect(readValue(parseTokens('!'))).resolves.toBe(true); + await expect(readValue(parseTokens(' ! '))).resolves.toBe(true); + await expect(readValue(parseTokens('-'))).resolves.toBe(false); + await expect(readValue(parseTokens(' - '))).resolves.toBe(false); }); it('deserializes null', async () => { - await expect(readValue(readTokens('--'))).resolves.toBeNull(); - await expect(readValue(readTokens(' --'))).resolves.toBeNull(); - await expect(readValue(readTokens('-- \r\n'))).resolves.toBeNull(); + await expect(readValue(parseTokens('--'))).resolves.toBeNull(); + await expect(readValue(parseTokens(' --'))).resolves.toBeNull(); + await expect(readValue(parseTokens('-- \r\n'))).resolves.toBeNull(); }); it('rejects number', async () => { - await expect(readValue(readTokens('-1'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('-1'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { diff --git a/src/schema/entity/uc-entity.deserializer.spec.ts b/src/schema/entity/uc-entity.deserializer.spec.ts index 7adf9a26..5ba9aab5 100644 --- a/src/schema/entity/uc-entity.deserializer.spec.ts +++ b/src/schema/entity/uc-entity.deserializer.spec.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; import { ucdSupportPrimitives } from '../../compiler/deserialization/ucd-support-primitives.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { UcErrorInfo } from '../uc-error.js'; describe('UcEntity deserializer', () => { @@ -17,14 +17,14 @@ describe('UcEntity deserializer', () => { it('(async) does not recognize unknown entity', async () => { const compiler = new UcdCompiler({ models: { - readNumber: ['async', Number], + readNumber: { model: Number, mode: 'async' }, }, features: ucdSupportPrimitives, }); const { readNumber } = await compiler.evaluate(); - await expect(readNumber(readTokens('!Infinity'), { onError })).resolves.toBeUndefined(); + await expect(readNumber(parseTokens('!Infinity'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { code: 'unrecognizedEntity', @@ -39,7 +39,7 @@ describe('UcEntity deserializer', () => { it('(sync) does not recognize unknown entity', async () => { const compiler = new UcdCompiler({ models: { - readNumber: ['sync', Number], + readNumber: { model: Number, mode: 'sync' }, }, features: ucdSupportPrimitives, }); diff --git a/src/schema/entity/uc-formatted.deserializer.spec.ts b/src/schema/entity/uc-formatted.deserializer.spec.ts index 02f69ecd..d6792b9c 100644 --- a/src/schema/entity/uc-formatted.deserializer.spec.ts +++ b/src/schema/entity/uc-formatted.deserializer.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; import { ucdSupportPrimitives } from '../../compiler/deserialization/ucd-support-primitives.js'; import { ucdSupportPlainEntity } from '../../spec/plain.format.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { ucdSupportTimestampFormat, ucdSupportTimestampFormatOnly, @@ -23,7 +23,7 @@ describe('UcFormatted deserializer', () => { it('recognizes by custom prefix', async () => { const compiler = new UcdCompiler({ models: { - readString: ['sync', String], + readString: { model: String, mode: 'sync' }, }, features: [ucdSupportPrimitives, ucdSupportPlainEntity], }); @@ -34,20 +34,20 @@ describe('UcFormatted deserializer', () => { it('closes hanging parentheses', async () => { const compiler = new UcdCompiler({ models: { - readString: ['async', String], + readString: { model: String, mode: 'async' }, }, features: [ucdSupportPrimitives, ucdSupportPlainEntity], }); const { readString } = await compiler.evaluate(); - await expect(readString(readTokens("!plain'(bar(item1,item2)baz("))).resolves.toBe( + await expect(readString(parseTokens("!plain'(bar(item1,item2)baz("))).resolves.toBe( "!plain'(bar(item1,item2)baz())", ); }); it('extends base ucrx', async () => { const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', Number], + readTimestamp: { model: Number, mode: 'sync' }, }, features: [ucdSupportPrimitives, ucdSupportTimestampFormat], }); @@ -59,7 +59,7 @@ describe('UcFormatted deserializer', () => { it('fails without required ucrx method', async () => { const compiler = new UcdCompiler({ models: { - readTimestamp: ['sync', Number], + readTimestamp: { model: Number, mode: 'sync' }, }, features: [ucdSupportPrimitives, ucdSupportTimestampFormatOnly], }); @@ -71,7 +71,7 @@ describe('UcFormatted deserializer', () => { it('does not recognize unknown format', async () => { const compiler = new UcdCompiler({ models: { - readNumber: ['sync', Number], + readNumber: { model: Number, mode: 'sync' }, }, features: ucdSupportPrimitives, }); diff --git a/src/schema/entity/uc-formatted.ts b/src/schema/entity/uc-formatted.ts index c7ade514..4acc2d79 100644 --- a/src/schema/entity/uc-formatted.ts +++ b/src/schema/entity/uc-formatted.ts @@ -1,7 +1,7 @@ import { AllUcrx } from '../../rx/all.ucrx.js'; import { chargeURI } from '../../rx/charge-uri.js'; import { UctxMode } from '../../rx/uctx-mode.js'; -import { UcLexer } from '../../syntax/uc-lexer.js'; +import { UcChargeLexer } from '../../syntax/lexers/uc-charge.lexer.js'; import { UcToken } from '../../syntax/uc-token.js'; /** @@ -22,7 +22,7 @@ export class UcFormatted { */ constructor(format: string, data: string | readonly UcToken[]) { this.#format = format; - this.#data = typeof data === 'string' ? UcLexer.scan(data) : data; + this.#data = typeof data === 'string' ? UcChargeLexer.scan(data) : data; } /** diff --git a/src/schema/list/uc-list.deserializer.spec.ts b/src/schema/list/uc-list.deserializer.spec.ts index 579ebe20..308cd442 100644 --- a/src/schema/list/uc-list.deserializer.spec.ts +++ b/src/schema/list/uc-list.deserializer.spec.ts @@ -2,11 +2,12 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { asis } from '@proc7ts/primitives'; import { UnsupportedUcSchemaError } from '../../compiler/common/unsupported-uc-schema.error.js'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { parseTokens, readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; +import { UcChargeLexer } from '../../syntax/lexers/uc-charge.lexer.js'; import { ucMap } from '../map/uc-map.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcError, UcErrorInfo } from '../uc-error.js'; -import { UcNullable, ucNullable } from '../uc-nullable.js'; +import { ucNullable } from '../uc-nullable.js'; import { UcModel } from '../uc-schema.js'; import { ucList } from './uc-list.js'; @@ -21,12 +22,12 @@ describe('UcList deserializer', () => { }); describe('with single: reject', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucList(Number), + readList: { model: ucList(Number) }, }, }); @@ -34,28 +35,28 @@ describe('UcList deserializer', () => { }); it('deserializes list', async () => { - await expect(readList(readTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); + await expect(readList(parseTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); }); it('deserializes list synchronously', () => { - expect(readList(parseTokens('1 , 2, 3 '))).toEqual([1, 2, 3]); + expect(readList(UcChargeLexer.scan('1 , 2, 3 '))).toEqual([1, 2, 3]); }); it('deserializes empty list', async () => { - await expect(readList(readTokens(', '))).resolves.toEqual([]); + await expect(readList(parseTokens(', '))).resolves.toEqual([]); }); it('deserializes list with leading comma', async () => { - await expect(readList(readTokens(' , 1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); + await expect(readList(parseTokens(' , 1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); }); it('deserializes list with trailing comma', async () => { - await expect(readList(readTokens('1, 2, 3,'))).resolves.toEqual([1, 2, 3]); + await expect(readList(parseTokens('1, 2, 3,'))).resolves.toEqual([1, 2, 3]); }); it('deserializes single item with leading comma', async () => { - await expect(readList(readTokens(' ,13 '))).resolves.toEqual([13]); + await expect(readList(parseTokens(' ,13 '))).resolves.toEqual([13]); }); it('deserializes single item with trailing comma', async () => { - await expect(readList(readTokens('13 , '))).resolves.toEqual([13]); + await expect(readList(parseTokens('13 , '))).resolves.toEqual([13]); }); it('rejects item instead of list', async () => { - await expect(readList(readTokens('13'), { onError })).resolves.toBeUndefined(); + await expect(readList(parseTokens('13'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -74,7 +75,7 @@ describe('UcList deserializer', () => { it('does not deserialize unrecognized schema', async () => { const compiler = new UcdCompiler({ models: { - readList: ucList({ type: 'test-type' }), + readList: { model: ucList({ type: 'test-type' }) }, }, }); @@ -95,12 +96,12 @@ describe('UcList deserializer', () => { }); describe('with single: accept', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucList(Number, { single: 'accept' }), + readList: { model: ucList(Number, { single: 'accept' }) }, }, }); @@ -108,19 +109,19 @@ describe('UcList deserializer', () => { }); it('accepts item instead of list', async () => { - await expect(readList(readTokens('13'), { onError })).resolves.toEqual([13]); + await expect(readList(parseTokens('13'), { onError })).resolves.toEqual([13]); expect(errors).toEqual([]); }); }); describe('nullable with single: accept', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucNullable(ucList(Number, { single: 'accept' })), + readList: { model: ucNullable(ucList(Number, { single: 'accept' })) }, }, }); @@ -128,24 +129,24 @@ describe('UcList deserializer', () => { }); it('accepts item instead of list', async () => { - await expect(readList(readTokens('13'), { onError })).resolves.toEqual([13]); + await expect(readList(parseTokens('13'), { onError })).resolves.toEqual([13]); expect(errors).toEqual([]); }); it('accepts null', async () => { - await expect(readList(readTokens('--'), { onError })).resolves.toBeNull(); + await expect(readList(parseTokens('--'), { onError })).resolves.toBeNull(); expect(errors).toEqual([]); }); }); describe('of booleans', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucList(Boolean), + readList: { model: ucList(Boolean) }, }, }); @@ -153,17 +154,17 @@ describe('UcList deserializer', () => { }); it('deserializes items', async () => { - await expect(readList(readTokens('-, ! , - '))).resolves.toEqual([false, true, false]); + await expect(readList(parseTokens('-, ! , - '))).resolves.toEqual([false, true, false]); }); }); describe('of strings', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucList(String), + readList: { model: ucList(String) }, }, }); @@ -171,22 +172,24 @@ describe('UcList deserializer', () => { }); it('deserializes quoted strings', async () => { - await expect(readList(readTokens("'a, 'b , 'c"))).resolves.toEqual(['a', 'b ', 'c']); + await expect(readList(parseTokens("'a, 'b , 'c"))).resolves.toEqual(['a', 'b ', 'c']); }); it('deserializes empty list item', async () => { - await expect(readList(readTokens(',,'))).resolves.toEqual(['']); - await expect(readList(readTokens(', ,'))).resolves.toEqual(['']); - await expect(readList(readTokens(' , , '))).resolves.toEqual(['']); + await expect(readList(parseTokens(',,'))).resolves.toEqual(['']); + await expect(readList(parseTokens(', ,'))).resolves.toEqual(['']); + await expect(readList(parseTokens(' , , '))).resolves.toEqual(['']); }); }); describe('of maps', () => { - let readList: UcDeserializer<{ foo: string }[]>; + let readList: UcDeserializer.ByTokens<{ foo: string }[]>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucList<{ foo: string }>(ucMap<{ foo: UcModel }>({ foo: String })), + readList: { + model: ucList<{ foo: string }>(ucMap<{ foo: UcModel }>({ foo: String })), + }, }, }); @@ -194,27 +197,27 @@ describe('UcList deserializer', () => { }); it('deserializes items', async () => { - await expect(readList(readTokens('$foo, foo(bar) , $foo(baz)'))).resolves.toEqual([ + await expect(readList(parseTokens('$foo, foo(bar) , $foo(baz)'))).resolves.toEqual([ { foo: '' }, { foo: 'bar' }, { foo: 'baz' }, ]); - await expect(readList(readTokens('$foo(), foo(bar) , foo(baz),'))).resolves.toEqual([ + await expect(readList(parseTokens('$foo(), foo(bar) , foo(baz),'))).resolves.toEqual([ { foo: '' }, { foo: 'bar' }, { foo: 'baz' }, ]); - await expect(readList(readTokens('foo(), foo(bar) , foo(baz)'))).resolves.toEqual([ + await expect(readList(parseTokens('foo(), foo(bar) , foo(baz)'))).resolves.toEqual([ { foo: '' }, { foo: 'bar' }, { foo: 'baz' }, ]); - await expect(readList(readTokens(',foo(), foo(bar) , foo(baz))'))).resolves.toEqual([ + await expect(readList(parseTokens(',foo(), foo(bar) , foo(baz))'))).resolves.toEqual([ { foo: '' }, { foo: 'bar' }, { foo: 'baz' }, ]); - await expect(readList(readTokens(',$foo(), foo(bar) , foo(baz))'))).resolves.toEqual([ + await expect(readList(parseTokens(',$foo(), foo(bar) , foo(baz))'))).resolves.toEqual([ { foo: '' }, { foo: 'bar' }, { foo: 'baz' }, @@ -222,7 +225,7 @@ describe('UcList deserializer', () => { }); it('rejects nested list', async () => { await expect( - readList(readTokens('foo() () foo(bar) , $foo(baz)'), { onError }), + readList(parseTokens('foo() () foo(bar) , $foo(baz)'), { onError }), ).resolves.toEqual([{ foo: '' }, { foo: 'bar' }, { foo: 'baz' }]); expect(errors).toEqual([ @@ -241,7 +244,7 @@ describe('UcList deserializer', () => { }); it('rejects nested list after $-prefixed map', async () => { await expect( - readList(readTokens('$foo() () foo(bar) , $foo(baz)'), { onError }), + readList(parseTokens('$foo() () foo(bar) , $foo(baz)'), { onError }), ).resolves.toEqual([{ foo: '' }, { foo: 'bar' }, { foo: 'baz' }]); expect(errors).toEqual([ @@ -261,13 +264,13 @@ describe('UcList deserializer', () => { }); describe('with nullable items', () => { - let readList: UcDeserializer<(number | null)[]>; + let readList: UcDeserializer.ByTokens<(number | null)[]>; beforeAll(async () => { const nullableNumber = ucNullable(Number); const compiler = new UcdCompiler({ models: { - readList: ucList(nullableNumber), + readList: { model: ucList(nullableNumber) }, }, }); @@ -275,13 +278,13 @@ describe('UcList deserializer', () => { }); it('deserializes null item', async () => { - await expect(readList(readTokens('--,'))).resolves.toEqual([null]); - await expect(readList(readTokens(',--'))).resolves.toEqual([null]); - await expect(readList(readTokens('--,1'))).resolves.toEqual([null, 1]); - await expect(readList(readTokens('1, --'))).resolves.toEqual([1, null]); + await expect(readList(parseTokens('--,'))).resolves.toEqual([null]); + await expect(readList(parseTokens(',--'))).resolves.toEqual([null]); + await expect(readList(parseTokens('--,1'))).resolves.toEqual([null, 1]); + await expect(readList(parseTokens('1, --'))).resolves.toEqual([1, null]); }); it('rejects null', async () => { - const error = await readList(readTokens('--')).catch(asis); + const error = await readList(parseTokens('--')).catch(asis); expect((error as UcError).toJSON()).toEqual({ code: 'unexpectedType', @@ -298,12 +301,12 @@ describe('UcList deserializer', () => { }); describe('nullable', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readList: UcNullable }>({ + const compiler = new UcdCompiler({ models: { - readList: ucNullable(ucList(Number)), + readList: { model: ucNullable(ucList(Number)) }, }, }); @@ -311,11 +314,11 @@ describe('UcList deserializer', () => { }); it('deserializes null', async () => { - await expect(readList(readTokens('--'))).resolves.toBeNull(); + await expect(readList(parseTokens('--'))).resolves.toBeNull(); }); it('rejects null items', async () => { await expect( - readList(readTokens('--,')) + readList(parseTokens('--,')) .catch(asis) .then(error => (error as UcError).toJSON()), ).resolves.toEqual({ @@ -330,7 +333,7 @@ describe('UcList deserializer', () => { message: 'Unexpected null instead of number', }); await expect( - readList(readTokens(',--')) + readList(parseTokens(',--')) .catch(asis) .then(error => (error as UcError).toJSON()), ).resolves.toEqual({ @@ -346,18 +349,18 @@ describe('UcList deserializer', () => { }); }); it('deserializes list', async () => { - await expect(readList(readTokens('1, 2'))).resolves.toEqual([1, 2]); + await expect(readList(parseTokens('1, 2'))).resolves.toEqual([1, 2]); }); }); describe('nullable with nullable items', () => { - let readList: UcDeserializer<(number | null)[] | null>; + let readList: UcDeserializer.ByTokens<(number | null)[] | null>; beforeAll(async () => { const nullableNumber = ucNullable(Number); - const compiler = new UcdCompiler<{ readList: UcNullable<(number | null)[]> }>({ + const compiler = new UcdCompiler({ models: { - readList: ucNullable(ucList(nullableNumber)), + readList: { model: ucNullable(ucList(nullableNumber)) }, }, }); @@ -365,16 +368,16 @@ describe('UcList deserializer', () => { }); it('deserializes null', async () => { - await expect(readList(readTokens('--'))).resolves.toBeNull(); + await expect(readList(parseTokens('--'))).resolves.toBeNull(); }); it('deserializes list', async () => { - await expect(readList(readTokens('1, 2'))).resolves.toEqual([1, 2]); + await expect(readList(parseTokens('1, 2'))).resolves.toEqual([1, 2]); }); it('deserializes null item', async () => { - await expect(readList(readTokens('--,'))).resolves.toEqual([null]); - await expect(readList(readTokens(',--'))).resolves.toEqual([null]); - await expect(readList(readTokens('--,1'))).resolves.toEqual([null, 1]); - await expect(readList(readTokens('1, --'))).resolves.toEqual([1, null]); + await expect(readList(parseTokens('--,'))).resolves.toEqual([null]); + await expect(readList(parseTokens(',--'))).resolves.toEqual([null]); + await expect(readList(parseTokens('--,1'))).resolves.toEqual([null, 1]); + await expect(readList(parseTokens('1, --'))).resolves.toEqual([1, null]); }); }); @@ -385,12 +388,12 @@ describe('UcList deserializer', () => { errors = []; }); - let readMatrix: UcDeserializer; + let readMatrix: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMatrix: ucList(ucList(Number)), + readMatrix: { model: ucList(ucList(Number)) }, }, }); @@ -398,11 +401,11 @@ describe('UcList deserializer', () => { }); it('deserializes nested list', async () => { - await expect(readMatrix(readTokens(' ( 13 ) '))).resolves.toEqual([[13]]); + await expect(readMatrix(parseTokens(' ( 13 ) '))).resolves.toEqual([[13]]); }); it('rejects missing items', async () => { await expect( - readMatrix(readTokens(''), { onError: error => errors.push(error) }), + readMatrix(parseTokens(''), { onError: error => errors.push(error) }), ).resolves.toBeUndefined(); expect(errors).toEqual([ @@ -420,13 +423,13 @@ describe('UcList deserializer', () => { ]); }); it('deserializes comma-separated lists', async () => { - await expect(readMatrix(readTokens(' (13, 14), (15, 16) '))).resolves.toEqual([ + await expect(readMatrix(parseTokens(' (13, 14), (15, 16) '))).resolves.toEqual([ [13, 14], [15, 16], ]); }); it('deserializes lists', async () => { - await expect(readMatrix(readTokens(' (13, 14) (15, 16) '))).resolves.toEqual([ + await expect(readMatrix(parseTokens(' (13, 14) (15, 16) '))).resolves.toEqual([ [13, 14], [15, 16], ]); @@ -434,37 +437,37 @@ describe('UcList deserializer', () => { it('deserializes deeply nested lists', async () => { const compiler = new UcdCompiler({ models: { - readCube: ucList(ucList(ucList(Number))), + readCube: { model: ucList(ucList(ucList(Number))) }, }, }); const { readCube } = await compiler.evaluate(); - await expect(readCube(readTokens('((13, 14))'))).resolves.toEqual([[[13, 14]]]); + await expect(readCube(parseTokens('((13, 14))'))).resolves.toEqual([[[13, 14]]]); }); it('recognized empty item of nested list', async () => { const compiler = new UcdCompiler({ models: { - readMatrix: ucList(ucList(String)), + readMatrix: { model: ucList(ucList(String)) }, }, }); const { readMatrix } = await compiler.evaluate(); - await expect(readMatrix(readTokens('(,,)'))).resolves.toEqual([['']]); - await expect(readMatrix(readTokens('(, ,)'))).resolves.toEqual([['']]); - await expect(readMatrix(readTokens('( , , )'))).resolves.toEqual([['']]); + await expect(readMatrix(parseTokens('(,,)'))).resolves.toEqual([['']]); + await expect(readMatrix(parseTokens('(, ,)'))).resolves.toEqual([['']]); + await expect(readMatrix(parseTokens('( , , )'))).resolves.toEqual([['']]); }); }); describe('nested or null', () => { - let readMatrix: UcDeserializer<(number[] | null)[]>; + let readMatrix: UcDeserializer.ByTokens<(number[] | null)[]>; beforeAll(async () => { const list = ucList(Number); const compiler = new UcdCompiler({ models: { - readMatrix: ucList(ucNullable(list)), + readMatrix: { model: ucList(ucNullable(list)) }, }, }); @@ -472,15 +475,15 @@ describe('UcList deserializer', () => { }); it('deserializes nested list', async () => { - await expect(readMatrix(readTokens(' ( 13 ) '))).resolves.toEqual([[13]]); + await expect(readMatrix(parseTokens(' ( 13 ) '))).resolves.toEqual([[13]]); }); it('deserializes null items', async () => { - await expect(readMatrix(readTokens('--,'))).resolves.toEqual([null]); - await expect(readMatrix(readTokens(', --'))).resolves.toEqual([null]); - await expect(readMatrix(readTokens('(13)--'))).resolves.toEqual([[13], null]); + await expect(readMatrix(parseTokens('--,'))).resolves.toEqual([null]); + await expect(readMatrix(parseTokens(', --'))).resolves.toEqual([null]); + await expect(readMatrix(parseTokens('(13)--'))).resolves.toEqual([[13], null]); }); it('rejects null', async () => { - const error = await readMatrix(readTokens('--')).catch(error => (error as UcError)?.toJSON?.()); + const error = await readMatrix(parseTokens('--')).catch(error => (error as UcError)?.toJSON?.()); expect(error).toEqual({ code: 'unexpectedType', @@ -497,13 +500,13 @@ describe('UcList deserializer', () => { }); describe('nullable with nested', () => { - let readMatrix: UcDeserializer; + let readMatrix: UcDeserializer.ByTokens; beforeAll(async () => { const matrix = ucList(ucList(Number)); const compiler = new UcdCompiler({ models: { - readMatrix: ucNullable(matrix), + readMatrix: { model: ucNullable(matrix) }, }, }); @@ -511,11 +514,11 @@ describe('UcList deserializer', () => { }); it('deserializes null', async () => { - await expect(readMatrix(readTokens('--'))).resolves.toBeNull(); + await expect(readMatrix(parseTokens('--'))).resolves.toBeNull(); }); it('rejects null items', async () => { await expect( - readMatrix(readTokens('--,')).catch(error => (error as UcError)?.toJSON?.()), + readMatrix(parseTokens('--,')).catch(error => (error as UcError)?.toJSON?.()), ).resolves.toEqual({ code: 'unexpectedType', path: [{ index: 0 }], @@ -528,7 +531,7 @@ describe('UcList deserializer', () => { message: 'Unexpected null instead of nested list', }); await expect( - readMatrix(readTokens(',--')).catch(error => (error as UcError)?.toJSON?.()), + readMatrix(parseTokens(',--')).catch(error => (error as UcError)?.toJSON?.()), ).resolves.toEqual({ code: 'unexpectedType', path: [{ index: 1 }], @@ -544,14 +547,14 @@ describe('UcList deserializer', () => { }); describe('nullable with nested or null', () => { - let readMatrix: UcDeserializer<(number[] | null)[] | null>; + let readMatrix: UcDeserializer.ByTokens<(number[] | null)[] | null>; beforeAll(async () => { const list = ucList(Number); const matrix = ucList(ucNullable(list)); const compiler = new UcdCompiler({ models: { - readMatrix: ucNullable(matrix), + readMatrix: { model: ucNullable(matrix) }, }, }); @@ -559,12 +562,12 @@ describe('UcList deserializer', () => { }); it('deserializes null', async () => { - await expect(readMatrix(readTokens('--'))).resolves.toBeNull(); + await expect(readMatrix(parseTokens('--'))).resolves.toBeNull(); }); it('deserializes null items', async () => { - await expect(readMatrix(readTokens('--,'))).resolves.toEqual([null]); - await expect(readMatrix(readTokens(', --'))).resolves.toEqual([null]); - await expect(readMatrix(readTokens('(13)--'))).resolves.toEqual([[13], null]); + await expect(readMatrix(parseTokens('--,'))).resolves.toEqual([null]); + await expect(readMatrix(parseTokens(', --'))).resolves.toEqual([null]); + await expect(readMatrix(parseTokens('(13)--'))).resolves.toEqual([[13], null]); }); }); }); diff --git a/src/schema/list/uc-multi-value.deserializer.spec.ts b/src/schema/list/uc-multi-value.deserializer.spec.ts index 5caf1d70..c261e9c9 100644 --- a/src/schema/list/uc-multi-value.deserializer.spec.ts +++ b/src/schema/list/uc-multi-value.deserializer.spec.ts @@ -1,18 +1,18 @@ import { beforeAll, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { ucNullable } from '../uc-nullable.js'; import { ucMultiValue } from './uc-multi-value.js'; describe('UcMultiValue deserializer', () => { describe('with single: as-is', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucMultiValue(Number), + readList: { model: ucMultiValue(Number) }, }, }); @@ -20,27 +20,27 @@ describe('UcMultiValue deserializer', () => { }); it('deserializes list', async () => { - await expect(readList(readTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); + await expect(readList(parseTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); }); it('deserializes list with single item', async () => { - await expect(readList(readTokens('1,'))).resolves.toEqual([1]); - await expect(readList(readTokens(',1'))).resolves.toEqual([1]); + await expect(readList(parseTokens('1,'))).resolves.toEqual([1]); + await expect(readList(parseTokens(',1'))).resolves.toEqual([1]); }); it('deserializes empty list', async () => { - await expect(readList(readTokens(','))).resolves.toEqual([]); + await expect(readList(parseTokens(','))).resolves.toEqual([]); }); it('deserializes single item', async () => { - await expect(readList(readTokens('13'))).resolves.toBe(13); + await expect(readList(parseTokens('13'))).resolves.toBe(13); }); }); describe('nullable with single: as-is', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucNullable(ucMultiValue(Number)), + readList: { model: ucNullable(ucMultiValue(Number)) }, }, }); @@ -48,17 +48,17 @@ describe('UcMultiValue deserializer', () => { }); it('deserializes null', async () => { - await expect(readList(readTokens('--'))).resolves.toBeNull(); + await expect(readList(parseTokens('--'))).resolves.toBeNull(); }); }); describe('with single: prefer', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucMultiValue(Number, { single: 'prefer' }), + readList: { model: ucMultiValue(Number, { single: 'prefer' }) }, }, }); @@ -66,27 +66,27 @@ describe('UcMultiValue deserializer', () => { }); it('deserializes list', async () => { - await expect(readList(readTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); + await expect(readList(parseTokens('1 , 2, 3 '))).resolves.toEqual([1, 2, 3]); }); it('deserializes single list item', async () => { - await expect(readList(readTokens('1,'))).resolves.toBe(1); - await expect(readList(readTokens(',1'))).resolves.toBe(1); + await expect(readList(parseTokens('1,'))).resolves.toBe(1); + await expect(readList(parseTokens(',1'))).resolves.toBe(1); }); it('deserializes empty list', async () => { - await expect(readList(readTokens(','))).resolves.toEqual([]); + await expect(readList(parseTokens(','))).resolves.toEqual([]); }); it('deserializes single item', async () => { - await expect(readList(readTokens('13'))).resolves.toBe(13); + await expect(readList(parseTokens('13'))).resolves.toBe(13); }); }); describe('nullable with single: prefer', () => { - let readList: UcDeserializer; + let readList: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readList: ucNullable(ucMultiValue(Number, { single: 'prefer' })), + readList: { model: ucNullable(ucMultiValue(Number, { single: 'prefer' })) }, }, }); @@ -94,7 +94,7 @@ describe('UcMultiValue deserializer', () => { }); it('deserializes null', async () => { - await expect(readList(readTokens('--'))).resolves.toBeNull(); + await expect(readList(parseTokens('--'))).resolves.toBeNull(); }); }); }); diff --git a/src/schema/map/uc-map.deserializer.spec.ts b/src/schema/map/uc-map.deserializer.spec.ts index d49bcd4a..ea4ad63f 100644 --- a/src/schema/map/uc-map.deserializer.spec.ts +++ b/src/schema/map/uc-map.deserializer.spec.ts @@ -1,7 +1,8 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { UnsupportedUcSchemaError } from '../../compiler/common/unsupported-uc-schema.error.js'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { parseTokens, readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; +import { UcChargeLexer } from '../../syntax/lexers/uc-charge.lexer.js'; import { ucList } from '../list/uc-list.js'; import { ucMultiValue } from '../list/uc-multi-value.js'; import { ucNumber } from '../numeric/uc-number.js'; @@ -24,12 +25,12 @@ describe('UcMap deserializer', () => { }); describe('empty map', () => { - let readMap: UcDeserializer; + let readMap: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap({}), + readMap: { model: ucMap({}) }, }, }); @@ -71,14 +72,16 @@ describe('UcMap deserializer', () => { }); describe('single entry', () => { - let readMap: UcDeserializer<{ foo: string }>; + let readMap: UcDeserializer.ByTokens<{ foo: string }>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap<{ foo: UcModel }>({ - foo: String, - }), + readMap: { + model: ucMap<{ foo: UcModel }>({ + foo: String, + }), + }, }, }); @@ -86,23 +89,23 @@ describe('UcMap deserializer', () => { }); it('deserializes entry', async () => { - await expect(readMap(readTokens('foo(bar)'))).resolves.toEqual({ foo: 'bar' }); - await expect(readMap(readTokens('foo(bar'))).resolves.toEqual({ foo: 'bar' }); + await expect(readMap(parseTokens('foo(bar)'))).resolves.toEqual({ foo: 'bar' }); + await expect(readMap(parseTokens('foo(bar'))).resolves.toEqual({ foo: 'bar' }); }); it('deserializes entry synchronously', () => { - expect(readMap(parseTokens('foo(bar)'))).toEqual({ foo: 'bar' }); - expect(readMap(parseTokens('foo(bar'))).toEqual({ foo: 'bar' }); + expect(readMap(UcChargeLexer.scan('foo(bar)'))).toEqual({ foo: 'bar' }); + expect(readMap(UcChargeLexer.scan('foo(bar'))).toEqual({ foo: 'bar' }); }); it('deserializes $-escaped entry', async () => { - await expect(readMap(readTokens('$foo(bar)'))).resolves.toEqual({ foo: 'bar' }); + await expect(readMap(parseTokens('$foo(bar)'))).resolves.toEqual({ foo: 'bar' }); }); it('deserializes $-escaped suffix', async () => { - await expect(readMap(readTokens('$foo'))).resolves.toEqual({ foo: '' }); - await expect(readMap(readTokens('$foo \r\n '))).resolves.toEqual({ foo: '' }); - await expect(readMap(readTokens('\r\n $foo'))).resolves.toEqual({ foo: '' }); + await expect(readMap(parseTokens('$foo'))).resolves.toEqual({ foo: '' }); + await expect(readMap(parseTokens('$foo \r\n '))).resolves.toEqual({ foo: '' }); + await expect(readMap(parseTokens('\r\n $foo'))).resolves.toEqual({ foo: '' }); }); it('handles whitespace', async () => { - await expect(readMap(readTokens(' \n foo \r \n (\n bar \n)\n'))).resolves.toEqual({ + await expect(readMap(parseTokens(' \n foo \r \n (\n bar \n)\n'))).resolves.toEqual({ foo: 'bar', }); }); @@ -110,12 +113,12 @@ describe('UcMap deserializer', () => { const errors: unknown[] = []; await expect( - readMap(readTokens('foo(bar)foo'), { onError: error => errors.push(error) }), + readMap(parseTokens('foo(bar)foo'), { onError: error => errors.push(error) }), ).resolves.toEqual({ foo: '' }); }); it('rejects null', async () => { await expect( - readMap(readTokens('--')).catch(error => (error as UcError)?.toJSON?.()), + readMap(parseTokens('--')).catch(error => (error as UcError)?.toJSON?.()), ).resolves.toEqual({ code: 'unexpectedType', path: [{}], @@ -129,7 +132,7 @@ describe('UcMap deserializer', () => { }); }); it('rejects second item', async () => { - await expect(readMap(readTokens('foo(),'), { onError })).resolves.toBeUndefined(); + await expect(readMap(parseTokens('foo(),'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -146,7 +149,7 @@ describe('UcMap deserializer', () => { ]); }); it('rejects second item after $-prefixes map', async () => { - await expect(readMap(readTokens('$foo(),'), { onError })).resolves.toBeUndefined(); + await expect(readMap(parseTokens('$foo(),'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -165,9 +168,11 @@ describe('UcMap deserializer', () => { it('does not deserialize unrecognized entity schema', async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap({ - test: { type: 'test-type' }, - }), + readMap: { + model: ucMap({ + test: { type: 'test-type' }, + }), + }, }, }); @@ -188,15 +193,17 @@ describe('UcMap deserializer', () => { }); describe('multiple entries', () => { - let readMap: UcDeserializer<{ foo: string; bar: string }>; + let readMap: UcDeserializer.ByTokens<{ foo: string; bar: string }>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap<{ foo: UcModel; bar: UcModel }>({ - foo: String, - bar: String, - }), + readMap: { + model: ucMap<{ foo: UcModel; bar: UcModel }>({ + foo: String, + bar: String, + }), + }, }, }); @@ -204,36 +211,36 @@ describe('UcMap deserializer', () => { }); it('deserializes entries', async () => { - await expect(readMap(readTokens('foo(first)bar(second'))).resolves.toEqual({ + await expect(readMap(parseTokens('foo(first)bar(second'))).resolves.toEqual({ foo: 'first', bar: 'second', }); }); it('deserializes entries synchronously', () => { - expect(readMap(parseTokens('foo(first)bar(second'))).toEqual({ + expect(readMap(UcChargeLexer.scan('foo(first)bar(second'))).toEqual({ foo: 'first', bar: 'second', }); }); it('deserializes $-escaped entries', async () => { - await expect(readMap(readTokens('foo(first)$bar(second'))).resolves.toEqual({ + await expect(readMap(parseTokens('foo(first)$bar(second'))).resolves.toEqual({ foo: 'first', bar: 'second', }); }); it('deserializes suffix', async () => { - await expect(readMap(readTokens('foo(first) \n bar \r\n '))).resolves.toEqual({ + await expect(readMap(parseTokens('foo(first) \n bar \r\n '))).resolves.toEqual({ foo: 'first', bar: '', }); - await expect(readMap(readTokens('foo(first) \n bar )'))).resolves.toEqual({ + await expect(readMap(parseTokens('foo(first) \n bar )'))).resolves.toEqual({ foo: 'first', bar: '', }); }); it('handles whitespace', async () => { await expect( - readMap(readTokens('foo(first\r \n) \n $bar \r \n ( \r second \n )')), + readMap(parseTokens('foo(first\r \n) \n $bar \r \n ( \r second \n )')), ).resolves.toEqual({ foo: 'first', bar: 'second', @@ -243,7 +250,7 @@ describe('UcMap deserializer', () => { const errors: UcErrorInfo[] = []; await expect( - readMap(readTokens('foo(first)'), { onError: error => errors.push(error) }), + readMap(parseTokens('foo(first)'), { onError: error => errors.push(error) }), ).resolves.toBeUndefined(); expect(errors).toEqual([ @@ -259,7 +266,7 @@ describe('UcMap deserializer', () => { }); it('rejects unknown entry', async () => { await expect( - readMap(readTokens('foo(first)wrong(123)bar(second'), { onError }), + readMap(parseTokens('foo(first)wrong(123)bar(second'), { onError }), ).resolves.toEqual({ foo: 'first', bar: 'second', @@ -278,7 +285,7 @@ describe('UcMap deserializer', () => { }); it('rejects nested list', async () => { await expect( - readMap(readTokens('foo(first) bar(second) () '), { onError }), + readMap(parseTokens('foo(first) bar(second) () '), { onError }), ).resolves.toBeUndefined(); expect(errors).toEqual([ @@ -297,7 +304,7 @@ describe('UcMap deserializer', () => { }); it('rejects second item', async () => { await expect( - readMap(readTokens('foo(first) bar(second) , '), { onError }), + readMap(parseTokens('foo(first) bar(second) , '), { onError }), ).resolves.toBeUndefined(); expect(errors).toEqual([ @@ -316,7 +323,7 @@ describe('UcMap deserializer', () => { }); it('overwrites entry value', async () => { await expect( - readMap(readTokens('foo(first)bar(second)foo(third)'), { onError }), + readMap(parseTokens('foo(first)bar(second)foo(third)'), { onError }), ).resolves.toEqual({ foo: 'third', bar: 'second', @@ -327,20 +334,22 @@ describe('UcMap deserializer', () => { }); describe('with duplicates: reject', () => { - let readMap: UcDeserializer<{ foo: string; bar: string }>; + let readMap: UcDeserializer.ByTokens<{ foo: string; bar: string }>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap<{ foo: UcModel; bar: UcModel }>( - { - foo: String, - bar: String, - }, - { - duplicates: 'reject', - }, - ), + readMap: { + model: ucMap<{ foo: UcModel; bar: UcModel }>( + { + foo: String, + bar: String, + }, + { + duplicates: 'reject', + }, + ), + }, }, }); @@ -349,7 +358,7 @@ describe('UcMap deserializer', () => { it('rejects entry duplicate', async () => { await expect( - readMap(readTokens('foo(first)bar(second)foo(third)'), { onError }), + readMap(parseTokens('foo(first)bar(second)foo(third)'), { onError }), ).resolves.toEqual({ foo: 'first', bar: 'second', @@ -369,20 +378,22 @@ describe('UcMap deserializer', () => { }); describe('with duplicates: collect', () => { - let readMap: UcDeserializer<{ foo: string | string[]; bar: string | string[] }>; + let readMap: UcDeserializer.ByTokens<{ foo: string | string[]; bar: string | string[] }>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap( - { - foo: ucMultiValue(String), - bar: ucMultiValue(String), - }, - { - duplicates: 'collect', - }, - ), + readMap: { + model: ucMap( + { + foo: ucMultiValue(String), + bar: ucMultiValue(String), + }, + { + duplicates: 'collect', + }, + ), + }, }, }); @@ -391,7 +402,7 @@ describe('UcMap deserializer', () => { it('collects entry duplicates', async () => { await expect( - readMap(readTokens('foo(first)bar(second)foo(third)'), { onError }), + readMap(parseTokens('foo(first)bar(second)foo(third)'), { onError }), ).resolves.toEqual({ foo: ['first', 'third'], bar: 'second', @@ -402,7 +413,7 @@ describe('UcMap deserializer', () => { }); describe('with duplicates: collect and without required members', () => { - let readMap: UcDeserializer<{ + let readMap: UcDeserializer.ByTokens<{ foo?: string | string[] | undefined; bar?: string | string[] | undefined; }>; @@ -410,15 +421,17 @@ describe('UcMap deserializer', () => { beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap( - { - foo: ucOptional(ucMultiValue(String)), - bar: ucOptional(ucMultiValue(String)), - }, - { - duplicates: 'collect', - }, - ), + readMap: { + model: ucMap( + { + foo: ucOptional(ucMultiValue(String)), + bar: ucOptional(ucMultiValue(String)), + }, + { + duplicates: 'collect', + }, + ), + }, }, }); @@ -427,7 +440,7 @@ describe('UcMap deserializer', () => { it('collects entry duplicates', async () => { await expect( - readMap(readTokens('foo(first)foo(second)foo(third)'), { onError }), + readMap(parseTokens('foo(first)foo(second)foo(third)'), { onError }), ).resolves.toEqual({ foo: ['first', 'second', 'third'], }); @@ -437,19 +450,23 @@ describe('UcMap deserializer', () => { }); describe('extra entries', () => { - let readMap: UcDeserializer<{ length: number } & { [key in Exclude]: string }>; + let readMap: UcDeserializer.ByTokens< + { length: number } & { [key in Exclude]: string } + >; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap( - { - length: ucNumber(), - }, - { - extra: ucString(), - }, - ), + readMap: { + model: ucMap( + { + length: ucNumber(), + }, + { + extra: ucString(), + }, + ), + }, }, }); @@ -466,14 +483,16 @@ describe('UcMap deserializer', () => { it('does not deserialize unrecognized extra schema', async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap( - { - test: String, - }, - { - extra: { type: 'test-type' }, - }, - ), + readMap: { + model: ucMap( + { + test: String, + }, + { + extra: { type: 'test-type' }, + }, + ), + }, }, }); @@ -494,21 +513,23 @@ describe('UcMap deserializer', () => { }); describe('optional entries', () => { - let readMap: UcDeserializer< + let readMap: UcDeserializer.ByTokens< { length?: number | undefined } & { [key in Exclude]: string } >; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucMap( - { - length: ucOptional(Number), - }, - { - extra: String as UcDataType, - }, - ), + readMap: { + model: ucMap( + { + length: ucOptional(Number), + }, + { + extra: String as UcDataType, + }, + ), + }, }, }); @@ -538,13 +559,13 @@ describe('UcMap deserializer', () => { beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: [ - 'sync', - ucMap({ + readMap: { + model: ucMap({ foo: ucList(String), bar: ucList(Number), }), - ], + mode: 'sync', + }, }, }); @@ -610,16 +631,18 @@ describe('UcMap deserializer', () => { }); describe('nullable', () => { - let readMap: UcDeserializer<{ foo: string } | null>; + let readMap: UcDeserializer.ByTokens<{ foo: string } | null>; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readMap: ucNullable( - ucMap<{ foo: UcModel }>({ - foo: String, - }), - ), + readMap: { + model: ucNullable( + ucMap<{ foo: UcModel }>({ + foo: String, + }), + ), + }, }, }); @@ -627,10 +650,10 @@ describe('UcMap deserializer', () => { }); it('deserializes entry', async () => { - await expect(readMap(readTokens('foo(bar)'))).resolves.toEqual({ foo: 'bar' }); + await expect(readMap(parseTokens('foo(bar)'))).resolves.toEqual({ foo: 'bar' }); }); it('deserializes null', async () => { - await expect(readMap(readTokens('--'))).resolves.toBeNull(); + await expect(readMap(parseTokens('--'))).resolves.toBeNull(); }); }); }); diff --git a/src/schema/meta/uc-meta.deserializer.spec.ts b/src/schema/meta/uc-meta.deserializer.spec.ts index a45e03e5..35341043 100644 --- a/src/schema/meta/uc-meta.deserializer.spec.ts +++ b/src/schema/meta/uc-meta.deserializer.spec.ts @@ -7,7 +7,7 @@ import { URICharge } from '../uri-charge/uri-charge.js'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; import { ucdSupportDefaults } from '../../compiler/deserialization/ucd-support-defaults.js'; import { ucdSupportMetaMapEntity } from '../../spec/meta-map.entity.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import '../../spec/uri-charge-matchers.js'; import { ucString } from '../string/uc-string.js'; import { ucUnknown } from '../unknown/uc-unknown.js'; @@ -81,12 +81,12 @@ describe('UcMeta deserializer', () => { }); describe('within unknown input', () => { - let parse: UcDeserializer.Async; + let parse: UcDeserializer.AsyncByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - parse: ucUnknown(), + parse: { model: ucUnknown() }, }, features: [ucdSupportDefaults, ucdSupportMetaMapEntity], }); @@ -102,13 +102,13 @@ describe('UcMeta deserializer', () => { }); it('attached to single value', async () => { - await expect(parse(readTokens('!test(value)!meta-map'))).resolves.toEqual({ + await expect(parse(parseTokens('!test(value)!meta-map'))).resolves.toEqual({ test: ['value'], }); }); it('attached to list items', async () => { await expect( - parse(readTokens('!test(value1)!meta-map,!test(value2)!meta-map')), + parse(parseTokens('!test(value1)!meta-map,!test(value2)!meta-map')), ).resolves.toEqual([ { test: ['value1'], @@ -120,7 +120,7 @@ describe('UcMeta deserializer', () => { }); it('attached to map entity', async () => { await expect( - parse(readTokens('first(!test(value1)!meta-map)second(!test(value2)!meta-map)')), + parse(parseTokens('first(!test(value1)!meta-map)second(!test(value2)!meta-map)')), ).resolves.toEqual({ first: { test: ['value1'], @@ -137,7 +137,7 @@ describe('UcMeta deserializer', () => { beforeAll(async () => { const compiler = new UcdCompiler({ - models: { parse: ucUnknown() }, + models: { parse: { model: ucUnknown() } }, features: [ ucdSupportDefaults, compiler => ({ diff --git a/src/schema/mod.ts b/src/schema/mod.ts index 7bb9f297..8ba1b5f2 100644 --- a/src/schema/mod.ts +++ b/src/schema/mod.ts @@ -12,6 +12,7 @@ export * from './uc-error.js'; export * from './uc-model-name.js'; export * from './uc-nullable.js'; export * from './uc-optional.js'; +export * from './uc-presentations.js'; export * from './uc-schema.js'; export * from './uc-serializer.js'; export * from './unknown/mod.js'; diff --git a/src/schema/numeric/uc-bigint.deserializer.spec.ts b/src/schema/numeric/uc-bigint.deserializer.spec.ts index cd32e00e..1f3de207 100644 --- a/src/schema/numeric/uc-bigint.deserializer.spec.ts +++ b/src/schema/numeric/uc-bigint.deserializer.spec.ts @@ -1,6 +1,6 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcErrorInfo } from '../uc-error.js'; import { ucNullable } from '../uc-nullable.js'; @@ -17,12 +17,12 @@ describe('UcBigInt deserializer', () => { }); describe('by default', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: BigInt, + readValue: { model: BigInt }, }, }); @@ -30,21 +30,21 @@ describe('UcBigInt deserializer', () => { }); it('deserializes bigint', async () => { - await expect(readValue(readTokens(' 0n123 '))).resolves.toBe(123n); - await expect(readValue(readTokens('-0n123'))).resolves.toBe(-123n); + await expect(readValue(parseTokens(' 0n123 '))).resolves.toBe(123n); + await expect(readValue(parseTokens('-0n123'))).resolves.toBe(-123n); }); it('deserializes hexadecimal bigint', async () => { - await expect(readValue(readTokens('0n0x123'))).resolves.toBe(0x123n); - await expect(readValue(readTokens('-0n0x123'))).resolves.toBe(-0x123n); + await expect(readValue(parseTokens('0n0x123'))).resolves.toBe(0x123n); + await expect(readValue(parseTokens('-0n0x123'))).resolves.toBe(-0x123n); }); it('deserializes bigint zero', async () => { - await expect(readValue(readTokens('0n0'))).resolves.toBe(0n); - await expect(readValue(readTokens('-0n0'))).resolves.toBe(-0n); - await expect(readValue(readTokens('0n'))).resolves.toBe(0n); - await expect(readValue(readTokens('-0n'))).resolves.toBe(-0n); + await expect(readValue(parseTokens('0n0'))).resolves.toBe(0n); + await expect(readValue(parseTokens('-0n0'))).resolves.toBe(-0n); + await expect(readValue(parseTokens('0n'))).resolves.toBe(0n); + await expect(readValue(parseTokens('-0n'))).resolves.toBe(-0n); }); it('rejects null', async () => { - await expect(readValue(readTokens('--'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('--'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -56,7 +56,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects malformed bigint', async () => { - await expect(readValue(readTokens('0nz'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('0nz'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -69,7 +69,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects malformed number', async () => { - await expect(readValue(readTokens('1z'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('1z'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -82,25 +82,25 @@ describe('UcBigInt deserializer', () => { ]); }); it('parses number', async () => { - await expect(readValue(readTokens('123'), { onError })).resolves.toBe(123n); - await expect(readValue(readTokens('-123'), { onError })).resolves.toBe(-123n); - await expect(readValue(readTokens('0x123'), { onError })).resolves.toBe(0x123n); - await expect(readValue(readTokens('-0x123'), { onError })).resolves.toBe(-0x123n); + await expect(readValue(parseTokens('123'), { onError })).resolves.toBe(123n); + await expect(readValue(parseTokens('-123'), { onError })).resolves.toBe(-123n); + await expect(readValue(parseTokens('0x123'), { onError })).resolves.toBe(0x123n); + await expect(readValue(parseTokens('-0x123'), { onError })).resolves.toBe(-0x123n); }); it('parses numeric string without 0n prefix', async () => { - await expect(readValue(readTokens("'123"), { onError })).resolves.toBe(123n); - await expect(readValue(readTokens("'-123"), { onError })).resolves.toBe(-123n); - await expect(readValue(readTokens("'0x123"), { onError })).resolves.toBe(0x123n); - await expect(readValue(readTokens("'-0x123"), { onError })).resolves.toBe(-0x123n); + await expect(readValue(parseTokens("'123"), { onError })).resolves.toBe(123n); + await expect(readValue(parseTokens("'-123"), { onError })).resolves.toBe(-123n); + await expect(readValue(parseTokens("'0x123"), { onError })).resolves.toBe(0x123n); + await expect(readValue(parseTokens("'-0x123"), { onError })).resolves.toBe(-0x123n); }); it('parses numeric string with 0n prefix', async () => { - await expect(readValue(readTokens("'0n123"), { onError })).resolves.toBe(123n); - await expect(readValue(readTokens("'-0n123"), { onError })).resolves.toBe(-123n); - await expect(readValue(readTokens("'0n0x123"), { onError })).resolves.toBe(0x123n); - await expect(readValue(readTokens("'-0n0x123"), { onError })).resolves.toBe(-0x123n); + await expect(readValue(parseTokens("'0n123"), { onError })).resolves.toBe(123n); + await expect(readValue(parseTokens("'-0n123"), { onError })).resolves.toBe(-123n); + await expect(readValue(parseTokens("'0n0x123"), { onError })).resolves.toBe(0x123n); + await expect(readValue(parseTokens("'-0n0x123"), { onError })).resolves.toBe(-0x123n); }); it('rejects malformed bigint string', async () => { - await expect(readValue(readTokens("'0nz"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'0nz"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -113,7 +113,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects malformed number string', async () => { - await expect(readValue(readTokens("'1z"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'1z"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -127,12 +127,12 @@ describe('UcBigInt deserializer', () => { }); describe('when strings rejected', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucBigInt({ string: 'reject' }), + readValue: { model: ucBigInt({ string: 'reject' }) }, }, }); @@ -140,7 +140,7 @@ describe('UcBigInt deserializer', () => { }); it('rejects numeric string without 0n prefix', async () => { - await expect(readValue(readTokens("'123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -152,7 +152,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects numeric string with 0n prefix', async () => { - await expect(readValue(readTokens("'0n123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'0n123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -167,12 +167,12 @@ describe('UcBigInt deserializer', () => { }); describe('when nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucNullable(BigInt), + readValue: { model: ucNullable(BigInt) }, }, }); @@ -180,21 +180,21 @@ describe('UcBigInt deserializer', () => { }); it('deserializes bigint', async () => { - await expect(readValue(readTokens(' 0n123 '))).resolves.toBe(123n); - await expect(readValue(readTokens('-0n123'))).resolves.toBe(-123n); + await expect(readValue(parseTokens(' 0n123 '))).resolves.toBe(123n); + await expect(readValue(parseTokens('-0n123'))).resolves.toBe(-123n); }); it('deserializes null', async () => { - await expect(readValue(readTokens(' -- '))).resolves.toBeNull(); + await expect(readValue(parseTokens(' -- '))).resolves.toBeNull(); }); }); describe('when numbers rejected', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucBigInt({ number: 'reject' }), + readValue: { model: ucBigInt({ number: 'reject' }) }, }, }); @@ -202,11 +202,11 @@ describe('UcBigInt deserializer', () => { }); it('deserializes bigint', async () => { - await expect(readValue(readTokens(' 0n123 '))).resolves.toBe(123n); - await expect(readValue(readTokens('-0n123'))).resolves.toBe(-123n); + await expect(readValue(parseTokens(' 0n123 '))).resolves.toBe(123n); + await expect(readValue(parseTokens('-0n123'))).resolves.toBe(-123n); }); it('rejects number', async () => { - await expect(readValue(readTokens('123'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('123'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -218,7 +218,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects negative number', async () => { - await expect(readValue(readTokens('-123'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('-123'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -230,7 +230,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects NaN', async () => { - await expect(readValue(readTokens('0nz'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('0nz'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -243,7 +243,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects numeric string without 0n prefix', async () => { - await expect(readValue(readTokens("'123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -255,7 +255,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects negative numeric string without 0n prefix', async () => { - await expect(readValue(readTokens("'-123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'-123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -267,13 +267,13 @@ describe('UcBigInt deserializer', () => { ]); }); it('parses numeric string with 0n prefix', async () => { - await expect(readValue(readTokens("'0n123"), { onError })).resolves.toBe(123n); - await expect(readValue(readTokens("'-0n123"), { onError })).resolves.toBe(-123n); - await expect(readValue(readTokens("'0n0x123"), { onError })).resolves.toBe(0x123n); - await expect(readValue(readTokens("'-0n0x123"), { onError })).resolves.toBe(-0x123n); + await expect(readValue(parseTokens("'0n123"), { onError })).resolves.toBe(123n); + await expect(readValue(parseTokens("'-0n123"), { onError })).resolves.toBe(-123n); + await expect(readValue(parseTokens("'0n0x123"), { onError })).resolves.toBe(0x123n); + await expect(readValue(parseTokens("'-0n0x123"), { onError })).resolves.toBe(-0x123n); }); it('rejects malformed bigint string', async () => { - await expect(readValue(readTokens("'0nz"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'0nz"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -287,12 +287,12 @@ describe('UcBigInt deserializer', () => { }); describe('when strings rejected', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucBigInt({ string: 'reject', number: 'reject' }), + readValue: { model: ucBigInt({ string: 'reject', number: 'reject' }) }, }, }); @@ -300,7 +300,7 @@ describe('UcBigInt deserializer', () => { }); it('rejects numeric string without 0n prefix', async () => { - await expect(readValue(readTokens("'123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -312,7 +312,7 @@ describe('UcBigInt deserializer', () => { ]); }); it('rejects numeric string with 0n prefix', async () => { - await expect(readValue(readTokens("'0n123"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'0n123"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { diff --git a/src/schema/numeric/uc-bigint.ts b/src/schema/numeric/uc-bigint.ts index a1e81054..82e77791 100644 --- a/src/schema/numeric/uc-bigint.ts +++ b/src/schema/numeric/uc-bigint.ts @@ -102,7 +102,7 @@ export namespace UcBigInt { /*#__NO_SIDE_EFFECTS__*/ export function ucBigInt(options?: UcBigInt.Options): UcBigInt.Schema { if (options) { - const { where, string, number } = options; + const { where, within, string, number } = options; const variant: UcBigInt.Variant | undefined = string || number ? { string, number } : undefined; return ucSchema(BigInt, { @@ -123,6 +123,7 @@ export function ucBigInt(options?: UcBigInt.Options): UcBigInt.Schema { ...asArray(where), ] : where, + within, }); } diff --git a/src/schema/numeric/uc-integer.deserializer.spec.ts b/src/schema/numeric/uc-integer.deserializer.spec.ts index 66dbfc46..31bfdbf9 100644 --- a/src/schema/numeric/uc-integer.deserializer.spec.ts +++ b/src/schema/numeric/uc-integer.deserializer.spec.ts @@ -16,12 +16,12 @@ describe('UcInteger deserializer', () => { }); describe('by default', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucInteger(), + readValue: { model: ucInteger() }, }, }); @@ -122,12 +122,12 @@ describe('UcInteger deserializer', () => { }); describe('when string parsed', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucInteger({}), + readValue: { model: ucInteger({}) }, }, }); @@ -142,12 +142,12 @@ describe('UcInteger deserializer', () => { }); describe('when nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucNullable(ucInteger()), + readValue: { model: ucNullable(ucInteger()) }, }, }); @@ -235,12 +235,12 @@ describe('UcInteger deserializer', () => { }); describe('when strings rejected', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucInteger({ string: 'reject' }), + readValue: { model: ucInteger({ string: 'reject' }) }, }, }); diff --git a/src/schema/numeric/uc-integer.ts b/src/schema/numeric/uc-integer.ts index d9d09f50..70b82cbe 100644 --- a/src/schema/numeric/uc-integer.ts +++ b/src/schema/numeric/uc-integer.ts @@ -55,16 +55,14 @@ export namespace UcInteger { /*#__NO_SIDE_EFFECTS__*/ export function ucInteger(options?: UcInteger.Options): UcInteger.Schema { if (options) { - const { where, string } = options; + const { string } = options; const variant: UcNumber.Variant | undefined = string ? { string, } : undefined; - return ucSchema(UcInteger$createSchema(variant), { - where, - }); + return ucSchema(UcInteger$createSchema(variant), options); } return UcInteger$schema; diff --git a/src/schema/numeric/uc-number.deserializer.spec.ts b/src/schema/numeric/uc-number.deserializer.spec.ts index 8374ed0d..8c73092b 100644 --- a/src/schema/numeric/uc-number.deserializer.spec.ts +++ b/src/schema/numeric/uc-number.deserializer.spec.ts @@ -2,7 +2,8 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; import { ucdSupportNonFinite } from '../../compiler/deserialization/ucd-support-non-finite.js'; import { ucdSupportPrimitives } from '../../compiler/deserialization/ucd-support-primitives.js'; -import { parseTokens, readTokens } from '../../spec/read-chunks.js'; +import { parseTokens } from '../../spec/read-chunks.js'; +import { UcChargeLexer } from '../../syntax/lexers/uc-charge.lexer.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcErrorInfo } from '../uc-error.js'; import { UcDataType } from '../uc-schema.js'; @@ -19,12 +20,12 @@ describe('UcNumber deserializer', () => { }); describe('by default', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: Number as UcDataType, + readValue: { model: Number as UcDataType }, }, features: [ucdSupportPrimitives, ucdSupportNonFinite], }); @@ -33,25 +34,25 @@ describe('UcNumber deserializer', () => { }); it('deserializes number', async () => { - await expect(readValue(readTokens('123'))).resolves.toBe(123); - await expect(readValue(readTokens('-123'))).resolves.toBe(-123); + await expect(readValue(parseTokens('123'))).resolves.toBe(123); + await expect(readValue(parseTokens('-123'))).resolves.toBe(-123); }); it('deserializes number synchronously', async () => { const compiler = new UcdCompiler({ models: { - parseValue: ['sync', Number], + parseValue: { model: Number, mode: 'sync' }, }, }); const { parseValue } = await compiler.evaluate(); - expect(parseValue(parseTokens('123'))).toBe(123); - expect(parseValue(parseTokens('-123'))).toBe(-123); + expect(parseValue(UcChargeLexer.scan('123'))).toBe(123); + expect(parseValue(UcChargeLexer.scan('-123'))).toBe(-123); }); it('deserializes number from string', async () => { const compiler = new UcdCompiler({ models: { - parseValue: ['sync', Number], + parseValue: { model: Number, mode: 'sync' }, }, }); const { parseValue } = await compiler.evaluate(); @@ -60,19 +61,19 @@ describe('UcNumber deserializer', () => { expect(parseValue('-123')).toBe(-123); }); it('deserializes percent-encoded number', async () => { - await expect(readValue(readTokens('%3123'))).resolves.toBe(123); - await expect(readValue(readTokens('%2D%3123'))).resolves.toBe(-123); + await expect(readValue(parseTokens('%3123'))).resolves.toBe(123); + await expect(readValue(parseTokens('%2D%3123'))).resolves.toBe(-123); }); it('deserializes hexadecimal number', async () => { - await expect(readValue(readTokens('0x123'))).resolves.toBe(0x123); - await expect(readValue(readTokens('-0x123'))).resolves.toBe(-0x123); + await expect(readValue(parseTokens('0x123'))).resolves.toBe(0x123); + await expect(readValue(parseTokens('-0x123'))).resolves.toBe(-0x123); }); it('deserializes zero', async () => { - await expect(readValue(readTokens('0'))).resolves.toBe(0); - await expect(readValue(readTokens('-0'))).resolves.toBe(-0); + await expect(readValue(parseTokens('0'))).resolves.toBe(0); + await expect(readValue(parseTokens('-0'))).resolves.toBe(-0); }); it('rejects NaN', async () => { - await expect(readValue(readTokens('0xz'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('0xz'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -84,7 +85,7 @@ describe('UcNumber deserializer', () => { ]); }); it('rejects bigint', async () => { - await expect(readValue(readTokens('0n1'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('0n1'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -96,7 +97,7 @@ describe('UcNumber deserializer', () => { ]); }); it('rejects boolean', async () => { - await expect(readValue(readTokens('-'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('-'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -108,21 +109,21 @@ describe('UcNumber deserializer', () => { ]); }); it('parses numeric string', async () => { - await expect(readValue(readTokens("'1"), { onError })).resolves.toBe(1); - await expect(readValue(readTokens("'-1"), { onError })).resolves.toBe(-1); + await expect(readValue(parseTokens("'1"), { onError })).resolves.toBe(1); + await expect(readValue(parseTokens("'-1"), { onError })).resolves.toBe(-1); expect(errors).toEqual([]); }); it('parses Infinity string', async () => { - await expect(readValue(readTokens("'Infinity"), { onError })).resolves.toBe(Infinity); - await expect(readValue(readTokens("'-Infinity"), { onError })).resolves.toBe(-Infinity); + await expect(readValue(parseTokens("'Infinity"), { onError })).resolves.toBe(Infinity); + await expect(readValue(parseTokens("'-Infinity"), { onError })).resolves.toBe(-Infinity); expect(errors).toEqual([]); }); it('parses NaN string', async () => { - await expect(readValue(readTokens("'NaN"), { onError })).resolves.toBeNaN(); + await expect(readValue(parseTokens("'NaN"), { onError })).resolves.toBeNaN(); expect(errors).toEqual([]); }); it('rejects non-numeric string', async () => { - await expect(readValue(readTokens("'abc"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'abc"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -134,23 +135,23 @@ describe('UcNumber deserializer', () => { ]); }); it('deserializes infinity', async () => { - await expect(readValue(readTokens('!Infinity'))).resolves.toBe(Infinity); + await expect(readValue(parseTokens('!Infinity'))).resolves.toBe(Infinity); }); it('deserializes negative infinity', async () => { - await expect(readValue(readTokens('!-Infinity'))).resolves.toBe(-Infinity); + await expect(readValue(parseTokens('!-Infinity'))).resolves.toBe(-Infinity); }); it('deserializes NaN', async () => { - await expect(readValue(readTokens('!NaN'))).resolves.toBeNaN(); + await expect(readValue(parseTokens('!NaN'))).resolves.toBeNaN(); }); }); describe('when strings rejected', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ models: { - readValue: ucNumber({ string: 'reject' }), + readValue: { model: ucNumber({ string: 'reject' }) }, }, features: [ucdSupportPrimitives, ucdSupportNonFinite], }); @@ -159,7 +160,7 @@ describe('UcNumber deserializer', () => { }); it('rejects numeric string', async () => { - await expect(readValue(readTokens("'1"), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens("'1"), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { diff --git a/src/schema/numeric/uc-number.ts b/src/schema/numeric/uc-number.ts index b5326f10..13c9e51f 100644 --- a/src/schema/numeric/uc-number.ts +++ b/src/schema/numeric/uc-number.ts @@ -70,7 +70,7 @@ export namespace UcNumber { /*#__NO_SIDE_EFFECTS__*/ export function ucNumber(options?: UcNumber.Options): UcNumber.Schema { if (options) { - const { where, string } = options; + const { where, within, string } = options; const variant: UcNumber.Variant | undefined = string ? { string, @@ -95,6 +95,7 @@ export function ucNumber(options?: UcNumber.Options): UcNumber.Schema { ...asArray(where), ] : where, + within, }); } diff --git a/src/schema/numeric/uc-numeric-range.validator.spec.ts b/src/schema/numeric/uc-numeric-range.validator.spec.ts index 1052d619..3dbebf30 100644 --- a/src/schema/numeric/uc-numeric-range.validator.spec.ts +++ b/src/schema/numeric/uc-numeric-range.validator.spec.ts @@ -233,7 +233,7 @@ describe('number range validator', () => { }); async function compile(schema: UcSchema): Promise> { - const compiler = new UcdCompiler({ models: { readValue: ['sync', schema] } }); + const compiler = new UcdCompiler({ models: { readValue: { model: schema, mode: 'sync' } } }); const { readValue } = await compiler.evaluate(); return readValue; @@ -424,7 +424,7 @@ describe('bigint range validator', () => { }); async function compile(schema: UcSchema): Promise> { - const compiler = new UcdCompiler({ models: { readValue: ['sync', schema] } }); + const compiler = new UcdCompiler({ models: { readValue: { model: schema, mode: 'sync' } } }); const { readValue } = await compiler.evaluate(); return readValue; diff --git a/src/schema/string/uc-string-length.validator.spec.ts b/src/schema/string/uc-string-length.validator.spec.ts index 939c1365..eedc6194 100644 --- a/src/schema/string/uc-string-length.validator.spec.ts +++ b/src/schema/string/uc-string-length.validator.spec.ts @@ -105,7 +105,7 @@ describe('string length validator', () => { }); async function compile(schema: UcSchema): Promise> { - const compiler = new UcdCompiler({ models: { readValue: ['sync', schema] } }); + const compiler = new UcdCompiler({ models: { readValue: { model: schema, mode: 'sync' } } }); const { readValue } = await compiler.evaluate(); return readValue; diff --git a/src/schema/string/uc-string-pattern.validator.spec.ts b/src/schema/string/uc-string-pattern.validator.spec.ts index 9de28578..e37f1f86 100644 --- a/src/schema/string/uc-string-pattern.validator.spec.ts +++ b/src/schema/string/uc-string-pattern.validator.spec.ts @@ -67,7 +67,10 @@ describe('ucItMatches', () => { schema: UcSchema, options?: Partial>, ): Promise> { - const compiler = new UcdCompiler({ ...options, models: { readValue: ['sync', schema] } }); + const compiler = new UcdCompiler({ + ...options, + models: { readValue: { model: schema, mode: 'sync' } }, + }); const { readValue } = await compiler.evaluate(); return readValue; diff --git a/src/schema/string/uc-string.deserializer.spec.ts b/src/schema/string/uc-string.deserializer.spec.ts index 0102414f..914dac61 100644 --- a/src/schema/string/uc-string.deserializer.spec.ts +++ b/src/schema/string/uc-string.deserializer.spec.ts @@ -1,10 +1,11 @@ import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; -import { readTokens } from '../../spec/read-chunks.js'; +import { UcdModels } from '../../compiler/deserialization/ucd-models.js'; +import { parseTokens } from '../../spec/read-chunks.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcErrorInfo } from '../uc-error.js'; -import { UcNullable, ucNullable } from '../uc-nullable.js'; -import { UcModel } from '../uc-schema.js'; +import { ucNullable } from '../uc-nullable.js'; +import { UcDataType } from '../uc-schema.js'; import { UcString, ucString } from './uc-string.js'; describe('UcString deserializer', () => { @@ -18,67 +19,69 @@ describe('UcString deserializer', () => { }); describe('by default', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcModel }>({ - models: { - readValue: String, + const compiler = new UcdCompiler<{ readValue: UcdModels.UniversalEntry> }>( + { + models: { + readValue: { model: String }, + }, }, - }); + ); ({ readValue } = await compiler.evaluate()); }); it('deserializes string', async () => { - await expect(readValue(readTokens('some string'))).resolves.toBe('some string'); + await expect(readValue(parseTokens('some string'))).resolves.toBe('some string'); }); it('deserializes multiline string', async () => { - await expect(readValue(readTokens('prefix\r', '\n-end(suffix'))).resolves.toBe( + await expect(readValue(parseTokens('prefix\r', '\n-end(suffix'))).resolves.toBe( 'prefix\r\n-end(suffix', ); - await expect(readValue(readTokens('prefix\r', '\n!end(suffix'))).resolves.toBe( + await expect(readValue(parseTokens('prefix\r', '\n!end(suffix'))).resolves.toBe( 'prefix\r\n!end(suffix', ); }); it('URI-decodes string', async () => { - await expect(readValue(readTokens('some%20string'))).resolves.toBe('some string'); + await expect(readValue(parseTokens('some%20string'))).resolves.toBe('some string'); }); it('ignores leading and trailing whitespace', async () => { - await expect(readValue(readTokens(' \n some string \n '))).resolves.toBe('some string'); + await expect(readValue(parseTokens(' \n some string \n '))).resolves.toBe('some string'); }); it('deserializes empty string', async () => { - await expect(readValue(readTokens(''))).resolves.toBe(''); - await expect(readValue(readTokens(')'))).resolves.toBe(''); + await expect(readValue(parseTokens(''))).resolves.toBe(''); + await expect(readValue(parseTokens(')'))).resolves.toBe(''); }); it('deserializes number as string', async () => { - await expect(readValue(readTokens('123'))).resolves.toBe('123'); - await expect(readValue(readTokens('-123'))).resolves.toBe('-123'); - await expect(readValue(readTokens('0x123'))).resolves.toBe('0x123'); + await expect(readValue(parseTokens('123'))).resolves.toBe('123'); + await expect(readValue(parseTokens('-123'))).resolves.toBe('-123'); + await expect(readValue(parseTokens('0x123'))).resolves.toBe('0x123'); }); it('deserializes hyphens', async () => { - await expect(readValue(readTokens('-'))).resolves.toBe('-'); - await expect(readValue(readTokens('--'))).resolves.toBe('--'); - await expect(readValue(readTokens('---'))).resolves.toBe('---'); + await expect(readValue(parseTokens('-'))).resolves.toBe('-'); + await expect(readValue(parseTokens('--'))).resolves.toBe('--'); + await expect(readValue(parseTokens('---'))).resolves.toBe('---'); }); it('deserializes minus-prefixed string', async () => { - await expect(readValue(readTokens('-a%55c'))).resolves.toBe('-aUc'); - await expect(readValue(readTokens('%2Da%55c'))).resolves.toBe('-aUc'); + await expect(readValue(parseTokens('-a%55c'))).resolves.toBe('-aUc'); + await expect(readValue(parseTokens('%2Da%55c'))).resolves.toBe('-aUc'); }); it('deserializes quoted string', async () => { - await expect(readValue(readTokens("'abc"))).resolves.toBe('abc'); + await expect(readValue(parseTokens("'abc"))).resolves.toBe('abc'); }); it('respects trailing whitespace after quoted string', async () => { - await expect(readValue(readTokens("'abc \n "))).resolves.toBe('abc \n '); + await expect(readValue(parseTokens("'abc \n "))).resolves.toBe('abc \n '); }); it('deserializes balanced parentheses within quoted string', async () => { - await expect(readValue(readTokens("'abc(def()))"))).resolves.toBe('abc(def())'); + await expect(readValue(parseTokens("'abc(def()))"))).resolves.toBe('abc(def())'); }); it('does not close unbalanced parentheses within quoted string', async () => { - await expect(readValue(readTokens("'abc(def("))).resolves.toBe('abc(def('); + await expect(readValue(parseTokens("'abc(def("))).resolves.toBe('abc(def('); }); it('rejects map', async () => { - await expect(readValue(readTokens('$foo(bar)'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('$foo(bar)'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -95,7 +98,7 @@ describe('UcString deserializer', () => { ]); }); it('rejects empty map', async () => { - await expect(readValue(readTokens('$'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('$'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -112,7 +115,7 @@ describe('UcString deserializer', () => { ]); }); it('rejects suffix', async () => { - await expect(readValue(readTokens('$foo'), { onError })).resolves.toBeUndefined(); + await expect(readValue(parseTokens('$foo'), { onError })).resolves.toBeUndefined(); expect(errors).toEqual([ { @@ -130,12 +133,12 @@ describe('UcString deserializer', () => { }); describe('when nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcNullable }>({ + const compiler = new UcdCompiler({ models: { - readValue: ucNullable(String), + readValue: { model: ucNullable(String) }, }, }); @@ -149,14 +152,16 @@ describe('UcString deserializer', () => { }); describe('when raw values parsed', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcString.Schema }>({ + const compiler = new UcdCompiler({ models: { - readValue: ucString({ - raw: 'parse', - }), + readValue: { + model: ucString({ + raw: 'parse', + }), + }, }, }); @@ -250,16 +255,18 @@ describe('UcString deserializer', () => { }); describe('when nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - const compiler = new UcdCompiler<{ readValue: UcNullable }>({ + const compiler = new UcdCompiler({ models: { - readValue: ucNullable( - ucString({ - raw: 'parse', - }), - ), + readValue: { + model: ucNullable( + ucString({ + raw: 'parse', + }), + ), + }, }, }); diff --git a/src/schema/string/uc-string.ts b/src/schema/string/uc-string.ts index 0ea8343b..fc5d5585 100644 --- a/src/schema/string/uc-string.ts +++ b/src/schema/string/uc-string.ts @@ -75,7 +75,7 @@ export namespace UcString { /*#__NO_SIDE_EFFECTS__*/ export function ucString(options?: UcString.Options): UcString.Schema { if (options) { - const { where, raw } = options; + const { where, within, raw } = options; const variant: UcString.Variant | undefined = raw ? { raw, @@ -100,6 +100,7 @@ export function ucString(options?: UcString.Options): UcString.Schema { ...asArray(where), ] : where, + within, }); } diff --git a/src/schema/uc-constraints.ts b/src/schema/uc-constraints.ts index b2672e83..52834d46 100644 --- a/src/schema/uc-constraints.ts +++ b/src/schema/uc-constraints.ts @@ -36,6 +36,11 @@ export interface UcConstraints readonly validator?: UcFeatureConstraint | readonly UcFeatureConstraint[] | undefined; } +/** + * Omnivorous constraints that can be applied to any schema. + */ +export type UcOmniConstraints = UcConstraints; + /** * Name of schema processor. * @@ -103,27 +108,21 @@ export function ucConstraints = UcSchema>( } const result: { - -readonly [processorName in keyof UcConstraints]: UcConstraints< - T, - TSchema - >[processorName]; + -readonly [processorName in UcProcessorName]?: UcConstraints[processorName]; } = {}; for (const constr of constraints) { - for (const [processorName, forProcessor] of Object.entries(constr) as [ + for (const [processorName, features] of Object.entries(constr) as [ UcProcessorName, UcFeatureConstraint | readonly UcFeatureConstraint[] | undefined, ][]) { - if (forProcessor) { - const prevForProcessor = result[processorName]; + if (features) { + const prevFeatures = result[processorName]; - if (prevForProcessor) { - result[processorName] = elementOrArray([ - ...asArray(prevForProcessor), - ...asArray(forProcessor), - ]); + if (prevFeatures) { + result[processorName] = elementOrArray([...asArray(prevFeatures), ...asArray(features)]); } else { - result[processorName] = forProcessor; + result[processorName] = features; } } } diff --git a/src/schema/uc-deserializer.ts b/src/schema/uc-deserializer.ts index f0148a96..ac773ac6 100644 --- a/src/schema/uc-deserializer.ts +++ b/src/schema/uc-deserializer.ts @@ -5,19 +5,23 @@ import { UcToken } from '../syntax/uc-token.js'; import { UcBundle } from './uc-bundle.js'; import { UcErrorInfo } from './uc-error.js'; import { ucModelName } from './uc-model-name.js'; +import { UcFormatName } from './uc-presentations.js'; import { UcModel } from './uc-schema.js'; /** - * Data deserializer signature. + * Data deserializer. * * Reads the data value from the given input. Deserializer may be either synchronous or asynchronous depending * on input type. * - * Serializers generated by {@link churi/compiler.js!UcdLib compiler}. - * * @typeParam T - Deserialized data type. */ -export type UcDeserializer = { +export interface UcDeserializer { + /** + * Brand property not supposed to be declared. + */ + __UcDeserializerByTokens__: false; + /** * Reads the data _synchronously_ from the given string or array of {@link UcToken tokens}. * @@ -31,13 +35,15 @@ export type UcDeserializer = { /** * Reads the data _asynchronously_ from the given readable `stream`. * + * Scans input `stream` for tokens and reads the encoded data. + * * @param stream - Input stream to read the data from. * @param options - Deserialization options. * * @returns Promise resolved to deserialized data value. */ - (stream: ReadableStream, options?: UcDeserializer.Options): Promise; -}; + (stream: ReadableStream, options?: UcDeserializer.Options): Promise; +} export namespace UcDeserializer { /** @@ -55,26 +61,90 @@ export namespace UcDeserializer { export type Mode = 'sync' | 'async' | 'universal'; /** - * Asynchronous data deserializer signature. + * Token deserializer. * - * Reads the data from stream. + * Reads the data value from input tokens. Deserializer may be either synchronous or asynchronous depending + * on input type. * - * Serializers of this kind generated by {@link churi/compiler.js!UcdLib compiler} in `async` - * deserialization {@link Mode mode}. + * @typeParam T - Deserialized data type. + */ + export interface ByTokens { + /** + * Brand property not supposed to be declared. + */ + __UcDeserializerByTokens__: true; + + /** + * Reads the data _synchronously_ from the given string or array of {@link UcToken tokens}. + * + * @param input - Either input string or array of tokens to parse. + * @param options - Deserialization options. + * + * @returns Deserialized data value. + */ + (input: string | readonly UcToken[], options?: UcDeserializer.Options): T; + + /** + * Reads the data _asynchronously_ from the given readable `stream` of tokens. + * + * @param stream - Input stream to read the data from. + * @param options - Deserialization options. + * + * @returns Promise resolved to deserialized data value. + */ + (stream: ReadableStream, options?: UcDeserializer.Options): Promise; + } + + /** + * Asynchronous deserializer. * * @typeParam T - Deserialized data type. - * @param stream - Input stream to read the data from. - * @param options - Deserialization options. + */ + export interface Async { + /** + * Brand property not supposed to be declared. + */ + __UcDeserializerByTokens__: false; + + /** + * Reads the data _asynchronously_ from the given readable `stream`. + * + * Scans input `stream` for tokens and reads the encoded data. + * + * @param stream - Input stream to read the data from. + * @param options - Deserialization options. + * + * @returns Promise resolved to deserialized data value. + */ + (stream: ReadableStream, options?: UcDeserializer.Options): Promise; + } + + /** + * Asynchronous deserializer by tokens. * - * @returns Promise resolved to deserialized data value. + * Reads data encoded as input `stream` of tokens. + * + * @typeParam T - Deserialized data type. */ - export type Async = ( - stream: ReadableStream, - options?: UcDeserializer.Options, - ) => Promise; + export interface AsyncByTokens { + /** + * Brand property not supposed to be declared. + */ + __UcDeserializerByTokens__: true; + + /** + * Reads the data _asynchronously_ from the given readable `stream` of tokens. + * + * @param stream - Input stream to read the data from. + * @param options - Deserialization options. + * + * @returns Promise resolved to deserialized data value. + */ + (stream: ReadableStream, options?: UcDeserializer.Options): Promise; + } /** - * Synchronous data deserializer signature. + * Synchronous data deserializer. * * Reads the data from `input` string or array of {@link UcToken tokens}. * @@ -128,46 +198,73 @@ export namespace UcDeserializer { readonly onMeta?: MetaUcrx; } - /** - * {@link UcDeserializer Universal} deserializer {@link createUcDeserializer compiler} configuration. - */ - export interface Config { + export interface BaseConfig { /** * Target bundle the compiled deserializer will be included into. * * Default bundle will be used when omitted. */ readonly bundle?: UcBundle | undefined; + } + /** + * {@link UcDeserializer Universal} deserializer {@link createUcDeserializer compiler} configuration. + */ + export interface Config extends BaseConfig { readonly mode?: 'universal' | undefined; + + /** + * Expected format of the input. + * + * By default, expects data encoded with {@link UcChargeLexer URI Charge Notation}. + */ + readonly from?: UcFormatName | undefined; + } + + /** + * {@link UcDeserializer.ByTokens By-tokens} deserializer {@link createUcDeserializer compiler} configuration. + */ + export interface ByTokensConfig extends BaseConfig { + readonly mode?: 'universal' | undefined; + + readonly from: 'tokens'; } /** * {@link Sync Synchronous} deserializer {@link createUcDeserializer compiler} configuration. */ - export interface SyncConfig { + export interface SyncConfig extends BaseConfig { + readonly mode: 'sync'; + /** - * Target bundle the compiled deserializer will be included into. + * Expected format of the input. * - * Default bundle will be used when omitted. + * By default, expects data encoded with {@link UcChargeLexer URI Charge Notation}. */ - readonly bundle?: UcBundle | undefined; - - readonly mode: 'sync'; + readonly from?: UcFormatName | undefined; } /** * {@link Async Asynchronous} deserializer {@link createUcDeserializer compiler} configuration. */ - export interface AsyncInit { + export interface AsyncConfig extends BaseConfig { + readonly mode: 'async'; + /** - * Target bundle the compiled deserializer will be included into. + * Expected format of the input. * - * Default bundle will be used when omitted. + * By default, expects data encoded with {@link UcChargeLexer URI Charge Notation}. */ - readonly bundle?: UcBundle | undefined; + readonly from?: UcFormatName | undefined; + } + /** + * {@link Async Asynchronous} by-tokens deserializer {@link createUcDeserializer compiler} configuration. + */ + export interface AsyncByTokensConfig extends BaseConfig { readonly mode: 'async'; + + readonly from: 'tokens'; } } @@ -190,6 +287,25 @@ export function createUcDeserializer( config?: UcDeserializer.Config, ): UcDeserializer; +/** + * Compiles {@link UcDeserializer.ByTokens by-tokens deserializer} for the given data `model`. + * + * **This is a placeholder**. It is replaced with actual deserializer when TypeScript code compiled with + * [ts-transformer-churi] enabled. It is expected that the result of this function call is stored to constant. + * + * @typeParam T - Deserialized data type. + * @param model - Deserialized data model. + * @param config - Compiler configuration. + * + * @returns Universal deserializer instance. + * + * [ts-transformer-churi]: https://www.npmjs.com/package/ts-transformer-churi + */ +export function createUcDeserializer( + model: UcModel, + config?: UcDeserializer.ByTokensConfig, +): UcDeserializer.ByTokens; + /** * Compiles {@link UcDeserializer.Sync synchronous deserializer} for the given data `model`. * @@ -225,16 +341,44 @@ export function createUcDeserializer( */ export function createUcDeserializer( model: UcModel, - config: UcDeserializer.AsyncInit, + config: UcDeserializer.AsyncConfig, ): UcDeserializer.Async; +/** + * Compiles {@link UcDeserializer.Async by-tokens asynchronous deserializer} for the given data `model`. + * + * **This is a placeholder**. It is replaced with actual deserializer when TypeScript code compiled with + * [ts-transformer-churi] enabled. It is expected that the result of this function call is stored to constant. + * + * @typeParam T - Deserialized data type. + * @param model - Deserialized data model. + * @param config - Compiler configuration. + * + * @returns Asynchronous deserializer instance. + * + * [ts-transformer-churi]: https://www.npmjs.com/package/ts-transformer-churi + */ +export function createUcDeserializer( + model: UcModel, + config: UcDeserializer.AsyncByTokensConfig, +): UcDeserializer.AsyncByTokens; + export function createUcDeserializer( model: UcModel, - _init?: UcDeserializer.Config | UcDeserializer.SyncConfig | UcDeserializer.AsyncInit, -): UcDeserializer { - return () => { + _init?: + | UcDeserializer.Config + | UcDeserializer.ByTokensConfig + | UcDeserializer.SyncConfig + | UcDeserializer.AsyncConfig + | UcDeserializer.AsyncByTokensConfig, +): + | UcDeserializer.Sync + | UcDeserializer.Async + | UcDeserializer.ByTokens + | UcDeserializer.AsyncByTokens { + return (() => { throw new TypeError( `Can not deserialize ${ucModelName(model)}. Is "ts-transformer-churi" applied?`, ); - }; + }) as unknown as UcDeserializer | UcDeserializer.ByTokens; } diff --git a/src/schema/uc-presentations.ts b/src/schema/uc-presentations.ts new file mode 100644 index 00000000..aabe7370 --- /dev/null +++ b/src/schema/uc-presentations.ts @@ -0,0 +1,146 @@ +import { UcConstraints, ucConstraints } from './uc-constraints.js'; +import { UcSchema } from './uc-schema.js'; + +/** + * Schema {@link UcConstraints constraints} for schema instances. + * + * Each property corresponds to particular schema instance presentation. + * + * @typeParam T - Implied data type. + * @typeParam TSchema - Supported schema type. + */ +export type UcPresentations< + T = unknown, + TSchema extends UcSchema = UcSchema, +> = UcFormatPresentations & UcInsetPresentations; + +/** + * Name of schema instance presentation. + * + * Used to provide {@link UcConstraints schema constraints} for particular presentation. + */ +export type UcPresentationName = keyof UcPresentations; + +/** + * Schema {@link UcConstraints constraints} for presentations of schema instances used as top-level formats. + * + * Each property corresponds to particular format presentation. + * + * More presentations may be added by augmenting this interface. + * + * @typeParam T - Implied data type. + * @typeParam TSchema - Supported schema type. + */ +export interface UcFormatPresentations< + out T = unknown, + out TSchema extends UcSchema = UcSchema, +> { + /** + * Constraints for schema instance represented in {@link UcChargeLexer URI Charge Notation}. + */ + readonly charge?: UcConstraints | undefined; + + /** + * Constraints for schema instance represented as {@link UcPlainTextLexer plain text}. + */ + readonly plainText?: UcConstraints | undefined; + + /** + * Constraints for schema instance represented as {@link UcURIEncodedLexer URI-encoded value}. + */ + readonly uriEncoded?: UcConstraints | undefined; + + /** + * Constraints for schema instance represented as `&` or `;` - separated {@link UcURIParamsLexer URI parameters}. + * + * This works for e.g.: + * + * - {@link ChURIQuery URI query} parameters, + * - {@link ChURIMatrix URI matrix} parameters, + * - {@link ChURIAnchor URI hash} parameters, + * - `application/x-www-form-urlencoded` body. + */ + readonly uriParams?: UcConstraints | undefined; +} + +/** + * Name of supported input format presentation. + */ +export type UcFormatName = keyof UcFormatPresentations; + +/** + * Schema {@link UcConstraints constraints} for presentations of schema instances existing as other presentations' + * insets. + * + * Each property corresponds to particular inset presentation. + * + * More presentations may be added by augmenting this interface. + * + * @typeParam T - Implied data type. + * @typeParam TSchema - Supported schema type. + */ +export interface UcInsetPresentations< + out T = unknown, + out TSchema extends UcSchema = UcSchema, +> { + /** + * Constraints for schema instance represented as {@link UcURIParamsLexer URI parameter} value. + * + * This works for e.g.: + * + * - {@link ChURIQuery URI query} parameters, + * - {@link ChURIMatrix URI matrix} parameters, + * - {@link ChURIAnchor URI hash} parameters, + * - `application/x-www-form-urlencoded` body. + */ + readonly uriParam?: UcConstraints | undefined; +} + +/** + * Name of supported inset presentation. + */ +export type UcInsetName = keyof UcInsetPresentations; + +/** + * Combines schema instance presentation constraints. + * + * @typeParam T - Implied data type. + * @typeParam TSchema - Supported schema type. + * @param presentations - Constraints to combine. + * + * @returns Combined schema constraints, or `undefined` if nothing to combine. + */ +/*#__NO_SIDE_EFFECTS__*/ +export function ucPresentations = UcSchema>( + ...presentations: UcPresentations[] +): UcPresentations | undefined { + if (presentations.length < 2) { + return presentations.length ? presentations[0] : undefined; + } + + const result: { + -readonly [presentationName in UcPresentationName]?: UcPresentations< + T, + TSchema + >[presentationName]; + } = {}; + + for (const constr of presentations) { + for (const [presentationName, constraints] of Object.entries(constr) as [ + UcPresentationName, + UcConstraints | undefined, + ][]) { + if (constraints) { + const prevConstraints = result[presentationName]; + + if (prevConstraints) { + result[presentationName] = ucConstraints(prevConstraints, constraints); + } else { + result[presentationName] = constraints; + } + } + } + } + + return result; +} diff --git a/src/schema/uc-schema.spec.ts b/src/schema/uc-schema.spec.ts index 7bbb07d5..ebd4db21 100644 --- a/src/schema/uc-schema.spec.ts +++ b/src/schema/uc-schema.spec.ts @@ -57,6 +57,65 @@ describe('ucSchema', () => { }, }); }); + it('merges schema presentations', () => { + expect( + ucSchema( + { + type: 'test-type', + within: { + charge: { + deserializer: { + use: 'test-feature', + from: 'test-module', + with: { test: 1 }, + }, + }, + }, + }, + { + within: { + charge: { + deserializer: [ + { + use: 'test-feature', + from: 'test-module', + with: { test: 2 }, + }, + { + use: 'test-feature3', + from: 'test-module', + with: { test: 3 }, + }, + ], + }, + }, + }, + ), + ).toEqual({ + type: 'test-type', + within: { + charge: { + deserializer: [ + { + use: 'test-feature', + from: 'test-module', + with: { test: 1 }, + }, + { + use: 'test-feature', + from: 'test-module', + with: { test: 2 }, + }, + { + use: 'test-feature3', + from: 'test-module', + with: { test: 3 }, + }, + ], + }, + }, + }); + }); it('returns schema without extension as is', () => { const schema: UcSchema = { type: 'test-type', diff --git a/src/schema/uc-schema.ts b/src/schema/uc-schema.ts index fc04be07..b567c41c 100644 --- a/src/schema/uc-schema.ts +++ b/src/schema/uc-schema.ts @@ -1,5 +1,6 @@ import { asArray } from '@proc7ts/primitives'; import { UcConstraints, ucConstraints } from './uc-constraints.js'; +import { UcPresentations, ucPresentations } from './uc-presentations.js'; /** * Data schema definition. @@ -47,6 +48,11 @@ export interface UcSchema { */ readonly where?: UcConstraints | undefined; + /** + * Schema instance presentation constraints. + */ + readonly within?: UcPresentations | undefined; + /** * Custom schema name. * @@ -123,7 +129,7 @@ export function ucSchema( /*#__NO_SIDE_EFFECTS__*/ export function ucSchema = UcSchema>( model: UcModel, - { where }: UcSchema.Extension = {}, + { where, within }: UcSchema.Extension = {}, ): TSchema { if (typeof model === 'function') { return { @@ -131,13 +137,18 @@ export function ucSchema = UcSchema>( nullable: false, type: model, where: ucConstraints(...asArray(where)), + within: ucPresentations(...asArray(within)), } as TSchema; } - if (!where) { + if (!where && !within) { return model; } - return { ...model, where: ucConstraints(...asArray(model.where), ...asArray(where)) }; + return { + ...model, + where: ucConstraints(...asArray(model.where), ...asArray(where)), + within: ucPresentations(...asArray(model.within), ...asArray(within)), + }; } /** @@ -163,6 +174,14 @@ export namespace UcSchema { * Additional schema constraints. */ readonly where?: UcConstraints | readonly UcConstraints[] | undefined; + + /** + * Additional schema instance presentation constraints. + */ + readonly within?: + | UcPresentations + | readonly UcPresentations[] + | undefined; } /** * Schema type corresponding to the given model type. diff --git a/src/schema/unknown/uc-unknown.deserializer.spec.ts b/src/schema/unknown/uc-unknown.deserializer.spec.ts index d05fc91f..4279c334 100644 --- a/src/schema/unknown/uc-unknown.deserializer.spec.ts +++ b/src/schema/unknown/uc-unknown.deserializer.spec.ts @@ -6,7 +6,7 @@ import { ucdSupportPlainEntity } from '../../spec/plain.format.js'; import { ucdSupportTimestampFormat } from '../../spec/timestamp.format.js'; import { UcDeserializer } from '../uc-deserializer.js'; import { UcErrorInfo } from '../uc-error.js'; -import { UcNullable, ucNullable } from '../uc-nullable.js'; +import { ucNullable } from '../uc-nullable.js'; import { UcUnknown, ucUnknown } from './uc-unknown.js'; describe('UcUnknown deserializer', () => { @@ -111,11 +111,11 @@ describe('UcUnknown deserializer', () => { }); describe('for non-nullable', () => { - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { const compiler = new UcdCompiler({ - models: { readValue: ucNullable(ucUnknown(), false) }, + models: { readValue: { model: ucNullable(ucUnknown(), false) } }, }); ({ readValue } = await compiler.evaluate()); @@ -138,14 +138,14 @@ describe('UcUnknown deserializer', () => { }); describe('with custom entity', () => { - let compiler: UcdCompiler<{ readValue: UcNullable }>; - let readValue: UcDeserializer; + let readValue: UcDeserializer.ByTokens; beforeAll(async () => { - compiler = new UcdCompiler({ - models: { readValue: ucUnknown() }, + const compiler = new UcdCompiler({ + models: { readValue: { model: ucUnknown() } }, features: [ucdSupportPrimitives, ucdSupportPlainEntity, ucdSupportTimestampFormat], }); + ({ readValue } = await compiler.evaluate()); }); diff --git a/src/spec/read-chunks.ts b/src/spec/read-chunks.ts index 1ec59227..fba862de 100644 --- a/src/spec/read-chunks.ts +++ b/src/spec/read-chunks.ts @@ -1,27 +1,15 @@ import { Readable } from 'node:stream'; import { UcLexerStream } from '../syntax/uc-lexer-stream.js'; -import { UcLexer } from '../syntax/uc-lexer.js'; import { UcToken } from '../syntax/uc-token.js'; export function readChunks(...chunks: string[]): ReadableStream { return Readable.toWeb(Readable.from(chunks)) as ReadableStream; } -export function readTokens(...chunks: string[]): ReadableStream { - return readChunks(...chunks).pipeThrough(new UcLexerStream()); +export function readTokens(...tokens: UcToken[]): ReadableStream { + return Readable.toWeb(Readable.from(tokens)) as ReadableStream; } -export function parseTokens(...chunks: string[]): readonly UcToken[] { - const tokens: UcToken[] = []; - const tokenizer = new UcLexer(token => { - tokens.push(token); - }); - - for (const chunk of chunks) { - tokenizer.scan(chunk); - } - - tokenizer.flush(); - - return tokens; +export function parseTokens(...chunks: string[]): ReadableStream { + return readChunks(...chunks).pipeThrough(new UcLexerStream()); } diff --git a/src/spec/timestamp.format.ts b/src/spec/timestamp.format.ts index 24b50fd6..136ad973 100644 --- a/src/spec/timestamp.format.ts +++ b/src/spec/timestamp.format.ts @@ -107,6 +107,7 @@ class TimestampUcrxClass extends UcrxClass { constructor(lib: UcrxLib, schema: UcSchema) { super({ + lib, schema, typeName: 'Timestamp', baseClass: lib.baseUcrx, diff --git a/src/syntax/lexers/mod.ts b/src/syntax/lexers/mod.ts new file mode 100644 index 00000000..08f8024d --- /dev/null +++ b/src/syntax/lexers/mod.ts @@ -0,0 +1,5 @@ +export * from './uc-charge.lexer.js'; +export * from './uc-opaque.lexer.js'; +export * from './uc-plain-text.lexer.js'; +export * from './uc-uri-encoded.lexer.js'; +export * from './uc-uri-params.lexer.js'; diff --git a/src/syntax/uc-lexer.spec.ts b/src/syntax/lexers/uc-charge.lexer.spec.ts similarity index 51% rename from src/syntax/uc-lexer.spec.ts rename to src/syntax/lexers/uc-charge.lexer.spec.ts index d96ec49f..f4846d87 100644 --- a/src/syntax/uc-lexer.spec.ts +++ b/src/syntax/lexers/uc-charge.lexer.spec.ts @@ -1,5 +1,10 @@ -import { beforeEach, describe, expect, it } from '@jest/globals'; -import { UcLexer } from './uc-lexer.js'; +import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { esline } from 'esgen'; +import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; +import { UC_MODULE_CHURI } from '../../compiler/impl/uc-modules.js'; +import { ucMap } from '../../schema/map/uc-map.js'; +import { UcDeserializer } from '../../schema/uc-deserializer.js'; +import { ucUnknown } from '../../schema/unknown/uc-unknown.js'; import { UC_TOKEN_AMPERSAND, UC_TOKEN_APOSTROPHE, @@ -25,65 +30,67 @@ import { UC_TOKEN_SEMICOLON, UC_TOKEN_SLASH, UcToken, -} from './uc-token.js'; +} from '../uc-token.js'; +import { UcChargeLexer, ucInsetCharge } from './uc-charge.lexer.js'; +import { UcURIParamsLexer } from './uc-uri-params.lexer.js'; -describe('UcLexer', () => { - let tokenizer: UcLexer; +describe('UcChargeLexer', () => { + let lexer: UcChargeLexer; let tokens: UcToken[]; beforeEach(() => { - tokenizer = new UcLexer(token => { + lexer = new UcChargeLexer(token => { tokens.push(token); }); tokens = []; }); it('handles Windows-style line separators', () => { - tokenizer.scan('abc\r'); - tokenizer.scan('\ndef'); - tokenizer.flush(); + lexer.scan('abc\r'); + lexer.scan('\ndef'); + lexer.flush(); expect(tokens).toEqual(['abc', UC_TOKEN_CRLF, 'def']); }); it('handles CR', () => { - tokenizer.scan('abc\rdef'); - tokenizer.flush(); + lexer.scan('abc\rdef'); + lexer.flush(); expect(tokens).toEqual(['abc', UC_TOKEN_CR, 'def']); }); it('handles CR as first char', () => { - tokenizer.scan('\rdef'); - tokenizer.flush(); + lexer.scan('\rdef'); + lexer.flush(); expect(tokens).toEqual([UC_TOKEN_CR, 'def']); }); it('handles CR after CR', () => { - tokenizer.scan('\r\rdef'); - tokenizer.flush(); + lexer.scan('\r\rdef'); + lexer.flush(); expect(tokens).toEqual([UC_TOKEN_CR, UC_TOKEN_CR, 'def']); }); it('handles CR after LF', () => { - tokenizer.scan('\n\rdef'); - tokenizer.flush(); + lexer.scan('\n\rdef'); + lexer.flush(); expect(tokens).toEqual([UC_TOKEN_LF, UC_TOKEN_CR, 'def']); }); it('handles LF', () => { - tokenizer.scan('abc\ndef'); - tokenizer.flush(); + lexer.scan('abc\ndef'); + lexer.flush(); expect(tokens).toEqual(['abc', UC_TOKEN_LF, 'def']); }); it('handles LF as first char', () => { - tokenizer.scan('\ndef'); - tokenizer.flush(); + lexer.scan('\ndef'); + lexer.flush(); expect(tokens).toEqual([UC_TOKEN_LF, 'def']); }); it('handles LF after LF', () => { - tokenizer.scan('\n\ndef'); - tokenizer.flush(); + lexer.scan('\n\ndef'); + lexer.flush(); expect(tokens).toEqual([UC_TOKEN_LF, UC_TOKEN_LF, 'def']); }); @@ -109,8 +116,8 @@ describe('UcLexer', () => { ['closing bracket', ']', UC_TOKEN_CLOSING_BRACKET], ])('around %s', (_name, char, token) => { it('reports pads', () => { - tokenizer.scan(`abc ${char} def`); - tokenizer.flush(); + lexer.scan(`abc ${char} def`); + lexer.flush(); expect(tokens).toEqual([ 'abc', @@ -128,12 +135,12 @@ describe('UcLexer', () => { ]); }); it('concatenates leading pads', () => { - tokenizer.scan('abc '); - tokenizer.scan(' '); - tokenizer.scan(char); - tokenizer.scan(' '); - tokenizer.scan(' def'); - tokenizer.flush(); + lexer.scan('abc '); + lexer.scan(' '); + lexer.scan(char); + lexer.scan(' '); + lexer.scan(' def'); + lexer.flush(); expect(tokens).toEqual([ 'abc', @@ -145,8 +152,8 @@ describe('UcLexer', () => { ]); }); it('handles mixed pads', () => { - tokenizer.scan(`abc ${char} \t\tdef`); - tokenizer.flush(); + lexer.scan(`abc ${char} \t\tdef`); + lexer.flush(); expect(tokens).toEqual([ 'abc', @@ -160,8 +167,8 @@ describe('UcLexer', () => { }); it('handles too long padding', () => { - tokenizer.scan('abc' + ' '.repeat(1000)); - tokenizer.flush(); + lexer.scan('abc' + ' '.repeat(1000)); + lexer.flush(); expect(tokens).toEqual([ 'abc', @@ -172,9 +179,9 @@ describe('UcLexer', () => { ]); }); it('handles paddings at the begin of input', () => { - tokenizer.scan(' '); - tokenizer.scan(' abc'); - tokenizer.flush(); + lexer.scan(' '); + lexer.scan(' abc'); + lexer.flush(); expect(tokens).toEqual([ UC_TOKEN_PREFIX_SPACE | (1 << 8), @@ -183,17 +190,17 @@ describe('UcLexer', () => { ]); }); it('handles paddings at the end of input', () => { - tokenizer.scan(''); - tokenizer.scan('abc '); - tokenizer.scan(' '); - tokenizer.flush(); + lexer.scan(''); + lexer.scan('abc '); + lexer.scan(' '); + lexer.flush(); expect(tokens).toEqual(['abc', UC_TOKEN_PREFIX_SPACE | (3 << 8)]); }); it('concatenates string tokens', () => { - tokenizer.scan('abc '); - tokenizer.scan(' def'); - tokenizer.flush(); + lexer.scan('abc '); + lexer.scan(' def'); + lexer.flush(); expect(tokens).toEqual(['abc def']); }); @@ -201,10 +208,89 @@ describe('UcLexer', () => { const input = '\u042a'; const encoded = encodeURIComponent('\u042a'); - tokenizer.scan(encoded.slice(0, 1)); - tokenizer.scan(encoded.slice(1)); - tokenizer.flush(); + lexer.scan(encoded.slice(0, 1)); + lexer.scan(encoded.slice(1)); + lexer.flush(); expect(tokens).toEqual([input]); }); + + describe('scanParam', () => { + it('decodes plus sign as space', () => { + expect(UcChargeLexer.scanParam('abc++', '++def')).toEqual(['abc def']); + }); + it('decodes plus sign as space padding', () => { + expect(UcChargeLexer.scanParam('++++abcdef')).toEqual([ + UC_TOKEN_PREFIX_SPACE, + UC_TOKEN_PREFIX_SPACE, + UC_TOKEN_PREFIX_SPACE, + UC_TOKEN_PREFIX_SPACE, + 'abcdef', + ]); + }); + }); +}); + +describe('ucInsetCharge', () => { + describe('in default mode', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ within: { uriParam: ucInsetCharge() } }), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('URI-decodes values', () => { + expect(readValue(`a='te%20st'`)).toEqual({ a: `te st'` }); + expect(readValue(`a=te+st`)).toEqual({ a: 'te+st' }); + expect(readValue(`a=%33`)).toEqual({ a: 3 }); + }); + }); + + describe('in plus-as-space mode', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ + within: { + uriParam: ucInsetCharge({ plusAsSpace: true }), + }, + }), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('decodes URI charge values', () => { + expect(readValue(`a='te%20st'`)).toEqual({ a: `te st'` }); + expect(readValue(`a='te+st'`)).toEqual({ a: `te st'` }); + expect(readValue(`a=%33`)).toEqual({ a: 3 }); + }); + }); }); diff --git a/src/syntax/lexers/uc-charge.lexer.ts b/src/syntax/lexers/uc-charge.lexer.ts new file mode 100644 index 00000000..0bc7bc49 --- /dev/null +++ b/src/syntax/lexers/uc-charge.lexer.ts @@ -0,0 +1,281 @@ +import { UcdInsetOptions } from '../../compiler/deserialization/ucd-support-inset.js'; +import { CHURI_MODULE, COMPILER_MODULE } from '../../impl/module-names.js'; +import { UcOmniConstraints } from '../../schema/uc-constraints.js'; +import { scanUcTokens } from '../scan-uc-tokens.js'; +import { UcLexer } from '../uc-lexer.js'; +import { + UC_TOKEN_AMPERSAND, + UC_TOKEN_APOSTROPHE, + UC_TOKEN_ASTERISK, + UC_TOKEN_AT_SIGN, + UC_TOKEN_CLOSING_BRACKET, + UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_COLON, + UC_TOKEN_COMMA, + UC_TOKEN_CR, + UC_TOKEN_CRLF, + UC_TOKEN_DOLLAR_SIGN, + UC_TOKEN_EQUALS_SIGN, + UC_TOKEN_EXCLAMATION_MARK, + UC_TOKEN_HASH, + UC_TOKEN_LF, + UC_TOKEN_OPENING_BRACKET, + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PLUS_SIGN, + UC_TOKEN_QUESTION_MARK, + UC_TOKEN_SEMICOLON, + UC_TOKEN_SLASH, + UcToken, +} from '../uc-token.js'; + +/** + * URI charge lexer that splits input string(s) onto tokens. + */ +export class UcChargeLexer implements UcLexer { + + /** + * Constructs URI charge lexer that decodes _plus sign_ (`"+" (U+002B)`) as {@link UC_TOKEN_PREFIX_SPACE space + * padding}. + * + * This is needed e.g. when tokenizing URI query parameters. + * + * @param emit - Emitter function called each time a token is found. + * + * @returns New URI charge lexer instance. + */ + static plusAsSpace(emit: (token: UcToken) => void): UcChargeLexer { + const lexer = new UcChargeLexer(emit); + + lexer.#tokens = this.#paramTokens; + + return lexer; + } + + /** + * Scans the `input` string for URI charge {@link UcToken tokens}. + * + * @param input - Array of input chunks to scan. + * + * @returns Array of tokens. + */ + static scan(...input: string[]): UcToken[] { + return scanUcTokens(emit => new this(emit), ...input); + } + + /** + * Scans the `input` string for URI query parameter charge {@link UcToken tokens}. + * + * In contrast to {@link scan}, decodes _plus sign_ (`"+" (U+002B)`) as {@link UC_TOKEN_PREFIX_SPACE space padding}. + * + * @param input - Array of input chunks to scan. + * + * @returns Array of tokens. + */ + static scanParam(...input: string[]): UcToken[] { + return scanUcTokens(emit => this.plusAsSpace(emit), ...input); + } + + static readonly #ucTokens: { readonly [token: string]: (lexer: UcChargeLexer) => void } = { + '\r': lexer => lexer.#addCR(), + '\n': lexer => lexer.#emitLF(), + '(': lexer => lexer.#emitReserved(UC_TOKEN_OPENING_PARENTHESIS), + ')': lexer => lexer.#emitReserved(UC_TOKEN_CLOSING_PARENTHESIS), + ',': lexer => lexer.#emitReserved(UC_TOKEN_COMMA), + '!': lexer => lexer.#emitReserved(UC_TOKEN_EXCLAMATION_MARK), + '#': lexer => lexer.#emitReserved(UC_TOKEN_HASH), + $: lexer => lexer.#emitReserved(UC_TOKEN_DOLLAR_SIGN), + '&': lexer => lexer.#emitReserved(UC_TOKEN_AMPERSAND), + "'": lexer => lexer.#emitReserved(UC_TOKEN_APOSTROPHE), + '*': lexer => lexer.#emitReserved(UC_TOKEN_ASTERISK), + '+': lexer => lexer.#emitReserved(UC_TOKEN_PLUS_SIGN), + '/': lexer => lexer.#emitReserved(UC_TOKEN_SLASH), + ':': lexer => lexer.#emitReserved(UC_TOKEN_COLON), + ';': lexer => lexer.#emitReserved(UC_TOKEN_SEMICOLON), + '=': lexer => lexer.#emitReserved(UC_TOKEN_EQUALS_SIGN), + '?': lexer => lexer.#emitReserved(UC_TOKEN_QUESTION_MARK), + '@': lexer => lexer.#emitReserved(UC_TOKEN_AT_SIGN), + '[': lexer => lexer.#emitReserved(UC_TOKEN_OPENING_BRACKET), + ']': lexer => lexer.#emitReserved(UC_TOKEN_CLOSING_BRACKET), + }; + + static readonly #paramTokens: { readonly [token: string]: (lexer: UcChargeLexer) => void } = { + ...this.#ucTokens, + '+': lexer => lexer.#addString(' '), + }; + + readonly #emit: (token: UcToken) => void; + #tokens: { readonly [token: string]: (lexer: UcChargeLexer) => void } = UcChargeLexer.#ucTokens; + #prev: string | typeof UC_TOKEN_CR | 0 = 0; + + /** + * Constructs URI charge lexer. + * + * @param emit - Emitter function called each time a token is found. + */ + constructor(emit: (token: UcToken) => void) { + this.#emit = emit; + } + + scan(chunk: string): void { + for (const token of chunk.split(UC_TOKEN_PATTERN)) { + this.#add(token); + } + } + + #add(token: string): void { + if (token.length === 1) { + const emitter = this.#tokens[token]; + + if (emitter) { + return emitter(this); + } + } + + this.#addString(token); + } + + #emitPrev(): void { + const prev = this.#prev; + + if (!prev) { + return; + } + this.#prev = 0; + + if (typeof prev === 'number') { + this.#emit(prev); + + return; + } + + const padStart = prev.search(UC_TRAILING_PADS_PATTERN); + + if (padStart < 0) { + return this.#emit(decodeURIComponent(prev)); + } + + if (padStart) { + // Emit non-empty token only. + this.#emit(decodeURIComponent(prev.slice(0, padStart))); + } + + this.#emitPads(prev, padStart, prev.length); + } + + #emitLF(): void { + if (this.#prev === UC_TOKEN_CR) { + this.#emit(UC_TOKEN_CRLF); + this.#prev = 0; + } else { + this.#emitPrev(); + this.#emit(UC_TOKEN_LF); + } + } + + #addCR(): void { + this.#emitPrev(); + this.#prev = UC_TOKEN_CR; + } + + #emitReserved(token: number): void { + this.#emitPrev(); + this.#emit(token); + } + + #addString(token: string): void { + if (!token) { + return; + } + + const prev = this.#prev; + + if (prev && typeof prev === 'number') { + this.#emit(prev); + } + + if (typeof prev === 'string') { + this.#prev += token; + } else { + const padEnd = token.search(UC_FIRST_NON_PAD_PATTERN); + + if (padEnd < 0) { + // Only pads found. + this.#emitPads(token, 0, token.length); + + return; + } + + if (padEnd) { + this.#emitPads(token, 0, padEnd); + this.#prev = token.slice(padEnd); + } else { + this.#prev = token; + } + } + } + + #emitPads(token: string, padStart: number, padEnd: number): void { + let pad = token.charCodeAt(padStart); + let count = 0; // One less than actual padding length. + + for (let i = padStart + 1; i < padEnd; ++i) { + const char = token.charCodeAt(i); + + if (char === pad && count < 255 /* prevent padding longer than 256 chars */) { + // Same padding. + ++count; + } else { + // Different padding char or too long padding. + // Emit current padding and start new one. + this.#emit(pad | (count << 8)); + pad = char; + count = 0; + } + } + + this.#emit(pad | (count << 8)); + } + + flush(): void { + this.#emitPrev(); + } + +} + +const UC_TOKEN_PATTERN = /([\r\n!#$&'()*+,/:;=?@[\]])/; +const UC_FIRST_NON_PAD_PATTERN = /[^ \t]/; +const UC_TRAILING_PADS_PATTERN = /[ \t]+$/; + +/** + * Enables processing of inset encoded with {@link UcChargeLexer URI Charge Notation}. + * + * @param options - Lexer options. + * + * @returns Schema constraints. + */ +export function ucInsetCharge(options?: { + /** + * Whether to decode _plus sign_ (`"+" (U+002B)`) as {@link UC_TOKEN_PREFIX_SPACE space padding}. + * + * @defaultValue `false` + */ + readonly plusAsSpace?: boolean | undefined; +}): UcOmniConstraints; + +export function ucInsetCharge({ + plusAsSpace, +}: { + readonly plusAsSpace?: boolean | undefined; +} = {}): UcOmniConstraints { + return { + deserializer: { + use: 'ucdSupportInset', + from: COMPILER_MODULE, + with: { + lexer: 'UcChargeLexer', + from: CHURI_MODULE, + method: plusAsSpace ? 'plusAsSpace' : undefined, + } satisfies UcdInsetOptions, + }, + }; +} diff --git a/src/syntax/lexers/uc-opaque.lexer.ts b/src/syntax/lexers/uc-opaque.lexer.ts new file mode 100644 index 00000000..6ab2e5ab --- /dev/null +++ b/src/syntax/lexers/uc-opaque.lexer.ts @@ -0,0 +1,14 @@ +import { UcLexer } from '../uc-lexer.js'; + +/** + * Charge input lexer that ignores the input. + */ + +export const ucOpaqueLexer: UcLexer = { + scan(_chunk) { + // Ignore input. + }, + flush() { + // Nothing to flush. + }, +}; diff --git a/src/syntax/lexers/uc-plain-text.lexer.spec.ts b/src/syntax/lexers/uc-plain-text.lexer.spec.ts new file mode 100644 index 00000000..9e816768 --- /dev/null +++ b/src/syntax/lexers/uc-plain-text.lexer.spec.ts @@ -0,0 +1,268 @@ +import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; +import { esline } from 'esgen'; +import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; +import { UC_MODULE_CHURI } from '../../compiler/impl/uc-modules.js'; +import { ucList } from '../../schema/list/uc-list.js'; +import { ucMap } from '../../schema/map/uc-map.js'; +import { UcNumber, ucNumber } from '../../schema/numeric/uc-number.js'; +import { UcString, ucString } from '../../schema/string/uc-string.js'; +import { UcDeserializer } from '../../schema/uc-deserializer.js'; +import { UcErrorInfo } from '../../schema/uc-error.js'; +import { ucUnknown } from '../../schema/unknown/uc-unknown.js'; +import { readTokens } from '../../spec/read-chunks.js'; +import { + UC_TOKEN_APOSTROPHE, + UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_COMMA, + UC_TOKEN_INSET_END, + UC_TOKEN_INSET_URI_PARAM, + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_SPACE, +} from '../uc-token.js'; + +describe('UcPlainTextLexer', () => { + let errors: UcErrorInfo[]; + + beforeEach(() => { + errors = []; + }); + + function onError(error: UcErrorInfo): void { + errors.push(error); + } + + describe('at top level', () => { + let readValue: UcDeserializer.ByTokens; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucUnknown(), + inset({ emit }) { + const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); + + return esline`return new ${UcPlainTextLexer}(${emit});`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('generates string synchronously', () => { + expect(readValue([UC_TOKEN_INSET_URI_PARAM, `'test'`, UC_TOKEN_INSET_END])).toBe(`'test'`); + expect(readValue([UC_TOKEN_INSET_URI_PARAM, `3d`, UC_TOKEN_INSET_END])).toBe(`3d`); + }); + + it('generates string asynchronously', async () => { + await expect( + readValue(readTokens(UC_TOKEN_INSET_URI_PARAM, `'test'`, UC_TOKEN_INSET_END)), + ).resolves.toBe(`'test'`); + await expect( + readValue(readTokens(UC_TOKEN_INSET_URI_PARAM, `3d`, UC_TOKEN_INSET_END)), + ).resolves.toBe(`3d`); + }); + }); + + describe('as list item', () => { + let readList: UcDeserializer.ByTokens; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readList: { + model: ucList(ucString()), + inset({ emit }) { + const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); + + return esline`return new ${UcPlainTextLexer}(${emit});`; + }, + }, + }, + }); + + ({ readList } = await compiler.evaluate()); + }); + + it('generates string item synchronously', () => { + expect( + readList([ + 'start', + UC_TOKEN_COMMA, + UC_TOKEN_INSET_URI_PARAM, + `'te`, + `st'`, + UC_TOKEN_INSET_END, + UC_TOKEN_COMMA, + UC_TOKEN_APOSTROPHE, + 'end', + ]), + ).toEqual(['start', `'test'`, 'end']); + }); + + it('generates string item asynchronously', async () => { + await expect( + readList( + readTokens( + 'start', + UC_TOKEN_COMMA, + UC_TOKEN_INSET_URI_PARAM, + `'te`, + `st'`, + UC_TOKEN_INSET_END, + UC_TOKEN_COMMA, + UC_TOKEN_APOSTROPHE, + 'end', + ), + ), + ).resolves.toEqual(['start', `'test'`, 'end']); + }); + }); + + describe('as map entry', () => { + let readMap: UcDeserializer.ByTokens<{ foo: UcNumber; bar: UcString; baz: UcString }>; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readMap: { + model: ucMap({ + foo: ucNumber(), + bar: ucString(), + baz: ucString(), + }), + inset({ emit }) { + const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); + + return esline`return new ${UcPlainTextLexer}(${emit});`; + }, + }, + }, + }); + + ({ readMap } = await compiler.evaluate()); + }); + + it('generates string item synchronously', () => { + expect( + readMap([ + 'foo', + UC_TOKEN_OPENING_PARENTHESIS, + '13', + UC_TOKEN_CLOSING_PARENTHESIS, + 'bar', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_SPACE | (2 << 8), + UC_TOKEN_INSET_URI_PARAM, + `'te`, + `st'`, + UC_TOKEN_INSET_END, + UC_TOKEN_PREFIX_SPACE | (2 << 8), + '!', + UC_TOKEN_CLOSING_PARENTHESIS, + 'baz', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_APOSTROPHE, + 'end', + UC_TOKEN_CLOSING_PARENTHESIS, + ]), + ).toEqual({ foo: 13, bar: `'test' !`, baz: 'end' }); + }); + + it('generates string item asynchronously', async () => { + await expect( + readMap( + readTokens( + 'foo', + UC_TOKEN_OPENING_PARENTHESIS, + '13', + UC_TOKEN_CLOSING_PARENTHESIS, + 'bar', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_SPACE | (2 << 8), + UC_TOKEN_INSET_URI_PARAM, + `'te`, + `st'`, + UC_TOKEN_INSET_END, + UC_TOKEN_PREFIX_SPACE | (2 << 8), + '!', + UC_TOKEN_CLOSING_PARENTHESIS, + 'baz', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_APOSTROPHE, + 'end', + UC_TOKEN_CLOSING_PARENTHESIS, + ), + ), + ).resolves.toEqual({ foo: 13, bar: `'test' !`, baz: 'end' }); + }); + }); + + describe('in raw string mode', () => { + let readValue: UcDeserializer.ByTokens; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucUnknown(), + inset({ emit }) { + const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); + + return esline`return new ${UcPlainTextLexer}(${emit}, true);`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('generates string synchronously', () => { + expect(readValue([UC_TOKEN_INSET_URI_PARAM, `'test'`, UC_TOKEN_INSET_END])).toBe(`'test'`); + expect(readValue([UC_TOKEN_INSET_URI_PARAM, `3`, UC_TOKEN_INSET_END])).toBe(3); + }); + + it('generates string asynchronously', async () => { + await expect( + readValue(readTokens(UC_TOKEN_INSET_URI_PARAM, `'test'`, UC_TOKEN_INSET_END)), + ).resolves.toBe(`'test'`); + await expect( + readValue(readTokens(UC_TOKEN_INSET_URI_PARAM, `3`, UC_TOKEN_INSET_END)), + ).resolves.toBe(3); + }); + }); + + describe('when turned off', () => { + let readValue: UcDeserializer.ByTokens; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { model: ucString() }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('errors on inset', () => { + expect(readValue([UC_TOKEN_INSET_URI_PARAM, `'test'`, UC_TOKEN_INSET_END], { onError })).toBe( + ``, + ); + + expect(errors).toEqual([ + { + code: 'unexpectedInset', + path: [{}], + details: { + insetId: UC_TOKEN_INSET_URI_PARAM, + }, + message: 'Unrecognized inset', + }, + ]); + }); + }); +}); diff --git a/src/syntax/lexers/uc-plain-text.lexer.ts b/src/syntax/lexers/uc-plain-text.lexer.ts new file mode 100644 index 00000000..fd0b4dbb --- /dev/null +++ b/src/syntax/lexers/uc-plain-text.lexer.ts @@ -0,0 +1,72 @@ +import { UcdInsetOptions } from '../../compiler/deserialization/ucd-support-inset.js'; +import { CHURI_MODULE, COMPILER_MODULE } from '../../impl/module-names.js'; +import { UcOmniConstraints } from '../../schema/uc-constraints.js'; +import { UcLexer } from '../uc-lexer.js'; +import { UC_TOKEN_APOSTROPHE, UcToken } from '../uc-token.js'; + +/** + * Plain text lexer. + * + * The input chunks converted to string tokens directly, without any change. + */ +export class UcPlainTextLexer implements UcLexer { + + readonly #emit: (token: UcToken) => void; + #prefix: boolean; + + /** + * Constructs plain text lexer. + * + * @param emit - Emitter function called each time a token is found. + * @param raw - Whether to emit a raw string rather quoted string. `false` by default. + */ + constructor(emit: (token: UcToken) => void, raw = false) { + this.#emit = emit; + this.#prefix = raw; + } + + scan(chunk: string): void { + if (!this.#prefix) { + this.#emit(UC_TOKEN_APOSTROPHE); + this.#prefix = true; + } + this.#emit(chunk); + } + + flush(): void {} + +} + +/** + * Enables inset processing as {@link UcPlainTextLexer plain text}. + * + * @param options - Lexer options. + * + * @returns Schema constraints. + */ +export function ucInsetPlainText(options?: { + /** + * Whether to emit a raw string rather quoted string. + * + * @defaultValue `false`. + */ + readonly raw?: boolean | undefined; +}): UcOmniConstraints; + +export function ucInsetPlainText({ + raw, +}: { + readonly raw?: boolean | undefined; +} = {}): UcOmniConstraints { + return { + deserializer: { + use: 'ucdSupportInset', + from: COMPILER_MODULE, + with: { + lexer: 'UcPlainTextLexer', + from: CHURI_MODULE, + args: raw ? [`true`] : undefined, + } satisfies UcdInsetOptions, + }, + }; +} diff --git a/src/syntax/lexers/uc-uri-encoded.lexer.spec.ts b/src/syntax/lexers/uc-uri-encoded.lexer.spec.ts new file mode 100644 index 00000000..97929f6c --- /dev/null +++ b/src/syntax/lexers/uc-uri-encoded.lexer.spec.ts @@ -0,0 +1,193 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; +import { esline } from 'esgen'; +import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; +import { UC_MODULE_CHURI } from '../../compiler/impl/uc-modules.js'; +import { ucMap } from '../../schema/map/uc-map.js'; +import { UcDeserializer } from '../../schema/uc-deserializer.js'; +import { ucUnknown } from '../../schema/unknown/uc-unknown.js'; +import { readChunks } from '../../spec/read-chunks.js'; +import { scanUcTokens } from '../scan-uc-tokens.js'; +import { UC_TOKEN_APOSTROPHE, UcToken } from '../uc-token.js'; +import { UcURIEncodedLexer, ucInsetURIEncoded } from './uc-uri-encoded.lexer.js'; +import { UcURIParamsLexer } from './uc-uri-params.lexer.js'; + +describe('UcURIEncodedLexer', () => { + it('decodes percent-encoded entities', () => { + expect(scan('%20')).toEqual([UC_TOKEN_APOSTROPHE, ' ']); + expect(scan('%20a')).toEqual([UC_TOKEN_APOSTROPHE, ' a']); + expect(scan('%20', 'a')).toEqual([UC_TOKEN_APOSTROPHE, ' a']); + expect(scan('%2', '0a')).toEqual([UC_TOKEN_APOSTROPHE, ' a']); + expect(scan('%', '20a')).toEqual([UC_TOKEN_APOSTROPHE, ' a']); + }); + it('decodes percent-encoded multi-char entities', () => { + expect(scan('%e1%9B%A4')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4']); + expect(scan('%e1%9B%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%e1%9B%A4', '!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%e1%9B', '%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%e1%9B%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%e1%', '9B%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%e1%9B%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + expect(scan('%', 'e1%9', 'B%A4!')).toEqual([UC_TOKEN_APOSTROPHE, '\u16e4!']); + }); + it('decodes non-percent-encoded chunk immediately', () => { + expect(scan('%20a', 'bc%20d')).toEqual([UC_TOKEN_APOSTROPHE, ' a', 'bc d']); + }); + it('decodes plus as plus by default', () => { + expect(scan('a+b')).toEqual([UC_TOKEN_APOSTROPHE, 'a+b']); + expect(scan('a+', 'b')).toEqual([UC_TOKEN_APOSTROPHE, 'a+', 'b']); + expect(scan('a', '+b')).toEqual([UC_TOKEN_APOSTROPHE, 'a', '+b']); + }); + it('decodes plus as space when requested', () => { + expect(scanAsParam('a+b')).toEqual([UC_TOKEN_APOSTROPHE, 'a b']); + expect(scanAsParam('a+', 'b')).toEqual([UC_TOKEN_APOSTROPHE, 'a ', 'b']); + expect(scanAsParam('a', '+b')).toEqual([UC_TOKEN_APOSTROPHE, 'a', ' b']); + }); + it('decodes raw text', () => { + expect(scanUcTokens(emit => new UcURIEncodedLexer(emit, true), 'a+b')).toEqual(['a+b']); + expect(scanUcTokens(emit => UcURIEncodedLexer.plusAsSpace(emit, true), 'a+b')).toEqual(['a b']); + }); + + function scan(...input: string[]): UcToken[] { + return scanUcTokens(emit => new UcURIEncodedLexer(emit), ...input); + } + + function scanAsParam(...input: string[]): UcToken[] { + return scanUcTokens(emit => UcURIEncodedLexer.plusAsSpace(emit), ...input); + } +}); + +describe('ucInsetURIEncoded', () => { + describe('in default mode', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ within: { uriParam: ucInsetURIEncoded() } }), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('URI-decodes values', () => { + expect(readValue(`a='te%20st'`)).toEqual({ a: `'te st'` }); + expect(readValue(`a=te+st`)).toEqual({ a: 'te+st' }); + expect(readValue(`a=%33`)).toEqual({ a: '3' }); + }); + }); + + describe('in raw text mode', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ within: { uriParam: ucInsetURIEncoded({ raw: true }) } }), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('URI-decodes values synchronously', () => { + expect(readValue(`a='te%20st'`)).toEqual({ a: `'te st'` }); + expect(readValue(`a='te+st'`)).toEqual({ a: `'te+st'` }); + expect(readValue(`a=%33`)).toEqual({ a: 3 }); + }); + it('URI-decodes values asynchronously', async () => { + await expect(readValue(readChunks(`a='te%20st'`))).resolves.toEqual({ a: `'te st'` }); + await expect(readValue(readChunks(`a='te+st'`))).resolves.toEqual({ a: `'te+st'` }); + await expect(readValue(readChunks(`a=%33`))).resolves.toEqual({ a: 3 }); + }); + }); + + describe('in plus-as-space mode', () => { + let readValue: UcDeserializer.Async; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ within: { uriParam: ucInsetURIEncoded({ plusAsSpace: true }) } }), + }), + mode: 'async', + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('URI-decodes values synchronously', async () => { + await expect(readValue(readChunks(`a='te%20st'`))).resolves.toEqual({ a: `'te st'` }); + await expect(readValue(readChunks(`a='te+st'`))).resolves.toEqual({ a: `'te st'` }); + await expect(readValue(readChunks(`a=%33`))).resolves.toEqual({ a: '3' }); + }); + }); + + describe('in raw plus-as-space mode', () => { + let readValue: UcDeserializer.Sync; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucUnknown({ + within: { uriParam: ucInsetURIEncoded({ plusAsSpace: true, raw: true }) }, + }), + }), + mode: 'sync', + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('URI-decodes values by chunks', () => { + expect(readValue(`a='te%20st'`)).toEqual({ a: `'te st'` }); + expect(readValue(`a='te+st'`)).toEqual({ a: `'te st'` }); + expect(readValue(`a=%33+`)).toEqual({ a: 3 }); + }); + it('URI-decodes values by tokens', () => { + expect(readValue(scan(`a='te%20st'`))).toEqual({ a: `'te st'` }); + expect(readValue(scan(`a='te+st'`))).toEqual({ a: `'te st'` }); + expect(readValue(scan(`a=%33+`))).toEqual({ a: 3 }); + + function scan(...input: string[]): UcToken[] { + return scanUcTokens(emit => new UcURIParamsLexer(emit), ...input); + } + }); + }); +}); diff --git a/src/syntax/lexers/uc-uri-encoded.lexer.ts b/src/syntax/lexers/uc-uri-encoded.lexer.ts new file mode 100644 index 00000000..13e79489 --- /dev/null +++ b/src/syntax/lexers/uc-uri-encoded.lexer.ts @@ -0,0 +1,127 @@ +import { decodeURISearchPart } from 'httongue'; +import { UcdInsetOptions } from '../../compiler/deserialization/ucd-support-inset.js'; +import { CHURI_MODULE, COMPILER_MODULE } from '../../impl/module-names.js'; +import { UcOmniConstraints } from '../../schema/uc-constraints.js'; +import { UcLexer } from '../uc-lexer.js'; +import { UC_TOKEN_APOSTROPHE, UcToken } from '../uc-token.js'; + +/** + * URI-encoded text lexer. + * + * Decodes URI-encoded text and emits it as string {@link UcToken tokens}. + */ +export class UcURIEncodedLexer implements UcLexer { + + /** + * Creates URI-encoded text lexer that decodes _plus sign_ (`"+" (U+002B)`) as {@link UC_TOKEN_PREFIX_SPACE space + * padding}. + * + * This is needed e.g. when tokenizing URI query parameters. + * + * @param emit - Emitter function called each time a token is found. + * @param raw - Whether to emit a raw string rather quoted string. `false` by default. + * + * @returns New URI-encoded text lexer instance. + */ + static plusAsSpace(emit: (token: UcToken) => void, raw?: boolean): UcURIEncodedLexer { + const lexer = new UcURIEncodedLexer(emit, raw); + + lexer.#decode = decodeURISearchPart; + + return lexer; + } + + readonly #emit: (token: UcToken) => void; + #decode: (encoded: string) => string = decodeURIComponent; + #prefix: boolean; + #pending = ''; + + /** + * Constructs URI-encoded text lexer. + * + * @param emit - Emitter function called each time a token is found. + * @param raw - Whether to emit a raw string rather quoted string. `false` by default. + */ + constructor(emit: (token: UcToken) => void, raw = false) { + this.#emit = emit; + this.#prefix = raw; + } + + scan(chunk: string): void { + if (PERCENT_ENCODED_TAIL_PATTERN.test(chunk)) { + this.#pending += chunk; + } else { + const pending = this.#pending; + + if (pending) { + this.#pending = ''; + this.#emitNext(pending + chunk); + } else { + this.#emitNext(chunk); + } + } + } + + flush(): void { + const pending = this.#pending; + + if (pending) { + this.#pending = ''; + this.#emitNext(pending); + } + } + + #emitNext(chunk: string): void { + if (!this.#prefix) { + this.#prefix = true; + this.#emit(UC_TOKEN_APOSTROPHE); + } + this.#emit(this.#decode(chunk)); + } + +} + +const PERCENT_ENCODED_TAIL_PATTERN = /%[\da-fA-F]{0,2}$/; + +/** + * Enables inset processing as {@link UcPlainTextLexer URI-encoded text}. + * + * @param options - Lexer options. + * + * @returns Schema constraints. + */ +export function ucInsetURIEncoded(options?: { + /** + * Whether to decode _plus sign_ (`"+" (U+002B)`) as {@link UC_TOKEN_PREFIX_SPACE space padding}. + * + * @defaultValue `false` + */ + readonly plusAsSpace?: boolean | undefined; + /** + * Whether to emit a raw string rather quoted string. + * + * @defaultValue `false`. + */ + readonly raw?: boolean | undefined; +}): UcOmniConstraints; + +export function ucInsetURIEncoded({ + plusAsSpace, + raw, +}: { + readonly plusAsSpace?: boolean | undefined; + readonly raw?: boolean | undefined; +} = {}): UcOmniConstraints { + return { + deserializer: { + use: 'ucdSupportInset', + from: COMPILER_MODULE, + with: { + lexer: 'UcURIEncodedLexer', + from: CHURI_MODULE, + method: plusAsSpace ? 'plusAsSpace' : undefined, + args: raw ? [`true`] : undefined, + } satisfies UcdInsetOptions, + }, + }; +} diff --git a/src/syntax/lexers/uc-uri-params.lexer.spec.ts b/src/syntax/lexers/uc-uri-params.lexer.spec.ts new file mode 100644 index 00000000..c1567d99 --- /dev/null +++ b/src/syntax/lexers/uc-uri-params.lexer.spec.ts @@ -0,0 +1,267 @@ +import { beforeAll, describe, expect, it } from '@jest/globals'; +import { esline } from 'esgen'; +import { UcdCompiler } from '../../compiler/deserialization/ucd-compiler.js'; +import { UC_MODULE_CHURI } from '../../compiler/impl/uc-modules.js'; +import { ucList } from '../../schema/list/uc-list.js'; +import { ucMap } from '../../schema/map/uc-map.js'; +import { ucString } from '../../schema/string/uc-string.js'; +import { UcDeserializer } from '../../schema/uc-deserializer.js'; +import { ucUnknown } from '../../schema/unknown/uc-unknown.js'; +import { scanUcTokens } from '../scan-uc-tokens.js'; +import { + UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_DOLLAR_SIGN, + UC_TOKEN_INSET_END, + UC_TOKEN_INSET_URI_PARAM, + UC_TOKEN_OPENING_PARENTHESIS, + UcToken, +} from '../uc-token.js'; +import { ucInsetPlainText } from './uc-plain-text.lexer.js'; +import { ucInsetURIEncoded } from './uc-uri-encoded.lexer.js'; +import { UcURIParamsLexer, ucInsetURIParams } from './uc-uri-params.lexer.js'; + +describe('UcURIParamsLexer', () => { + it('recognizes query params', () => { + expect(scan('first=1&second=2')).toEqual([ + 'first', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '1', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + 'second', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '2', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('recognizes matrix params', () => { + expect(scanMatrix('first=1;second=2')).toEqual([ + 'first', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '1', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + 'second', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '2', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('recognizes empty params', () => { + expect(scan('')).toEqual([UC_TOKEN_DOLLAR_SIGN]); + expect(scan('&&')).toEqual([UC_TOKEN_DOLLAR_SIGN]); + }); + it('recognizes URI-encoded key split across chunks', () => { + expect(scan('ab+', 'cd=123')).toEqual([ + 'ab cd', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '123', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + expect(scan('ab%', '20cd=123')).toEqual([ + 'ab cd', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + '123', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('recognizes value split across chunks', () => { + expect(scan('a=', 'cd=1%23', '=456')).toEqual([ + 'a', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + 'cd=1%23', + '=456', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('recognizes parameter without value', () => { + expect(scan('a&b&', '&&c')).toEqual([ + 'a', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_CLOSING_PARENTHESIS, + 'b', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_CLOSING_PARENTHESIS, + 'c', + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('recognizes empty parameter name', () => { + expect(scan('=a', '&=', 'b')).toEqual([ + UC_TOKEN_DOLLAR_SIGN, + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + 'a', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_DOLLAR_SIGN, + UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_INSET_URI_PARAM, + 'b', + UC_TOKEN_INSET_END, + UC_TOKEN_CLOSING_PARENTHESIS, + ]); + }); + it('permits custom inset', async () => { + const compiler = new UcdCompiler({ + models: { + readParams: { + model: ucMap({ + foo: ucString({ + within: { + uriParam: ucInsetPlainText(), + }, + }), + bar: ucList(ucString()), + }), + mode: 'sync', + inset({ emit }) { + const UcChargeLexer = UC_MODULE_CHURI.import('UcChargeLexer'); + + return esline`return ${UcChargeLexer}.plusAsSpace(${emit});`; + }, + }, + }, + presentations: ['uriParam', 'charge'], + }); + + const { readParams } = await compiler.evaluate(); + + expect( + readParams(scanUcTokens(emit => new UcURIParamsLexer(emit), 'foo=1,2,3&bar=4,+5+,6+')), + ).toEqual({ + foo: '1,2,3', + bar: ['4', '5', '6'], + }); + }); + it('uses default custom inset', async () => { + const compiler = new UcdCompiler({ + models: { + readParams: { + model: ucMap({ + foo: ucString({ + where: ucInsetPlainText(), + within: { + charge: ucInsetPlainText({ raw: true }), + }, + }), + bar: ucList(ucString()), + }), + mode: 'sync', + inset({ emit }) { + const UcChargeLexer = UC_MODULE_CHURI.import('UcChargeLexer'); + + return esline`return ${UcChargeLexer}.plusAsSpace(${emit});`; + }, + }, + }, + presentations: ['uriParam', 'charge'], + }); + + const { readParams } = await compiler.evaluate(); + + expect( + readParams(scanUcTokens(emit => new UcURIParamsLexer(emit), 'foo=1,2,3&bar=4,+5+,6+')), + ).toEqual({ + foo: '1,2,3', + bar: ['4', '5', '6'], + }); + }); + + function scan(...input: string[]): UcToken[] { + return scanUcTokens(emit => new UcURIParamsLexer(emit), ...input); + } + + function scanMatrix(...input: string[]): UcToken[] { + return scanUcTokens(emit => new UcURIParamsLexer(emit, ';'), ...input); + } +}); + +describe('ucInsetURIParams', () => { + describe('with default splitter', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucMap( + { + b: ucUnknown({ within: { uriParam: ucInsetURIEncoded() } }), + c: ucUnknown({ within: { uriParam: ucInsetURIEncoded() } }), + }, + { + within: { uriParam: ucInsetURIParams() }, + }, + ), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit}, ';')`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('recognizes params', () => { + expect(readValue(`a=b='te%20st'&c=2`)).toEqual({ a: { b: `'te st'`, c: '2' } }); + expect(readValue(`a=b=te+st&c=2`)).toEqual({ a: { b: 'te+st', c: '2' } }); + expect(readValue(`a=b=%33&c=2`)).toEqual({ a: { b: '3', c: '2' } }); + }); + }); + + describe('with matrix splitter', () => { + let readValue: UcDeserializer; + + beforeAll(async () => { + const compiler = new UcdCompiler({ + models: { + readValue: { + model: ucMap({ + a: ucMap( + { + b: ucUnknown({ within: { uriParam: ucInsetURIEncoded() } }), + c: ucUnknown({ within: { uriParam: ucInsetURIEncoded() } }), + }, + { + within: { uriParam: ucInsetURIParams(';') }, + }, + ), + }), + lexer: ({ emit }) => { + const Lexer = UC_MODULE_CHURI.import(UcURIParamsLexer.name); + + return esline`return new ${Lexer}(${emit})`; + }, + }, + }, + }); + + ({ readValue } = await compiler.evaluate()); + }); + + it('recognizes params', () => { + expect(readValue(`a=b='te%20st';c=2`)).toEqual({ a: { b: `'te st'`, c: '2' } }); + expect(readValue(`a=b=te+st;c=2`)).toEqual({ a: { b: 'te+st', c: '2' } }); + expect(readValue(`a=b=%33;c=2`)).toEqual({ a: { b: '3', c: '2' } }); + }); + }); +}); diff --git a/src/syntax/lexers/uc-uri-params.lexer.ts b/src/syntax/lexers/uc-uri-params.lexer.ts new file mode 100644 index 00000000..64bfc41a --- /dev/null +++ b/src/syntax/lexers/uc-uri-params.lexer.ts @@ -0,0 +1,171 @@ +import { esStringLiteral } from 'esgen'; +import { decodeURISearchPart } from 'httongue'; +import { UcdInsetOptions } from '../../compiler/deserialization/ucd-support-inset.js'; +import { CHURI_MODULE, COMPILER_MODULE } from '../../impl/module-names.js'; +import { UcOmniConstraints } from '../../schema/uc-constraints.js'; +import { UcLexer } from '../uc-lexer.js'; +import { + UC_TOKEN_CLOSING_PARENTHESIS, + UC_TOKEN_DOLLAR_SIGN, + UC_TOKEN_INSET_END, + UC_TOKEN_INSET_URI_PARAM, + UC_TOKEN_OPENING_PARENTHESIS, + UcToken, +} from '../uc-token.js'; + +/** + * URI parameters lexer. + * + * Can be used to tokenize e.g.: + * + * - {@link ChURIQuery URI query}, + * - {@link ChURIMatrix URI matrix parameters}, + * - {@link ChURIAnchor URI hash parameters}, + * - `application/x-ww-form-urlencoded` message body. + * + * Parameter values emitted as inset starting with {@link UC_TOKEN_INSET_URI_PARAM} token. + */ +export class UcURIParamsLexer implements UcLexer { + + readonly #emit: (token: UcToken) => void; + readonly #splitter: string; + readonly #delimiter: RegExp; + #emitted = false; + #key = ''; + #value = false; + + /** + * Constructs URI parameters lexer. + * + * @param emit - Emitter function called each time a token is found. + * @param splitter - Parameters splitter character. + * + * Either `'&'` (by default), or `';'`. + */ + constructor(emit: (token: UcToken) => void, splitter: '&' | ';' = '&') { + this.#emit = token => { + emit(token); + this.#emitted = true; + }; + this.#splitter = splitter; + this.#delimiter = CHURI_DELIMITER_PATTERNS[splitter]; + } + + scan(chunk: string): void { + while (chunk) { + if (this.#value) { + const valueEnd = chunk.indexOf(this.#splitter); + + if (valueEnd < 0) { + this.#emit(chunk); + + break; + } + if (valueEnd) { + this.#emit(chunk.slice(0, valueEnd)); + } + this.#emit(UC_TOKEN_INSET_END); // End of value. + this.#emit(UC_TOKEN_CLOSING_PARENTHESIS); + + this.#value = false; + chunk = chunk.slice(valueEnd + 1); + } else { + const match = this.#delimiter.exec(chunk); + + if (!match) { + this.#key += chunk; + + break; + } + + const { index } = match; + const [found] = match; + + if (found === '=') { + // Value following the key. + this.#key += chunk.slice(0, index); + if (this.#key) { + this.#emitKey(); + } else { + // Empty key. + this.#emit(UC_TOKEN_DOLLAR_SIGN); + } + + // Start the value. + this.#value = true; + this.#emit(UC_TOKEN_OPENING_PARENTHESIS); + this.#emit(UC_TOKEN_INSET_URI_PARAM); + chunk = chunk.slice(index + 1); + } else { + // Entry without value. + if (index) { + this.#key += chunk.slice(0, index); + } + if (this.#key) { + this.#emitEmptyEntry(); + } + + chunk = chunk.slice(index + found.length); + } + } + } + } + + flush(): void { + if (this.#value) { + this.#value = false; + this.#emit(UC_TOKEN_INSET_END); + this.#emit(UC_TOKEN_CLOSING_PARENTHESIS); + } else if (this.#key) { + this.#emitEmptyEntry(); + } else if (!this.#emitted) { + // Emit empty map. + this.#emit(UC_TOKEN_DOLLAR_SIGN); + } + } + + #emitKey(): void { + const key = this.#key; + + this.#key = ''; + + this.#emit(decodeURISearchPart(key)); + } + + #emitEmptyEntry(): void { + this.#emitKey(); + this.#emit(UC_TOKEN_OPENING_PARENTHESIS); + this.#emit(UC_TOKEN_CLOSING_PARENTHESIS); + } + +} + +const CHURI_DELIMITER_PATTERNS = { + '&': /=|&+/, + ';': /=|;+/, +}; + +/** + * Enables inset processing as {@link UcURIParamsLexer URI params}. + * + * E.g. for `application/x-www-form-urlencoded` processing. + * + * @param splitter - Parameters splitter character. + * + * Either `'&'` (by default), or `';'`. + * + * @returns Schema constraints. + */ +export function ucInsetURIParams(splitter?: '&' | ';'): UcOmniConstraints { + return { + deserializer: { + use: 'ucdSupportInset', + from: COMPILER_MODULE, + with: { + lexer: 'UcURIParamsLexer', + from: CHURI_MODULE, + args: splitter ? [esStringLiteral(splitter)] : undefined, + } satisfies UcdInsetOptions, + }, + }; +} diff --git a/src/syntax/mod.ts b/src/syntax/mod.ts index 562035ea..9b036181 100644 --- a/src/syntax/mod.ts +++ b/src/syntax/mod.ts @@ -1,8 +1,8 @@ +export * from './lexers/mod.js'; export * from './print-uc-token.js'; +export * from './scan-uc-tokens.js'; export * from './trim-uc-tokens-tail.js'; -export * from './uc-input-lexer.js'; export * from './uc-lexer-stream.js'; export * from './uc-lexer.js'; -export * from './uc-plain-text-lexer.js'; export * from './uc-token-kind.js'; export * from './uc-token.js'; diff --git a/src/syntax/print-uc-token.spec.ts b/src/syntax/print-uc-token.spec.ts index df2904ea..198bc05a 100644 --- a/src/syntax/print-uc-token.spec.ts +++ b/src/syntax/print-uc-token.spec.ts @@ -4,7 +4,8 @@ import { UC_TOKEN_CLOSING_PARENTHESIS, UC_TOKEN_CR, UC_TOKEN_CRLF, - UC_TOKEN_INSET, + UC_TOKEN_INSET_END, + UC_TOKEN_INSET_URI_PARAM, UC_TOKEN_LF, UC_TOKEN_OPENING_PARENTHESIS, UC_TOKEN_PREFIX_SPACE, @@ -33,9 +34,9 @@ describe('printUcToken', () => { expect( printUcTokens([ UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_INSET, + UC_TOKEN_INSET_URI_PARAM, 'test', - UC_TOKEN_INSET, + UC_TOKEN_INSET_END, UC_TOKEN_CLOSING_PARENTHESIS, ]), ).toBe('(test)'); diff --git a/src/syntax/print-uc-token.ts b/src/syntax/print-uc-token.ts index 612e9fee..3ac4d471 100644 --- a/src/syntax/print-uc-token.ts +++ b/src/syntax/print-uc-token.ts @@ -2,7 +2,7 @@ import { asis } from '@proc7ts/primitives'; import { UC_TOKEN_CR, UC_TOKEN_CRLF, - UC_TOKEN_INSET, + UC_TOKEN_PREFIX_INSET, UC_TOKEN_PREFIX_SPACE, UC_TOKEN_PREFIX_TAB, UcToken, @@ -27,7 +27,7 @@ export function printUcToken(token: UcToken, encodeString?: (token: string) => s return '\t'.repeat((token >>> 8) + 1); case UC_TOKEN_CR: return token === UC_TOKEN_CRLF ? '\r\n' : '\r'; - case UC_TOKEN_INSET: + case UC_TOKEN_PREFIX_INSET: return ''; } diff --git a/src/syntax/scan-uc-tokens.ts b/src/syntax/scan-uc-tokens.ts new file mode 100644 index 00000000..131bd696 --- /dev/null +++ b/src/syntax/scan-uc-tokens.ts @@ -0,0 +1,30 @@ +import { UcLexer } from './uc-lexer.js'; +import { UcToken } from './uc-token.js'; + +/** + * Scans the `input` for URI charge {@link UcToken tokens}. + * + * @param createLexer - Creates lexer. + * @param input - Array of input chunks to scan. + * + * @returns Array of tokens. + */ +export function scanUcTokens( + createLexer: ( + /** + * Emitter function called each time a is token found. + */ + emit: (token: UcToken) => void, + ) => UcLexer, + ...input: string[] +): UcToken[] { + const tokens: UcToken[] = []; + const lexer = createLexer(token => tokens.push(token)); + + for (const chunk of input) { + lexer.scan(chunk); + } + lexer.flush(); + + return tokens; +} diff --git a/src/syntax/uc-input-lexer.ts b/src/syntax/uc-input-lexer.ts deleted file mode 100644 index 3ceb19b7..00000000 --- a/src/syntax/uc-input-lexer.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * Charge lexer that splits the input onto tokens. - * - * The input chunks {@link UcInputLexer#scan scanned} by lexer one at a time. Each token found is emitted by calling - * provided emitter. On completion, the input has to by {@link UcInputLexer#flush flushed} in order to process the - * remaining input. - */ -export interface UcInputLexer { - /** - * Scans the input `chunk` for tokens. - * - * @param chunk - Chunk of input to scan. - */ - scan(chunk: string): void; - - /** - * Flushes the input emitting all pending tokens. - */ - flush(): void; -} - -/** - * Charge input lexer that ignores the input. - */ -export const ucOpaqueLexer: UcInputLexer = { - scan(_chunk) { - // Ignore input. - }, - flush() { - // Nothing to flush. - }, -}; diff --git a/src/syntax/uc-lexer-stream.ts b/src/syntax/uc-lexer-stream.ts index f62c9657..26eb1592 100644 --- a/src/syntax/uc-lexer-stream.ts +++ b/src/syntax/uc-lexer-stream.ts @@ -1,29 +1,27 @@ -import { UcInputLexer } from './uc-input-lexer.js'; +import { UcChargeLexer } from './lexers/uc-charge.lexer.js'; import { UcLexer } from './uc-lexer.js'; import { UcToken } from './uc-token.js'; /** * A stream that transforms input chunks to URI charge {@link UcToken tokens}. * - * Utilizes URI charge {@link UcLexer lexer} internally. + * Utilizes URI charge {@link UcChargeLexer lexer} internally. */ export class UcLexerStream extends TransformStream { /** * Constructs lexer stream. * - * @param createLexer - Creates an input lexer to use. By default, creates {@link UcLexer} instance. + * @param createLexer - Creates an input lexer to use. By default, creates {@link UcChargeLexer} instance. * @param writableStrategy - An object that optionally defines a queuing strategy for the input (chunks) stream. * @param readableStrategy - An object that optionally defines a queuing strategy for the output (tokens) stream. */ constructor( - createLexer: ( - emit: (token: UcToken) => void, - ) => UcInputLexer = UcLexerStream$createDefaultLexer, + createLexer: (emit: (token: UcToken) => void) => UcLexer = UcLexerStream$createDefaultLexer, writableStrategy?: QueuingStrategy, readableStrategy?: QueuingStrategy, ) { - let lexer: UcInputLexer; + let lexer: UcLexer; super( { @@ -40,6 +38,6 @@ export class UcLexerStream extends TransformStream { } -function UcLexerStream$createDefaultLexer(emit: (token: UcToken) => void): UcInputLexer { - return new UcLexer(emit); +function UcLexerStream$createDefaultLexer(emit: (token: UcToken) => void): UcLexer { + return new UcChargeLexer(emit); } diff --git a/src/syntax/uc-lexer.ts b/src/syntax/uc-lexer.ts index b2123b83..7077703f 100644 --- a/src/syntax/uc-lexer.ts +++ b/src/syntax/uc-lexer.ts @@ -1,216 +1,20 @@ -import { UcInputLexer } from './uc-input-lexer.js'; -import { - UC_TOKEN_AMPERSAND, - UC_TOKEN_APOSTROPHE, - UC_TOKEN_ASTERISK, - UC_TOKEN_AT_SIGN, - UC_TOKEN_CLOSING_BRACKET, - UC_TOKEN_CLOSING_PARENTHESIS, - UC_TOKEN_COLON, - UC_TOKEN_COMMA, - UC_TOKEN_CR, - UC_TOKEN_CRLF, - UC_TOKEN_DOLLAR_SIGN, - UC_TOKEN_EQUALS_SIGN, - UC_TOKEN_EXCLAMATION_MARK, - UC_TOKEN_HASH, - UC_TOKEN_LF, - UC_TOKEN_OPENING_BRACKET, - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_PLUS_SIGN, - UC_TOKEN_QUESTION_MARK, - UC_TOKEN_SEMICOLON, - UC_TOKEN_SLASH, - UcToken, -} from './uc-token.js'; - /** - * URI charge lexer that splits input string(s) onto tokens. + * Lexer splits the input onto tokens. * * The input chunks {@link UcLexer#scan scanned} by lexer one at a time. Each token found is emitted by calling - * the given emitter function. On completion, the input has to by {@link UcLexer#flush flushed} in order to process - * the remaining input. + * provided emitter. On completion, the input has to by {@link UcLexer#flush flushed} in order to process the + * remaining input. */ -export class UcLexer implements UcInputLexer { - +export interface UcLexer { /** - * Scans the `input` string for URI charge {@link UcToken tokens}. - * - * @param input - String to scan. + * Scans the input `chunk` for tokens. * - * @returns Array of tokens. + * @param chunk - Chunk of input to scan. */ - static scan(input: string): UcToken[] { - const tokens: UcToken[] = []; - const tokenizer = new UcLexer(token => tokens.push(token)); - - tokenizer.scan(input); - tokenizer.flush(); - - return tokens; - } - - static readonly #tokens: { [token: string]: (tokenizer: UcLexer) => void } = { - '\r': tokenizer => tokenizer.#addCR(), - '\n': tokenizer => tokenizer.#emitLF(), - '(': tokenizer => tokenizer.#emitReserved(UC_TOKEN_OPENING_PARENTHESIS), - ')': tokenizer => tokenizer.#emitReserved(UC_TOKEN_CLOSING_PARENTHESIS), - ',': tokenizer => tokenizer.#emitReserved(UC_TOKEN_COMMA), - '!': tokenizer => tokenizer.#emitReserved(UC_TOKEN_EXCLAMATION_MARK), - '#': tokenizer => tokenizer.#emitReserved(UC_TOKEN_HASH), - $: tokenizer => tokenizer.#emitReserved(UC_TOKEN_DOLLAR_SIGN), - '&': tokenizer => tokenizer.#emitReserved(UC_TOKEN_AMPERSAND), - "'": tokenizer => tokenizer.#emitReserved(UC_TOKEN_APOSTROPHE), - '*': tokenizer => tokenizer.#emitReserved(UC_TOKEN_ASTERISK), - '+': tokenizer => tokenizer.#emitReserved(UC_TOKEN_PLUS_SIGN), - '/': tokenizer => tokenizer.#emitReserved(UC_TOKEN_SLASH), - ':': tokenizer => tokenizer.#emitReserved(UC_TOKEN_COLON), - ';': tokenizer => tokenizer.#emitReserved(UC_TOKEN_SEMICOLON), - '=': tokenizer => tokenizer.#emitReserved(UC_TOKEN_EQUALS_SIGN), - '?': tokenizer => tokenizer.#emitReserved(UC_TOKEN_QUESTION_MARK), - '@': tokenizer => tokenizer.#emitReserved(UC_TOKEN_AT_SIGN), - '[': tokenizer => tokenizer.#emitReserved(UC_TOKEN_OPENING_BRACKET), - ']': tokenizer => tokenizer.#emitReserved(UC_TOKEN_CLOSING_BRACKET), - }; - - readonly #emit: (token: UcToken) => void; - #prev: string | typeof UC_TOKEN_CR | 0 = 0; + scan(chunk: string): void; /** - * Constructs URI charge tokenizer. - * - * @param emit - Emitter function called each time a token is found. + * Flushes the input emitting all pending tokens. */ - constructor(emit: (token: UcToken) => void) { - this.#emit = emit; - } - - scan(chunk: string): void { - for (const token of chunk.split(UC_TOKEN_PATTERN)) { - this.#add(token); - } - } - - #add(token: string): void { - if (token.length === 1) { - const emitter = UcLexer.#tokens[token]; - - if (emitter) { - return emitter(this); - } - } - - this.#addString(token); - } - - #emitPrev(): void { - const prev = this.#prev; - - if (!prev) { - return; - } - this.#prev = 0; - - if (typeof prev === 'number') { - this.#emit(prev); - - return; - } - - const padStart = prev.search(UC_TRAILING_PADS_PATTERN); - - if (padStart < 0) { - return this.#emit(decodeURIComponent(prev)); - } - - if (padStart) { - // Emit non-empty token only. - this.#emit(decodeURIComponent(prev.slice(0, padStart))); - } - - this.#emitPads(prev, padStart, prev.length); - } - - #emitLF(): void { - if (this.#prev === UC_TOKEN_CR) { - this.#emit(UC_TOKEN_CRLF); - this.#prev = 0; - } else { - this.#emitPrev(); - this.#emit(UC_TOKEN_LF); - } - } - - #addCR(): void { - this.#emitPrev(); - this.#prev = UC_TOKEN_CR; - } - - #emitReserved(token: number): void { - this.#emitPrev(); - this.#emit(token); - } - - #addString(token: string): void { - if (!token) { - return; - } - - const prev = this.#prev; - - if (prev && typeof prev === 'number') { - this.#emit(prev); - } - - if (typeof prev === 'string') { - this.#prev += token; - } else { - const padEnd = token.search(UC_FIRST_NON_PAD_PATTERN); - - if (padEnd < 0) { - // Only pads found. - this.#emitPads(token, 0, token.length); - - return; - } - - if (padEnd) { - this.#emitPads(token, 0, padEnd); - this.#prev = token.slice(padEnd); - } else { - this.#prev = token; - } - } - } - - #emitPads(token: string, padStart: number, padEnd: number): void { - let pad = token.charCodeAt(padStart); - let count = 0; // One less than actual padding length. - - for (let i = padStart + 1; i < padEnd; ++i) { - const char = token.charCodeAt(i); - - if (char === pad && count < 255 /* prevent padding longer than 256 chars */) { - // Same padding. - ++count; - } else { - // Different padding char or too long padding. - // Emit current padding and start new one. - this.#emit(pad | (count << 8)); - pad = char; - count = 0; - } - } - - this.#emit(pad | (count << 8)); - } - - flush(): void { - this.#emitPrev(); - } - + flush(): void; } - -const UC_TOKEN_PATTERN = /([\r\n!#$&'()*+,/:;=?@[\]])/; -const UC_FIRST_NON_PAD_PATTERN = /[^ \t]/; -const UC_TRAILING_PADS_PATTERN = /[ \t]+$/; diff --git a/src/syntax/uc-plain-text-lexer.spec.ts b/src/syntax/uc-plain-text-lexer.spec.ts deleted file mode 100644 index f0eb6eb3..00000000 --- a/src/syntax/uc-plain-text-lexer.spec.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { beforeAll, beforeEach, describe, expect, it } from '@jest/globals'; -import { esline } from 'esgen'; -import { Readable } from 'node:stream'; -import { UcdCompiler } from '../compiler/deserialization/ucd-compiler.js'; -import { UC_MODULE_CHURI } from '../compiler/impl/uc-modules.js'; -import { ucList } from '../schema/list/uc-list.js'; -import { ucMap } from '../schema/map/uc-map.js'; -import { UcNumber, ucNumber } from '../schema/numeric/uc-number.js'; -import { UcString, ucString } from '../schema/string/uc-string.js'; -import { UcDeserializer } from '../schema/uc-deserializer.js'; -import { UcErrorInfo } from '../schema/uc-error.js'; -import { ucUnknown } from '../schema/unknown/uc-unknown.js'; -import { - UC_TOKEN_APOSTROPHE, - UC_TOKEN_CLOSING_PARENTHESIS, - UC_TOKEN_COMMA, - UC_TOKEN_INSET, - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_PREFIX_SPACE, - UcToken, -} from './uc-token.js'; - -describe('UcPlainTextLexer', () => { - let errors: UcErrorInfo[]; - - beforeEach(() => { - errors = []; - }); - - function onError(error: UcErrorInfo): void { - errors.push(error); - } - - describe('at top level', () => { - let readValue: UcDeserializer; - - beforeAll(async () => { - const compiler = new UcdCompiler({ - models: { - readValue: ucUnknown(), - }, - inset: code => { - const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); - - code.line(esline`emit => new ${UcPlainTextLexer}(emit)`); - }, - }); - - ({ readValue } = await compiler.evaluate()); - }); - - it('generates string synchronously', () => { - expect(readValue([UC_TOKEN_INSET, `'test'`, UC_TOKEN_INSET])).toBe(`'test'`); - expect(readValue([UC_TOKEN_INSET, `3d`, UC_TOKEN_INSET])).toBe(`3d`); - }); - - it('generates string asynchronously', async () => { - await expect(readValue(readTokens(UC_TOKEN_INSET, `'test'`, UC_TOKEN_INSET))).resolves.toBe( - `'test'`, - ); - await expect(readValue(readTokens(UC_TOKEN_INSET, `3d`, UC_TOKEN_INSET))).resolves.toBe(`3d`); - }); - }); - - describe('as list item', () => { - let readList: UcDeserializer; - - beforeAll(async () => { - const compiler = new UcdCompiler({ - models: { - readList: ucList(ucString()), - }, - inset: code => { - const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); - - code.line(esline`emit => new ${UcPlainTextLexer}(emit)`); - }, - }); - - ({ readList } = await compiler.evaluate()); - }); - - it('generates string item synchronously', () => { - expect( - readList([ - 'start', - UC_TOKEN_COMMA, - UC_TOKEN_INSET, - `'te`, - `st'`, - UC_TOKEN_INSET, - UC_TOKEN_COMMA, - UC_TOKEN_APOSTROPHE, - 'end', - ]), - ).toEqual(['start', `'test'`, 'end']); - }); - - it('generates string item asynchronously', async () => { - await expect( - readList( - readTokens( - 'start', - UC_TOKEN_COMMA, - UC_TOKEN_INSET, - `'te`, - `st'`, - UC_TOKEN_INSET, - UC_TOKEN_COMMA, - UC_TOKEN_APOSTROPHE, - 'end', - ), - ), - ).resolves.toEqual(['start', `'test'`, 'end']); - }); - }); - - describe('as map entry', () => { - let readMap: UcDeserializer<{ foo: UcNumber; bar: UcString; baz: UcString }>; - - beforeAll(async () => { - const compiler = new UcdCompiler({ - models: { - readMap: ucMap({ foo: ucNumber(), bar: ucString(), baz: ucString() }), - }, - inset: code => { - const UcPlainTextLexer = UC_MODULE_CHURI.import('UcPlainTextLexer'); - - code.line(esline`emit => new ${UcPlainTextLexer}(emit)`); - }, - }); - - ({ readMap } = await compiler.evaluate()); - }); - - it('generates string item synchronously', () => { - expect( - readMap([ - 'foo', - UC_TOKEN_OPENING_PARENTHESIS, - '13', - UC_TOKEN_CLOSING_PARENTHESIS, - 'bar', - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_PREFIX_SPACE | (2 << 8), - UC_TOKEN_INSET, - `'te`, - `st'`, - UC_TOKEN_INSET, - UC_TOKEN_PREFIX_SPACE | (2 << 8), - '!', - UC_TOKEN_CLOSING_PARENTHESIS, - 'baz', - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_APOSTROPHE, - 'end', - UC_TOKEN_CLOSING_PARENTHESIS, - ]), - ).toEqual({ foo: 13, bar: `'test' !`, baz: 'end' }); - }); - - it('generates string item asynchronously', async () => { - await expect( - readMap( - readTokens( - 'foo', - UC_TOKEN_OPENING_PARENTHESIS, - '13', - UC_TOKEN_CLOSING_PARENTHESIS, - 'bar', - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_PREFIX_SPACE | (2 << 8), - UC_TOKEN_INSET, - `'te`, - `st'`, - UC_TOKEN_INSET, - UC_TOKEN_PREFIX_SPACE | (2 << 8), - '!', - UC_TOKEN_CLOSING_PARENTHESIS, - 'baz', - UC_TOKEN_OPENING_PARENTHESIS, - UC_TOKEN_APOSTROPHE, - 'end', - UC_TOKEN_CLOSING_PARENTHESIS, - ), - ), - ).resolves.toEqual({ foo: 13, bar: `'test' !`, baz: 'end' }); - }); - }); - - describe('when turned off', () => { - let readValue: UcDeserializer; - - beforeAll(async () => { - const compiler = new UcdCompiler({ - models: { - readValue: ucString(), - }, - }); - - ({ readValue } = await compiler.evaluate()); - }); - - it('errors on inset', () => { - expect(readValue([UC_TOKEN_INSET, `'test'`, UC_TOKEN_INSET], { onError })).toBe(``); - - expect(errors).toEqual([ - { - code: 'unexpectedInset', - path: [{}], - message: 'Unrecognized inset', - }, - ]); - }); - }); - - function readTokens(...tokens: UcToken[]): ReadableStream { - return Readable.toWeb(Readable.from(tokens)) as ReadableStream; - } -}); diff --git a/src/syntax/uc-plain-text-lexer.ts b/src/syntax/uc-plain-text-lexer.ts deleted file mode 100644 index 533b3ec4..00000000 --- a/src/syntax/uc-plain-text-lexer.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { UcInputLexer } from './uc-input-lexer.js'; -import { UC_TOKEN_APOSTROPHE, UcToken } from './uc-token.js'; - -/** - * Plain text lexer. - * - * The input chunks converted to string tokens directly, without any change. - */ -export class UcPlainTextLexer implements UcInputLexer { - - readonly #emit: (token: UcToken) => void; - #prefix = false; - - constructor(emit: (token: UcToken) => void) { - this.#emit = emit; - } - - scan(chunk: string): void { - if (!this.#prefix) { - this.#emit(UC_TOKEN_APOSTROPHE); - this.#prefix = true; - } - this.#emit(chunk); - } - - flush(): void {} - -} diff --git a/src/syntax/uc-token-kind.spec.ts b/src/syntax/uc-token-kind.spec.ts index 59c604f6..c570147b 100644 --- a/src/syntax/uc-token-kind.spec.ts +++ b/src/syntax/uc-token-kind.spec.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from '@jest/globals'; import { UC_TOKEN_KIND_CONTROL, ucTokenKind } from './uc-token-kind.js'; -import { UC_TOKEN_INSET } from './uc-token.js'; +import { UC_TOKEN_INSET_URI_PARAM } from './uc-token.js'; describe('ucTokenKind', () => { it('is control for inset bound', () => { - expect(ucTokenKind(UC_TOKEN_INSET)).toBe(UC_TOKEN_KIND_CONTROL); + expect(ucTokenKind(UC_TOKEN_INSET_URI_PARAM)).toBe(UC_TOKEN_KIND_CONTROL); }); }); diff --git a/src/syntax/uc-token-kind.ts b/src/syntax/uc-token-kind.ts index 99b0c57b..7c5fa94e 100644 --- a/src/syntax/uc-token-kind.ts +++ b/src/syntax/uc-token-kind.ts @@ -2,9 +2,9 @@ import { UC_TOKEN_CLOSING_PARENTHESIS, UC_TOKEN_COMMA, UC_TOKEN_CR, - UC_TOKEN_INSET, UC_TOKEN_LF, UC_TOKEN_OPENING_PARENTHESIS, + UC_TOKEN_PREFIX_INSET, UC_TOKEN_PREFIX_SPACE, UC_TOKEN_PREFIX_TAB, UcToken, @@ -47,7 +47,7 @@ export function ucTokenKind(token: UcToken): UcTokenKind { case UC_TOKEN_OPENING_PARENTHESIS: case UC_TOKEN_CLOSING_PARENTHESIS: return UC_TOKEN_KIND_BOUND; - case UC_TOKEN_INSET: + case UC_TOKEN_PREFIX_INSET: return UC_TOKEN_KIND_CONTROL; default: return UC_TOKEN_KIND_DELIMITER; diff --git a/src/syntax/uc-token.ts b/src/syntax/uc-token.ts index 45b7e82c..c232b559 100644 --- a/src/syntax/uc-token.ts +++ b/src/syntax/uc-token.ts @@ -1,12 +1,3 @@ -/** - * _Inset_ bounds enclose the tokens that considered as input chunks rather normal tokens. - * The _inset_ supposed to be processed by appropriate {@link UcInputLexer input lexer}. - * The bounds themselves are control tokens to be ignored. - * - * _Inset_ expected at value position and nowhere else. E.g. it can't be inserted into the middle of a string. - */ -export const UC_TOKEN_INSET = 0x1f; - // Line terminators. export const UC_TOKEN_LF = 0x0a as const; export const UC_TOKEN_CR = 0x0d as const; @@ -17,6 +8,19 @@ export const UC_TOKEN_CRLF = 0x0a0d as const; // Windows-style export const UC_TOKEN_PREFIX_TAB = 0x09 as const; export const UC_TOKEN_PREFIX_SPACE = 0x20 as const; +/** + * _Inset_ starts with number token which lowest byte equals to this prefix. Tokens after this token considered + * as _inset_ input chunks rather normal tokens. The _inset_ supposed to be processed by appropriate {@link UcLexer + * lexer}. The token itself is used as _inset_ format identifier. The _inset_ input ends with {@link UC_TOKEN_INSET_END} + * token. The _inset_ bounds themselves are control tokens to be ignored. + * + * _Inset_ expected at value position and nowhere else. E.g. it can't be inserted into the middle of a string. + */ +export const UC_TOKEN_PREFIX_INSET = 0x1f; + +export const UC_TOKEN_INSET_URI_PARAM = 0x011f as const; +export const UC_TOKEN_INSET_END = UC_TOKEN_PREFIX_INSET; + // [Reserved characters](https://www.rfc-editor.org/rfc/rfc3986#section-2.2). export const UC_TOKEN_EXCLAMATION_MARK = 0x21 as const; export const UC_TOKEN_HASH = 0x23 as const; @@ -62,8 +66,9 @@ export const UC_TOKEN_CLOSING_BRACKET = 0x5d as const; * Such padding always emitted for spaces and tabs around [reserved characters], line terminators, after input * beginning, and before input end. Spaces and tabs e.g. between words may be emitted as part of string tokens. * - * - Number corresponding to {@link UC_TOKEN_INSET inset bound}. The tokens between inset bounds considered as - * input chunks to be processed by appropriate {@link UcInputLexer input lexer}. + * - Contains {@link UC_TOKEN_PREFIX_INSET} as the lowest byte. The tokens after this token considered an _inset_ + * input chunks to be processed by appropriate {@link UcLexer lexer}. The token itself is used as _inset_ format + * identifier. The _inset_ input ends with {@link UC_TOKEN_INSET_END} token. * * [percent-decoded]: https://www.rfc-editor.org/rfc/rfc3986#section-2.1 * [reserved characters]: https://www.rfc-editor.org/rfc/rfc3986#section-2.2 diff --git a/src/uri/churi-params.ts b/src/uri/churi-params.ts index 5fb8fb91..777d790c 100644 --- a/src/uri/churi-params.ts +++ b/src/uri/churi-params.ts @@ -1,6 +1,7 @@ import { parseURICharge } from '#churi/uri-charge/deserializer.js'; import { URICharge$List } from '../schema/uri-charge/impl/uri-charge.some.js'; import { URICharge } from '../schema/uri-charge/uri-charge.js'; +import { UcChargeLexer } from '../syntax/lexers/uc-charge.lexer.js'; import { ChURIAnchor$splitter, ChURIMatrix$splitter, @@ -312,12 +313,14 @@ export class ChURIMatrix extends ChURIParams { function ChURIParams$parse(rawValues: string[], _key: string | null, _params: ChURIParams): any { if (rawValues.length < 2) { - return rawValues.length ? parseURICharge(rawValues[0]) : URICharge.none; + return rawValues.length + ? parseURICharge(UcChargeLexer.scanParam(rawValues[0])) + : URICharge.none; } return new URICharge$List( rawValues - .map(rawValue => parseURICharge(rawValue)) + .map(rawValue => parseURICharge(UcChargeLexer.scanParam(rawValue))) .filter((charge: URICharge): charge is URICharge.Some => charge.isSome()), ); } diff --git a/src/uri/churi-query.spec.ts b/src/uri/churi-query.spec.ts index 2ba5a9f2..9e30cff0 100644 --- a/src/uri/churi-query.spec.ts +++ b/src/uri/churi-query.spec.ts @@ -72,7 +72,6 @@ describe('ChURIQuery', () => { ]); expect(String(params)).toBe(input); expect([...params]).toEqual([...urlParams]); - // expect(String(params)).toBe(String(urlParams)); }); it('handles empty key', () => { const input = '=1&=2'; @@ -107,6 +106,16 @@ describe('ChURIQuery', () => { expect([...params]).toEqual([...urlParams]); expect(String(params)).toBe(String(urlParams)); }); + it('handles plus-encoded space', () => { + const input = 'key+foo=value++bar'; + const params = new ChURIQuery(input); + const urlParams = new URLSearchParams(input); + + expect([...params]).toEqual([['key foo', 'value bar']]); + expect(String(params)).toBe(input); + expect([...params]).toEqual([...urlParams]); + expect(String(params)).toBe(String(urlParams)); + }); it('ignores leading `?`', () => { const input = '?a=1&b=2&a=3'; const params = new ChURIQuery(input); @@ -306,6 +315,13 @@ describe('ChURIQuery', () => { expect(params.getCharge('baz')).toHaveURIChargeItems('(21)(22)'); expect(params.getCharge('test')).toHaveURIChargeItems(''); }); + it('decodes plus as spaces', () => { + const params = new ChURIQuery('?foo=bar+(+te+++st+)+&foo=+1&baz=21+,+22&test'); + + expect(params.getCharge('foo')).toHaveURIChargeItems({ bar: 'te st' }, 1); + expect(params.getCharge('baz')).toHaveURIChargeItems(21, 22); + expect(params.getCharge('test')).toHaveURIChargeItems(''); + }); }); describe('toString', () => {