diff --git a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts index c4763781..276e7c15 100644 --- a/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts +++ b/src/codegen/capacity/__tests__/CapacityEstimatorCodegenContext.spec.ts @@ -215,3 +215,46 @@ describe('or', () => { expect(estimator([1, 2, 3])).toBe(maxEncodingCapacity([1, 2, 3])); }); }); + +describe('standalone codegen function', () => { + test('generates capacity estimator equivalent to compileCapacityEstimator', () => { + const system = new TypeSystem(); + const type = system.t.Array(system.t.str); + + // Compare standalone codegen function with the class method + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + const classEstimator = type.compileCapacityEstimator({}); + + const testData = ['hello', 'world', 'test']; + expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); + expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); + }); + + test('works with complex nested types', () => { + const system = new TypeSystem(); + const type = system.t.Object( + system.t.prop('name', system.t.str), + system.t.prop('items', system.t.Array(system.t.num)), + ); + + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + const classEstimator = type.compileCapacityEstimator({}); + + const testData = {name: 'test', items: [1, 2, 3, 4, 5]}; + expect(standaloneEstimator(testData)).toBe(classEstimator(testData)); + expect(standaloneEstimator(testData)).toBe(maxEncodingCapacity(testData)); + }); + + test('works with const types', () => { + const system = new TypeSystem(); + const type = system.t.Const('hello world'); + + const {codegen} = require('../estimators'); + const standaloneEstimator = codegen(type, {}); + + // For const types, the value doesn't matter + expect(standaloneEstimator(null)).toBe(maxEncodingCapacity('hello world')); + }); +}); diff --git a/src/codegen/capacity/estimators.ts b/src/codegen/capacity/estimators.ts new file mode 100644 index 00000000..7e4a7a43 --- /dev/null +++ b/src/codegen/capacity/estimators.ts @@ -0,0 +1,255 @@ +import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; +import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; +import {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +import type { + CapacityEstimatorCodegenContextOptions, + CompiledCapacityEstimator, +} from './CapacityEstimatorCodegenContext'; +import type {Type} from '../../type'; + +type EstimatorFunction = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type) => void; + +const normalizeAccessor = (key: string): string => { + // Simple property access for valid identifiers, bracket notation otherwise + if (/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) { + return `.${key}`; + } + return `[${JSON.stringify(key)}]`; +}; + +export const any = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const codegen = ctx.codegen; + codegen.link('Value'); + const r = codegen.var(value.use()); + codegen.if( + `${r} instanceof Value`, + () => { + codegen.if( + `${r}.type`, + () => { + ctx.codegen.js(`size += ${r}.type.capacityEstimator()(${r}.data);`); + }, + () => { + ctx.codegen.js(`size += maxEncodingCapacity(${r}.data);`); + }, + ); + }, + () => { + ctx.codegen.js(`size += maxEncodingCapacity(${r});`); + }, + ); +}; + +export const bool = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { + ctx.inc(MaxEncodingOverhead.Boolean); +}; + +export const num = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { + ctx.inc(MaxEncodingOverhead.Number); +}; + +export const str = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { + ctx.inc(MaxEncodingOverhead.String); + ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); +}; + +export const bin = (ctx: CapacityEstimatorCodegenContext, value: JsExpression): void => { + ctx.inc(MaxEncodingOverhead.Binary); + ctx.codegen.js(`size += ${MaxEncodingOverhead.BinaryLengthMultiplier} * ${value.use()}.length;`); +}; + +export const const_ = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const constType = type as any; // ConstType + ctx.inc(maxEncodingCapacity(constType.value())); +}; + +export const arr = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const codegen = ctx.codegen; + ctx.inc(MaxEncodingOverhead.Array); + const rLen = codegen.var(`${value.use()}.length`); + const arrayType = type as any; // ArrayType + const elementType = arrayType.type; + codegen.js(`size += ${MaxEncodingOverhead.ArrayElement} * ${rLen}`); + const fn = elementType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const isConstantSizeType = ['const', 'bool', 'num'].includes(elementType.getTypeName()); + if (isConstantSizeType) { + const rFn = codegen.linkDependency(fn); + codegen.js(`size += ${rLen} * ${rFn}(${elementType.random()});`); + } else { + const rFn = codegen.linkDependency(fn); + const ri = codegen.var('0'); + codegen.js(`for (; ${ri} < ${rLen}; ${ri}++) {`); + codegen.js(`size += ${rFn}(${value.use()}[${ri}]);`); + codegen.js(`}`); + } +}; + +export const tup = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const codegen = ctx.codegen; + const r = codegen.var(value.use()); + const tupleType = type as any; // TupleType + const types = tupleType.types; + const overhead = MaxEncodingOverhead.Array + MaxEncodingOverhead.ArrayElement * types.length; + ctx.inc(overhead); + for (let i = 0; i < types.length; i++) { + const elementType = types[i]; + const fn = elementType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const rFn = codegen.linkDependency(fn); + codegen.js(`size += ${rFn}(${r}[${i}]);`); + } +}; + +export const obj = ( + ctx: CapacityEstimatorCodegenContext, + value: JsExpression, + type: Type, + estimateCapacityFn: EstimatorFunction, +): void => { + const codegen = ctx.codegen; + const r = codegen.var(value.use()); + const objectType = type as any; // ObjectType + const encodeUnknownFields = !!objectType.schema.encodeUnknownFields; + if (encodeUnknownFields) { + codegen.js(`size += maxEncodingCapacity(${r});`); + return; + } + const fields = objectType.fields; + const overhead = MaxEncodingOverhead.Object + fields.length * MaxEncodingOverhead.ObjectElement; + ctx.inc(overhead); + for (const field of fields) { + ctx.inc(maxEncodingCapacity(field.key)); + const accessor = normalizeAccessor(field.key); + const isOptional = field.optional || field.constructor?.name === 'ObjectOptionalFieldType'; + const block = () => estimateCapacityFn(ctx, new JsExpression(() => `${r}${accessor}`), field.value); + if (isOptional) { + codegen.if(`${r}${accessor} !== undefined`, block); + } else block(); + } +}; + +export const map = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const codegen = ctx.codegen; + ctx.inc(MaxEncodingOverhead.Object); + const r = codegen.var(value.use()); + const rKeys = codegen.var(`Object.keys(${r})`); + const rKey = codegen.var(); + const rLen = codegen.var(`${rKeys}.length`); + codegen.js(`size += ${MaxEncodingOverhead.ObjectElement} * ${rLen}`); + const mapType = type as any; // MapType + const valueType = mapType.type; + const fn = valueType.compileCapacityEstimator({ + system: ctx.options.system, + name: ctx.options.name, + }); + const rFn = codegen.linkDependency(fn); + const ri = codegen.var('0'); + codegen.js(`for (; ${ri} < ${rLen}; ${ri}++) {`); + codegen.js(`${rKey} = ${rKeys}[${ri}];`); + codegen.js(`size += maxEncodingCapacity(${rKey}) + ${rFn}(${r}[${rKey}]);`); + codegen.js(`}`); +}; + +export const ref = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const refType = type as any; // RefType + const system = ctx.options.system || refType.system; + if (!system) throw new Error('NO_SYSTEM'); + const estimator = system.resolve(refType.schema.ref).type.capacityEstimator(); + const d = ctx.codegen.linkDependency(estimator); + ctx.codegen.js(`size += ${d}(${value.use()});`); +}; + +export const or = ( + ctx: CapacityEstimatorCodegenContext, + value: JsExpression, + type: Type, + estimateCapacityFn: EstimatorFunction, +): void => { + const codegen = ctx.codegen; + const orType = type as any; // OrType + const discriminator = orType.discriminator(); + const d = codegen.linkDependency(discriminator); + const types = orType.types; + codegen.switch( + `${d}(${value.use()})`, + types.map((childType: Type, index: number) => [ + index, + () => { + estimateCapacityFn(ctx, value, childType); + }, + ]), + ); +}; + +/** + * Main router function that dispatches capacity estimation to the appropriate + * estimator function based on the type's kind. + */ +export const generate = (ctx: CapacityEstimatorCodegenContext, value: JsExpression, type: Type): void => { + const kind = type.getTypeName(); + + switch (kind) { + case 'any': + any(ctx, value, type); + break; + case 'bool': + bool(ctx, value); + break; + case 'num': + num(ctx, value); + break; + case 'str': + str(ctx, value); + break; + case 'bin': + bin(ctx, value); + break; + case 'const': + const_(ctx, value, type); + break; + case 'arr': + arr(ctx, value, type); + break; + case 'tup': + tup(ctx, value, type); + break; + case 'obj': + obj(ctx, value, type, generate); + break; + case 'map': + map(ctx, value, type); + break; + case 'ref': + ref(ctx, value, type); + break; + case 'or': + or(ctx, value, type, generate); + break; + default: + throw new Error(`${kind} type capacity estimation not implemented`); + } +}; + +/** + * Standalone function to generate a capacity estimator for a given type. + */ +export const codegen = ( + type: Type, + options: Omit, +): CompiledCapacityEstimator => { + const ctx = new CapacityEstimatorCodegenContext({ + system: type.system, + ...options, + type: type as any, + }); + const r = ctx.codegen.options.args[0]; + const value = new JsExpression(() => r); + // Use the centralized router instead of the abstract method + generate(ctx, value, type); + return ctx.compile(); +}; diff --git a/src/codegen/capacity/index.ts b/src/codegen/capacity/index.ts new file mode 100644 index 00000000..c8d1075b --- /dev/null +++ b/src/codegen/capacity/index.ts @@ -0,0 +1,7 @@ +export * from './estimators'; + +export {CapacityEstimatorCodegenContext} from './CapacityEstimatorCodegenContext'; +export type { + CapacityEstimatorCodegenContextOptions, + CompiledCapacityEstimator, +} from './CapacityEstimatorCodegenContext'; diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 0d505eb4..27271aba 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -36,6 +36,7 @@ import { type CapacityEstimatorCodegenContextOptions, type CompiledCapacityEstimator, } from '../../codegen/capacity/CapacityEstimatorCodegenContext'; +import {generate} from '../../codegen/capacity/estimators'; import type {JsonValueCodec} from '@jsonjoy.com/json-pack/lib/codecs/types'; import type * as jsonSchema from '../../json-schema'; import type {BaseType} from '../types'; @@ -265,14 +266,11 @@ export abstract class AbstractType implements BaseType< }); const r = ctx.codegen.options.args[0]; const value = new JsExpression(() => r); - this.codegenCapacityEstimator(ctx, value); + // Use the centralized router instead of the abstract method + generate(ctx, value, this as any); return ctx.compile(); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - throw new Error(`${this.toStringName()}.codegenCapacityEstimator() not implemented`); - } - private __capacityEstimator: CompiledCapacityEstimator | undefined; public capacityEstimator(): CompiledCapacityEstimator { return ( diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index 9d4c7c70..ab44b9fd 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -82,29 +82,6 @@ export class AnyType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - const codegen = ctx.codegen; - codegen.link('Value'); - const r = codegen.var(value.use()); - codegen.if( - `${r} instanceof Value`, - () => { - codegen.if( - `${r}.type`, - () => { - ctx.codegen.js(`size += ${r}.type.capacityEstimator()(${r}.data);`); - }, - () => { - ctx.codegen.js(`size += maxEncodingCapacity(${r}.data);`); - }, - ); - }, - () => { - ctx.codegen.js(`size += maxEncodingCapacity(${r});`); - }, - ); - } - public random(): unknown { return RandomJson.generate({nodeCount: 5}); } diff --git a/src/type/classes/ArrayType.ts b/src/type/classes/ArrayType.ts index 2e2e8f54..9b1d6173 100644 --- a/src/type/classes/ArrayType.ts +++ b/src/type/classes/ArrayType.ts @@ -11,12 +11,7 @@ import {CborEncoderCodegenContext} from '../../codegen/binary/CborEncoderCodegen import type {JsonEncoderCodegenContext} from '../../codegen/binary/JsonEncoderCodegenContext'; import type {BinaryEncoderCodegenContext} from '../../codegen/binary/BinaryEncoderCodegenContext'; import {MessagePackEncoderCodegenContext} from '../../codegen/binary/MessagePackEncoderCodegenContext'; -import type {CapacityEstimatorCodegenContext} from '../../codegen/capacity/CapacityEstimatorCodegenContext'; -import {MaxEncodingOverhead} from '@jsonjoy.com/util/lib/json-size'; import {AbstractType} from './AbstractType'; -import {ConstType} from './ConstType'; -import {BooleanType} from './BooleanType'; -import {NumberType} from './NumberType'; import type * as jsonSchema from '../../json-schema'; import type {SchemaOf, Type} from '../types'; import type {TypeSystem} from '../../system/TypeSystem'; @@ -163,27 +158,6 @@ export class ArrayType extends AbstractType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Boolean); - } - public random(): boolean { return RandomJson.genBoolean(); } diff --git a/src/type/classes/ConstType.ts b/src/type/classes/ConstType.ts index 3d3715ce..8d39e3a6 100644 --- a/src/type/classes/ConstType.ts +++ b/src/type/classes/ConstType.ts @@ -84,10 +84,6 @@ export class ConstType extends AbstractType> { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(maxEncodingCapacity(this.value())); - } - public random(): unknown { return cloneBinary(this.schema.value); } diff --git a/src/type/classes/MapType.ts b/src/type/classes/MapType.ts index e1aa19b2..dda38976 100644 --- a/src/type/classes/MapType.ts +++ b/src/type/classes/MapType.ts @@ -151,30 +151,6 @@ export class MapType extends AbstractType { const length = Math.round(Math.random() * 10); const res: Record = {}; diff --git a/src/type/classes/NumberType.ts b/src/type/classes/NumberType.ts index 8cadd782..d6618ced 100644 --- a/src/type/classes/NumberType.ts +++ b/src/type/classes/NumberType.ts @@ -166,10 +166,6 @@ export class NumberType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Number); - } - public random(): number { let num = Math.random(); let min = Number.MIN_SAFE_INTEGER; diff --git a/src/type/classes/ObjectType.ts b/src/type/classes/ObjectType.ts index 9c97cd65..cf080acd 100644 --- a/src/type/classes/ObjectType.ts +++ b/src/type/classes/ObjectType.ts @@ -497,28 +497,6 @@ if (${rLength}) { } } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - const codegen = ctx.codegen; - const r = codegen.var(value.use()); - const encodeUnknownFields = !!this.schema.encodeUnknownFields; - if (encodeUnknownFields) { - codegen.js(`size += maxEncodingCapacity(${r});`); - return; - } - const fields = this.fields; - const overhead = MaxEncodingOverhead.Object + fields.length * MaxEncodingOverhead.ObjectElement; - ctx.inc(overhead); - for (const field of fields) { - ctx.inc(maxEncodingCapacity(field.key)); - const accessor = normalizeAccessor(field.key); - const isOptional = field instanceof ObjectOptionalFieldType; - const block = () => field.value.codegenCapacityEstimator(ctx, new JsExpression(() => `${r}${accessor}`)); - if (isOptional) { - codegen.if(`${r}${accessor} !== undefined`, block); - } else block(); - } - } - public random(): Record { const schema = this.schema; const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; diff --git a/src/type/classes/OrType.ts b/src/type/classes/OrType.ts index 369f44a0..0c15d059 100644 --- a/src/type/classes/OrType.ts +++ b/src/type/classes/OrType.ts @@ -144,22 +144,6 @@ export class OrType extends AbstractType [ - index, - () => { - type.codegenCapacityEstimator(ctx, value); - }, - ]), - ); - } - public random(): unknown { const types = this.types; const index = Math.floor(Math.random() * types.length); diff --git a/src/type/classes/RefType.ts b/src/type/classes/RefType.ts index 06793538..9b38f3c9 100644 --- a/src/type/classes/RefType.ts +++ b/src/type/classes/RefType.ts @@ -128,14 +128,6 @@ export class RefType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.String); - ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); - } - public random(): string { let length = Math.round(Math.random() * 10); const {min, max} = this.schema; diff --git a/src/type/classes/TupleType.ts b/src/type/classes/TupleType.ts index a5e4c09c..56c03bd6 100644 --- a/src/type/classes/TupleType.ts +++ b/src/type/classes/TupleType.ts @@ -142,23 +142,6 @@ export class TupleType extends AbstractType type.random()); }