From 679aae93fe814166201f8849dfc7a5e46b608144 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:42:15 +0000 Subject: [PATCH 1/8] Initial plan From 68517ca12fe4c1d7499a7e05970d3be0efd91ddb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:53:49 +0000 Subject: [PATCH 2/8] Create random generators and main router function Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/random/any.ts | 6 +++ src/random/array.ts | 14 +++++++ src/random/binary.ts | 9 +++++ src/random/boolean.ts | 6 +++ src/random/const.ts | 6 +++ src/random/function.ts | 5 +++ src/random/generator.ts | 69 ++++++++++++++++++++++++++++++++ src/random/index.ts | 17 ++++++++ src/random/map.ts | 11 +++++ src/random/number.ts | 55 +++++++++++++++++++++++++ src/random/object.ts | 13 ++++++ src/random/or.ts | 7 ++++ src/random/ref.ts | 7 ++++ src/random/string.ts | 11 +++++ src/random/tuple.ts | 5 +++ src/random/types.ts | 3 ++ src/type/classes/AbstractType.ts | 3 +- 17 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/random/any.ts create mode 100644 src/random/array.ts create mode 100644 src/random/binary.ts create mode 100644 src/random/boolean.ts create mode 100644 src/random/const.ts create mode 100644 src/random/function.ts create mode 100644 src/random/generator.ts create mode 100644 src/random/index.ts create mode 100644 src/random/map.ts create mode 100644 src/random/number.ts create mode 100644 src/random/object.ts create mode 100644 src/random/or.ts create mode 100644 src/random/ref.ts create mode 100644 src/random/string.ts create mode 100644 src/random/tuple.ts create mode 100644 src/random/types.ts diff --git a/src/random/any.ts b/src/random/any.ts new file mode 100644 index 00000000..b5797d4e --- /dev/null +++ b/src/random/any.ts @@ -0,0 +1,6 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {AbstractType} from '../type/classes/AbstractType'; + +export function randomAny(type: AbstractType): unknown { + return RandomJson.generate({nodeCount: 5}); +} \ No newline at end of file diff --git a/src/random/array.ts b/src/random/array.ts new file mode 100644 index 00000000..c4816f6c --- /dev/null +++ b/src/random/array.ts @@ -0,0 +1,14 @@ +import type {ArrayType} from '../type/classes/ArrayType'; + +export function randomArray(type: ArrayType): unknown[] { + let length = Math.round(Math.random() * 10); + const schema = type.getSchema(); + const {min, max} = schema; + if (min !== undefined && length < min) length = min + length; + if (max !== undefined && length > max) length = max; + const result: unknown[] = []; + for (let i = 0; i < length; i++) { + result.push((type as any).type.random()); + } + return result; +} \ No newline at end of file diff --git a/src/random/binary.ts b/src/random/binary.ts new file mode 100644 index 00000000..5a20163e --- /dev/null +++ b/src/random/binary.ts @@ -0,0 +1,9 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {BinaryType} from '../type/classes/BinaryType'; + +export function randomBinary(type: BinaryType): Uint8Array { + const octets = RandomJson.genString() + .split('') + .map((c) => c.charCodeAt(0)); + return new Uint8Array(octets); +} \ No newline at end of file diff --git a/src/random/boolean.ts b/src/random/boolean.ts new file mode 100644 index 00000000..bf5fc3a4 --- /dev/null +++ b/src/random/boolean.ts @@ -0,0 +1,6 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {BooleanType} from '../type/classes/BooleanType'; + +export function randomBoolean(type: BooleanType): boolean { + return RandomJson.genBoolean(); +} \ No newline at end of file diff --git a/src/random/const.ts b/src/random/const.ts new file mode 100644 index 00000000..fd4a6cbb --- /dev/null +++ b/src/random/const.ts @@ -0,0 +1,6 @@ +import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone'; +import type {ConstType} from '../type/classes/ConstType'; + +export function randomConst(type: ConstType): unknown { + return cloneBinary(type.getSchema().value); +} \ No newline at end of file diff --git a/src/random/function.ts b/src/random/function.ts new file mode 100644 index 00000000..abbd53f3 --- /dev/null +++ b/src/random/function.ts @@ -0,0 +1,5 @@ +import type {FunctionType} from '../type/classes/FunctionType'; + +export function randomFunction(type: FunctionType): unknown { + return async () => type.res.random(); +} \ No newline at end of file diff --git a/src/random/generator.ts b/src/random/generator.ts new file mode 100644 index 00000000..df664619 --- /dev/null +++ b/src/random/generator.ts @@ -0,0 +1,69 @@ +import type {AbstractType} from '../type/classes/AbstractType'; +import type {AnyType} from '../type/classes/AnyType'; +import type {ArrayType} from '../type/classes/ArrayType'; +import type {BinaryType} from '../type/classes/BinaryType'; +import type {BooleanType} from '../type/classes/BooleanType'; +import type {ConstType} from '../type/classes/ConstType'; +import type {FunctionType} from '../type/classes/FunctionType'; +import type {MapType} from '../type/classes/MapType'; +import type {NumberType} from '../type/classes/NumberType'; +import type {ObjectType} from '../type/classes/ObjectType'; +import type {OrType} from '../type/classes/OrType'; +import type {RefType} from '../type/classes/RefType'; +import type {StringType} from '../type/classes/StringType'; +import type {TupleType} from '../type/classes/TupleType'; + +import {randomAny} from './any'; +import {randomArray} from './array'; +import {randomBinary} from './binary'; +import {randomBoolean} from './boolean'; +import {randomConst} from './const'; +import {randomFunction} from './function'; +import {randomMap} from './map'; +import {randomNumber} from './number'; +import {randomObject} from './object'; +import {randomOr} from './or'; +import {randomRef} from './ref'; +import {randomString} from './string'; +import {randomTuple} from './tuple'; + +/** + * Main router function that dispatches to the correct random generator based on the type's kind. + * This replaces the individual random() methods in each type class. + */ +export function generateRandom(type: AbstractType): unknown { + const kind = type.getTypeName(); + + switch (kind) { + case 'any': + return randomAny(type as AnyType); + case 'arr': + return randomArray(type as ArrayType); + case 'bin': + return randomBinary(type as BinaryType); + case 'bool': + return randomBoolean(type as BooleanType); + case 'const': + return randomConst(type as ConstType); + case 'fn': + case 'fn$': + return randomFunction(type as FunctionType); + case 'map': + return randomMap(type as MapType); + case 'num': + return randomNumber(type as NumberType); + case 'obj': + return randomObject(type as ObjectType); + case 'or': + return randomOr(type as OrType); + case 'ref': + return randomRef(type as RefType); + case 'str': + return randomString(type as StringType); + case 'tup': + return randomTuple(type as TupleType); + default: + // Fallback to generic random JSON for unknown types + return randomAny(type); + } +} \ No newline at end of file diff --git a/src/random/index.ts b/src/random/index.ts new file mode 100644 index 00000000..0099b10c --- /dev/null +++ b/src/random/index.ts @@ -0,0 +1,17 @@ +export * from './generator'; +export * from './types'; + +// Individual generators +export * from './any'; +export * from './array'; +export * from './binary'; +export * from './boolean'; +export * from './const'; +export * from './function'; +export * from './map'; +export * from './number'; +export * from './object'; +export * from './or'; +export * from './ref'; +export * from './string'; +export * from './tuple'; \ No newline at end of file diff --git a/src/random/map.ts b/src/random/map.ts new file mode 100644 index 00000000..dfa08148 --- /dev/null +++ b/src/random/map.ts @@ -0,0 +1,11 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {MapType} from '../type/classes/MapType'; + +export function randomMap(type: MapType): Record { + const length = Math.round(Math.random() * 10); + const res: Record = {}; + for (let i = 0; i < length; i++) { + res[RandomJson.genString(length)] = (type as any).type.random(); + } + return res; +} \ No newline at end of file diff --git a/src/random/number.ts b/src/random/number.ts new file mode 100644 index 00000000..7cf58c0e --- /dev/null +++ b/src/random/number.ts @@ -0,0 +1,55 @@ +import type {NumberType} from '../type/classes/NumberType'; + +export function randomNumber(type: NumberType): number { + let num = Math.random(); + let min = Number.MIN_SAFE_INTEGER; + let max = Number.MAX_SAFE_INTEGER; + const schema = type.getSchema(); + if (schema.gt !== undefined) min = schema.gt; + if (schema.gte !== undefined) min = schema.gte + 0.000000000000001; + if (schema.lt !== undefined) max = schema.lt; + if (schema.lte !== undefined) max = schema.lte - 0.000000000000001; + if (schema.format) { + switch (schema.format) { + case 'i8': + min = Math.max(min, -0x80); + max = Math.min(max, 0x7f); + break; + case 'i16': + min = Math.max(min, -0x8000); + max = Math.min(max, 0x7fff); + break; + case 'i32': + min = Math.max(min, -0x80000000); + max = Math.min(max, 0x7fffffff); + break; + case 'i64': + case 'i': + min = Math.max(min, -0x8000000000); + max = Math.min(max, 0x7fffffffff); + break; + case 'u8': + min = Math.max(min, 0); + max = Math.min(max, 0xff); + break; + case 'u16': + min = Math.max(min, 0); + max = Math.min(max, 0xffff); + break; + case 'u32': + min = Math.max(min, 0); + max = Math.min(max, 0xffffffff); + break; + case 'u64': + case 'u': + min = Math.max(min, 0); + max = Math.min(max, 0xffffffffffff); + break; + } + return Math.round(num * (max - min)) + min; + } + num = num * (max - min) + min; + if (Math.random() > 0.7) num = Math.round(num); + if (num === 0) return 0; + return num; +} \ No newline at end of file diff --git a/src/random/object.ts b/src/random/object.ts new file mode 100644 index 00000000..74e0969e --- /dev/null +++ b/src/random/object.ts @@ -0,0 +1,13 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {ObjectType} from '../type/classes/ObjectType'; + +export function randomObject(type: ObjectType): Record { + const schema = type.getSchema(); + const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; + // Use runtime check to avoid circular import with ObjectOptionalFieldType + for (const field of (type as any).fields) { + if (field.constructor.name === 'ObjectOptionalFieldType') if (Math.random() > 0.5) continue; + obj[field.key] = field.value.random(); + } + return obj; +} \ No newline at end of file diff --git a/src/random/or.ts b/src/random/or.ts new file mode 100644 index 00000000..99bc6ef3 --- /dev/null +++ b/src/random/or.ts @@ -0,0 +1,7 @@ +import type {OrType} from '../type/classes/OrType'; + +export function randomOr(type: OrType): unknown { + const types = (type as any).types; + const index = Math.floor(Math.random() * types.length); + return types[index].random(); +} \ No newline at end of file diff --git a/src/random/ref.ts b/src/random/ref.ts new file mode 100644 index 00000000..6f5fd6ef --- /dev/null +++ b/src/random/ref.ts @@ -0,0 +1,7 @@ +import type {RefType} from '../type/classes/RefType'; + +export function randomRef(type: RefType): unknown { + if (!type.system) throw new Error('NO_SYSTEM'); + const alias = type.system.resolve(type.getSchema().ref); + return alias.type.random(); +} \ No newline at end of file diff --git a/src/random/string.ts b/src/random/string.ts new file mode 100644 index 00000000..68e06768 --- /dev/null +++ b/src/random/string.ts @@ -0,0 +1,11 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import type {StringType} from '../type/classes/StringType'; + +export function randomString(type: StringType): string { + let length = Math.round(Math.random() * 10); + const schema = type.getSchema(); + const {min, max} = schema; + if (min !== undefined && length < min) length = min + length; + if (max !== undefined && length > max) length = max; + return RandomJson.genString(length); +} \ No newline at end of file diff --git a/src/random/tuple.ts b/src/random/tuple.ts new file mode 100644 index 00000000..f692d723 --- /dev/null +++ b/src/random/tuple.ts @@ -0,0 +1,5 @@ +import type {TupleType} from '../type/classes/TupleType'; + +export function randomTuple(type: TupleType): unknown[] { + return (type as any).types.map((subType: any) => subType.random()); +} \ No newline at end of file diff --git a/src/random/types.ts b/src/random/types.ts new file mode 100644 index 00000000..b17c16b1 --- /dev/null +++ b/src/random/types.ts @@ -0,0 +1,3 @@ +import type {AbstractType} from '../type/classes/AbstractType'; + +export type RandomGeneratorFunction = (type: AbstractType) => unknown; \ No newline at end of file diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 0d505eb4..8a158681 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -45,6 +45,7 @@ import type * as ts from '../../typescript/types'; import type {TypeExportContext} from '../../system/TypeExportContext'; import type {Validators} from './types'; import type * as jtd from '../../jtd/types'; +import {generateRandom} from '../../random/generator'; export abstract class AbstractType implements BaseType, Printable { /** Default type system to use, if any. */ @@ -282,7 +283,7 @@ export abstract class AbstractType implements BaseType< } public random(): unknown { - return RandomJson.generate({nodeCount: 5}); + return generateRandom(this); } public toTypeScriptAst(): ts.TsNode { From e98dd06574c6c503277a5e6812c02e87946084a3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:57:45 +0000 Subject: [PATCH 3/8] Changes before error encountered Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/type/classes/AnyType.ts | 4 ---- src/type/classes/ArrayType.ts | 10 ---------- src/type/classes/BinaryType.ts | 7 ------- src/type/classes/BooleanType.ts | 4 ---- src/type/classes/ConstType.ts | 4 ---- src/type/classes/FunctionType.ts | 8 -------- src/type/classes/MapType.ts | 7 ------- 7 files changed, 44 deletions(-) diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index 9d4c7c70..2a298104 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -105,10 +105,6 @@ export class AnyType extends AbstractType { ); } - public random(): unknown { - return RandomJson.generate({nodeCount: 5}); - } - public toTypeScriptAst(): ts.TsType { return {node: 'AnyKeyword'}; } diff --git a/src/type/classes/ArrayType.ts b/src/type/classes/ArrayType.ts index 2e2e8f54..e7d77dd4 100644 --- a/src/type/classes/ArrayType.ts +++ b/src/type/classes/ArrayType.ts @@ -184,16 +184,6 @@ export class ArrayType extends AbstractType max) length = max; - const arr = []; - for (let i = 0; i < length; i++) arr.push(this.type.random()); - return arr; - } - public toTypeScriptAst(): ts.TsArrayType { return { node: 'ArrayType', diff --git a/src/type/classes/BinaryType.ts b/src/type/classes/BinaryType.ts index ae740075..0f545b1e 100644 --- a/src/type/classes/BinaryType.ts +++ b/src/type/classes/BinaryType.ts @@ -127,13 +127,6 @@ export class BinaryType extends AbstractType c.charCodeAt(0)); - return new Uint8Array(octets); - } - public toTypeScriptAst(): ts.TsGenericTypeAnnotation { return { node: 'GenericTypeAnnotation', diff --git a/src/type/classes/BooleanType.ts b/src/type/classes/BooleanType.ts index a6136fe1..f0c33975 100644 --- a/src/type/classes/BooleanType.ts +++ b/src/type/classes/BooleanType.ts @@ -67,10 +67,6 @@ export class BooleanType extends AbstractType { ctx.inc(MaxEncodingOverhead.Boolean); } - public random(): boolean { - return RandomJson.genBoolean(); - } - public toTypeScriptAst(): ts.TsBooleanKeyword { return {node: 'BooleanKeyword'}; } diff --git a/src/type/classes/ConstType.ts b/src/type/classes/ConstType.ts index 3d3715ce..8ba2673a 100644 --- a/src/type/classes/ConstType.ts +++ b/src/type/classes/ConstType.ts @@ -88,10 +88,6 @@ export class ConstType extends AbstractType> { ctx.inc(maxEncodingCapacity(this.value())); } - public random(): unknown { - return cloneBinary(this.schema.value); - } - public toTypeScriptAst() { const value = this.schema.value; if (value === null) { diff --git a/src/type/classes/FunctionType.ts b/src/type/classes/FunctionType.ts index 98a3a8a9..153d1bc6 100644 --- a/src/type/classes/FunctionType.ts +++ b/src/type/classes/FunctionType.ts @@ -57,10 +57,6 @@ export class FunctionType extends AbstractTy this.res.validateSchema(); } - public random(): unknown { - return async () => this.res.random(); - } - public singleton?: FunctionImpl = undefined; public implement(singleton: FunctionImpl): this { @@ -136,10 +132,6 @@ export class FunctionStreamingType extends A this.res.validateSchema(); } - public random(): unknown { - return async () => this.res.random(); - } - public singleton?: FunctionStreamingImpl = undefined; public implement(singleton: FunctionStreamingImpl): this { diff --git a/src/type/classes/MapType.ts b/src/type/classes/MapType.ts index e1aa19b2..d676f64a 100644 --- a/src/type/classes/MapType.ts +++ b/src/type/classes/MapType.ts @@ -175,13 +175,6 @@ export class MapType extends AbstractType { - const length = Math.round(Math.random() * 10); - const res: Record = {}; - for (let i = 0; i < length; i++) res[RandomJson.genString(length)] = this.type.random(); - return res; - } - public toTypeScriptAst(): ts.TsTypeReference { const node: ts.TsTypeReference = { node: 'TypeReference', From 5eba91e12305b3a5492f44ce7da14a8f88793ca7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 08:08:05 +0000 Subject: [PATCH 4/8] Remove random() methods from concrete type classes Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/type/classes/NumberType.ts | 53 +--------------------------------- src/type/classes/ObjectType.ts | 11 +------ src/type/classes/OrType.ts | 6 +--- src/type/classes/RefType.ts | 6 +--- src/type/classes/StringType.ts | 9 +----- src/type/classes/TupleType.ts | 4 +-- 6 files changed, 6 insertions(+), 83 deletions(-) diff --git a/src/type/classes/NumberType.ts b/src/type/classes/NumberType.ts index 8cadd782..dfaca32e 100644 --- a/src/type/classes/NumberType.ts +++ b/src/type/classes/NumberType.ts @@ -170,58 +170,7 @@ export class NumberType extends AbstractType { ctx.inc(MaxEncodingOverhead.Number); } - public random(): number { - let num = Math.random(); - let min = Number.MIN_SAFE_INTEGER; - let max = Number.MAX_SAFE_INTEGER; - if (this.schema.gt !== undefined) min = this.schema.gt; - if (this.schema.gte !== undefined) min = this.schema.gte + 0.000000000000001; - if (this.schema.lt !== undefined) max = this.schema.lt; - if (this.schema.lte !== undefined) max = this.schema.lte - 0.000000000000001; - if (this.schema.format) { - switch (this.schema.format) { - case 'i8': - min = Math.max(min, -0x80); - max = Math.min(max, 0x7f); - break; - case 'i16': - min = Math.max(min, -0x8000); - max = Math.min(max, 0x7fff); - break; - case 'i32': - min = Math.max(min, -0x80000000); - max = Math.min(max, 0x7fffffff); - break; - case 'i64': - case 'i': - min = Math.max(min, -0x8000000000); - max = Math.min(max, 0x7fffffffff); - break; - case 'u8': - min = Math.max(min, 0); - max = Math.min(max, 0xff); - break; - case 'u16': - min = Math.max(min, 0); - max = Math.min(max, 0xffff); - break; - case 'u32': - min = Math.max(min, 0); - max = Math.min(max, 0xffffffff); - break; - case 'u64': - case 'u': - min = Math.max(min, 0); - max = Math.min(max, 0xffffffffffff); - break; - } - return Math.round(num * (max - min)) + min; - } - num = num * (max - min) + min; - if (Math.random() > 0.7) num = Math.round(num); - if (num === 0) return 0; - return num; - } + public toTypeScriptAst(): ts.TsNumberKeyword { return {node: 'NumberKeyword'}; diff --git a/src/type/classes/ObjectType.ts b/src/type/classes/ObjectType.ts index 9c97cd65..ffa54c88 100644 --- a/src/type/classes/ObjectType.ts +++ b/src/type/classes/ObjectType.ts @@ -2,7 +2,6 @@ import {normalizeAccessor} from '@jsonjoy.com/util/lib/codegen/util/normalizeAcc import {MaxEncodingOverhead, maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; import {JsExpression} from '@jsonjoy.com/util/lib/codegen/util/JsExpression'; import {asString} from '@jsonjoy.com/util/lib/strings/asString'; -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; import {printTree} from 'tree-dump/lib/printTree'; import * as schema from '../../schema'; import {validateTType, validateWithValidator} from '../../schema/validate'; @@ -519,15 +518,7 @@ if (${rLength}) { } } - public random(): Record { - const schema = this.schema; - const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; - for (const field of this.fields) { - if (field instanceof ObjectOptionalFieldType) if (Math.random() > 0.5) continue; - obj[field.key] = field.value.random(); - } - return obj; - } + public toTypeScriptAst(): ts.TsTypeLiteral { const node: ts.TsTypeLiteral = { diff --git a/src/type/classes/OrType.ts b/src/type/classes/OrType.ts index 369f44a0..4347213f 100644 --- a/src/type/classes/OrType.ts +++ b/src/type/classes/OrType.ts @@ -160,11 +160,7 @@ export class OrType extends AbstractType extends AbstractType { ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); } - public random(): string { - let length = Math.round(Math.random() * 10); - const {min, max} = this.schema; - if (min !== undefined && length < min) length = min + length; - if (max !== undefined && length > max) length = max; - return RandomJson.genString(length); - } + public toTypeScriptAst(): ts.TsStringKeyword { return {node: 'StringKeyword'}; diff --git a/src/type/classes/TupleType.ts b/src/type/classes/TupleType.ts index a5e4c09c..65593725 100644 --- a/src/type/classes/TupleType.ts +++ b/src/type/classes/TupleType.ts @@ -159,9 +159,7 @@ export class TupleType extends AbstractType type.random()); - } + public toTypeScriptAst(): ts.TsTupleType { return { From 7156590f82ea0033e5c5a20d754962b8f927f1ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 08:59:21 +0000 Subject: [PATCH 5/8] Consolidate random generators and rename main function - Rename generateRandom to random in main export - Consolidate all individual type random generators into single module with arrow functions and short names - Update generator.ts to use consolidated generators - Remove individual generator files - All tests passing Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/random/any.ts | 6 -- src/random/array.ts | 14 --- src/random/binary.ts | 9 -- src/random/boolean.ts | 6 -- src/random/const.ts | 6 -- src/random/function.ts | 5 - src/random/generator.ts | 44 ++++----- src/random/generators.ts | 151 +++++++++++++++++++++++++++++++ src/random/index.ts | 18 +--- src/random/map.ts | 11 --- src/random/number.ts | 55 ----------- src/random/object.ts | 13 --- src/random/or.ts | 7 -- src/random/ref.ts | 7 -- src/random/string.ts | 11 --- src/random/tuple.ts | 5 - src/type/classes/AbstractType.ts | 4 +- 17 files changed, 171 insertions(+), 201 deletions(-) delete mode 100644 src/random/any.ts delete mode 100644 src/random/array.ts delete mode 100644 src/random/binary.ts delete mode 100644 src/random/boolean.ts delete mode 100644 src/random/const.ts delete mode 100644 src/random/function.ts create mode 100644 src/random/generators.ts delete mode 100644 src/random/map.ts delete mode 100644 src/random/number.ts delete mode 100644 src/random/object.ts delete mode 100644 src/random/or.ts delete mode 100644 src/random/ref.ts delete mode 100644 src/random/string.ts delete mode 100644 src/random/tuple.ts diff --git a/src/random/any.ts b/src/random/any.ts deleted file mode 100644 index b5797d4e..00000000 --- a/src/random/any.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {AbstractType} from '../type/classes/AbstractType'; - -export function randomAny(type: AbstractType): unknown { - return RandomJson.generate({nodeCount: 5}); -} \ No newline at end of file diff --git a/src/random/array.ts b/src/random/array.ts deleted file mode 100644 index c4816f6c..00000000 --- a/src/random/array.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type {ArrayType} from '../type/classes/ArrayType'; - -export function randomArray(type: ArrayType): unknown[] { - let length = Math.round(Math.random() * 10); - const schema = type.getSchema(); - const {min, max} = schema; - if (min !== undefined && length < min) length = min + length; - if (max !== undefined && length > max) length = max; - const result: unknown[] = []; - for (let i = 0; i < length; i++) { - result.push((type as any).type.random()); - } - return result; -} \ No newline at end of file diff --git a/src/random/binary.ts b/src/random/binary.ts deleted file mode 100644 index 5a20163e..00000000 --- a/src/random/binary.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {BinaryType} from '../type/classes/BinaryType'; - -export function randomBinary(type: BinaryType): Uint8Array { - const octets = RandomJson.genString() - .split('') - .map((c) => c.charCodeAt(0)); - return new Uint8Array(octets); -} \ No newline at end of file diff --git a/src/random/boolean.ts b/src/random/boolean.ts deleted file mode 100644 index bf5fc3a4..00000000 --- a/src/random/boolean.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {BooleanType} from '../type/classes/BooleanType'; - -export function randomBoolean(type: BooleanType): boolean { - return RandomJson.genBoolean(); -} \ No newline at end of file diff --git a/src/random/const.ts b/src/random/const.ts deleted file mode 100644 index fd4a6cbb..00000000 --- a/src/random/const.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone'; -import type {ConstType} from '../type/classes/ConstType'; - -export function randomConst(type: ConstType): unknown { - return cloneBinary(type.getSchema().value); -} \ No newline at end of file diff --git a/src/random/function.ts b/src/random/function.ts deleted file mode 100644 index abbd53f3..00000000 --- a/src/random/function.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {FunctionType} from '../type/classes/FunctionType'; - -export function randomFunction(type: FunctionType): unknown { - return async () => type.res.random(); -} \ No newline at end of file diff --git a/src/random/generator.ts b/src/random/generator.ts index df664619..3e44ab12 100644 --- a/src/random/generator.ts +++ b/src/random/generator.ts @@ -13,57 +13,45 @@ import type {RefType} from '../type/classes/RefType'; import type {StringType} from '../type/classes/StringType'; import type {TupleType} from '../type/classes/TupleType'; -import {randomAny} from './any'; -import {randomArray} from './array'; -import {randomBinary} from './binary'; -import {randomBoolean} from './boolean'; -import {randomConst} from './const'; -import {randomFunction} from './function'; -import {randomMap} from './map'; -import {randomNumber} from './number'; -import {randomObject} from './object'; -import {randomOr} from './or'; -import {randomRef} from './ref'; -import {randomString} from './string'; -import {randomTuple} from './tuple'; +import * as gen from './generators'; /** * Main router function that dispatches to the correct random generator based on the type's kind. * This replaces the individual random() methods in each type class. */ -export function generateRandom(type: AbstractType): unknown { +export function random(type: AbstractType): unknown { const kind = type.getTypeName(); switch (kind) { case 'any': - return randomAny(type as AnyType); + return gen.any(type as AnyType); case 'arr': - return randomArray(type as ArrayType); + return gen.arr(type as ArrayType); case 'bin': - return randomBinary(type as BinaryType); + return gen.bin(type as BinaryType); case 'bool': - return randomBoolean(type as BooleanType); + return gen.bool(type as BooleanType); case 'const': - return randomConst(type as ConstType); + return gen.const_(type as ConstType); case 'fn': case 'fn$': - return randomFunction(type as FunctionType); + return gen.fn(type as FunctionType); case 'map': - return randomMap(type as MapType); + return gen.map(type as MapType); case 'num': - return randomNumber(type as NumberType); + return gen.num(type as NumberType); case 'obj': - return randomObject(type as ObjectType); + return gen.obj(type as ObjectType); case 'or': - return randomOr(type as OrType); + return gen.or(type as OrType); case 'ref': - return randomRef(type as RefType); + return gen.ref(type as RefType); case 'str': - return randomString(type as StringType); + return gen.str(type as StringType); case 'tup': - return randomTuple(type as TupleType); + return gen.tup(type as TupleType); default: // Fallback to generic random JSON for unknown types - return randomAny(type); + return gen.any(type as AnyType); } } \ No newline at end of file diff --git a/src/random/generators.ts b/src/random/generators.ts new file mode 100644 index 00000000..3f5ac019 --- /dev/null +++ b/src/random/generators.ts @@ -0,0 +1,151 @@ +import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; +import {cloneBinary} from '@jsonjoy.com/util/lib/json-clone'; +import type {AbstractType} from '../type/classes/AbstractType'; +import type {AnyType} from '../type/classes/AnyType'; +import type {ArrayType} from '../type/classes/ArrayType'; +import type {BinaryType} from '../type/classes/BinaryType'; +import type {BooleanType} from '../type/classes/BooleanType'; +import type {ConstType} from '../type/classes/ConstType'; +import type {FunctionType} from '../type/classes/FunctionType'; +import type {MapType} from '../type/classes/MapType'; +import type {NumberType} from '../type/classes/NumberType'; +import type {ObjectType} from '../type/classes/ObjectType'; +import type {OrType} from '../type/classes/OrType'; +import type {RefType} from '../type/classes/RefType'; +import type {StringType} from '../type/classes/StringType'; +import type {TupleType} from '../type/classes/TupleType'; + +export const any = (type: AnyType): unknown => { + return RandomJson.generate({nodeCount: 5}); +}; + +export const arr = (type: ArrayType): unknown[] => { + let length = Math.round(Math.random() * 10); + const schema = type.getSchema(); + const {min, max} = schema; + if (min !== undefined && length < min) length = min + length; + if (max !== undefined && length > max) length = max; + const result: unknown[] = []; + for (let i = 0; i < length; i++) { + result.push((type as any).type.random()); + } + return result; +}; + +export const bin = (type: BinaryType): Uint8Array => { + const octets = RandomJson.genString() + .split('') + .map((c) => c.charCodeAt(0)); + return new Uint8Array(octets); +}; + +export const bool = (type: BooleanType): boolean => { + return RandomJson.genBoolean(); +}; + +export const const_ = (type: ConstType): unknown => { + return cloneBinary(type.getSchema().value); +}; + +export const fn = (type: FunctionType): unknown => { + return async () => type.res.random(); +}; + +export const map = (type: MapType): Record => { + const length = Math.round(Math.random() * 10); + const res: Record = {}; + for (let i = 0; i < length; i++) { + res[RandomJson.genString(length)] = (type as any).type.random(); + } + return res; +}; + +export const num = (type: NumberType): number => { + let num = Math.random(); + let min = Number.MIN_SAFE_INTEGER; + let max = Number.MAX_SAFE_INTEGER; + const schema = type.getSchema(); + if (schema.gt !== undefined) min = schema.gt; + if (schema.gte !== undefined) min = schema.gte + 0.000000000000001; + if (schema.lt !== undefined) max = schema.lt; + if (schema.lte !== undefined) max = schema.lte - 0.000000000000001; + if (schema.format) { + switch (schema.format) { + case 'i8': + min = Math.max(min, -0x80); + max = Math.min(max, 0x7f); + break; + case 'i16': + min = Math.max(min, -0x8000); + max = Math.min(max, 0x7fff); + break; + case 'i32': + min = Math.max(min, -0x80000000); + max = Math.min(max, 0x7fffffff); + break; + case 'i64': + case 'i': + min = Math.max(min, -0x8000000000); + max = Math.min(max, 0x7fffffffff); + break; + case 'u8': + min = Math.max(min, 0); + max = Math.min(max, 0xff); + break; + case 'u16': + min = Math.max(min, 0); + max = Math.min(max, 0xffff); + break; + case 'u32': + min = Math.max(min, 0); + max = Math.min(max, 0xffffffff); + break; + case 'u64': + case 'u': + min = Math.max(min, 0); + max = Math.min(max, 0xffffffffffff); + break; + } + return Math.round(num * (max - min)) + min; + } + num = num * (max - min) + min; + if (Math.random() > 0.7) num = Math.round(num); + if (num === 0) return 0; + return num; +}; + +export const obj = (type: ObjectType): Record => { + const schema = type.getSchema(); + const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; + // Use runtime check to avoid circular import with ObjectOptionalFieldType + for (const field of (type as any).fields) { + if (field.constructor.name === 'ObjectOptionalFieldType') if (Math.random() > 0.5) continue; + obj[field.key] = field.value.random(); + } + return obj; +}; + +export const or = (type: OrType): unknown => { + const types = (type as any).types; + const index = Math.floor(Math.random() * types.length); + return types[index].random(); +}; + +export const ref = (type: RefType): unknown => { + if (!type.system) throw new Error('NO_SYSTEM'); + const alias = type.system.resolve(type.getSchema().ref); + return alias.type.random(); +}; + +export const str = (type: StringType): string => { + let length = Math.round(Math.random() * 10); + const schema = type.getSchema(); + const {min, max} = schema; + if (min !== undefined && length < min) length = min + length; + if (max !== undefined && length > max) length = max; + return RandomJson.genString(length); +}; + +export const tup = (type: TupleType): unknown[] => { + return (type as any).types.map((subType: any) => subType.random()); +}; \ No newline at end of file diff --git a/src/random/index.ts b/src/random/index.ts index 0099b10c..2d23486e 100644 --- a/src/random/index.ts +++ b/src/random/index.ts @@ -1,17 +1,3 @@ export * from './generator'; -export * from './types'; - -// Individual generators -export * from './any'; -export * from './array'; -export * from './binary'; -export * from './boolean'; -export * from './const'; -export * from './function'; -export * from './map'; -export * from './number'; -export * from './object'; -export * from './or'; -export * from './ref'; -export * from './string'; -export * from './tuple'; \ No newline at end of file +export * from './generators'; +export * from './types'; \ No newline at end of file diff --git a/src/random/map.ts b/src/random/map.ts deleted file mode 100644 index dfa08148..00000000 --- a/src/random/map.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {MapType} from '../type/classes/MapType'; - -export function randomMap(type: MapType): Record { - const length = Math.round(Math.random() * 10); - const res: Record = {}; - for (let i = 0; i < length; i++) { - res[RandomJson.genString(length)] = (type as any).type.random(); - } - return res; -} \ No newline at end of file diff --git a/src/random/number.ts b/src/random/number.ts deleted file mode 100644 index 7cf58c0e..00000000 --- a/src/random/number.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type {NumberType} from '../type/classes/NumberType'; - -export function randomNumber(type: NumberType): number { - let num = Math.random(); - let min = Number.MIN_SAFE_INTEGER; - let max = Number.MAX_SAFE_INTEGER; - const schema = type.getSchema(); - if (schema.gt !== undefined) min = schema.gt; - if (schema.gte !== undefined) min = schema.gte + 0.000000000000001; - if (schema.lt !== undefined) max = schema.lt; - if (schema.lte !== undefined) max = schema.lte - 0.000000000000001; - if (schema.format) { - switch (schema.format) { - case 'i8': - min = Math.max(min, -0x80); - max = Math.min(max, 0x7f); - break; - case 'i16': - min = Math.max(min, -0x8000); - max = Math.min(max, 0x7fff); - break; - case 'i32': - min = Math.max(min, -0x80000000); - max = Math.min(max, 0x7fffffff); - break; - case 'i64': - case 'i': - min = Math.max(min, -0x8000000000); - max = Math.min(max, 0x7fffffffff); - break; - case 'u8': - min = Math.max(min, 0); - max = Math.min(max, 0xff); - break; - case 'u16': - min = Math.max(min, 0); - max = Math.min(max, 0xffff); - break; - case 'u32': - min = Math.max(min, 0); - max = Math.min(max, 0xffffffff); - break; - case 'u64': - case 'u': - min = Math.max(min, 0); - max = Math.min(max, 0xffffffffffff); - break; - } - return Math.round(num * (max - min)) + min; - } - num = num * (max - min) + min; - if (Math.random() > 0.7) num = Math.round(num); - if (num === 0) return 0; - return num; -} \ No newline at end of file diff --git a/src/random/object.ts b/src/random/object.ts deleted file mode 100644 index 74e0969e..00000000 --- a/src/random/object.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {ObjectType} from '../type/classes/ObjectType'; - -export function randomObject(type: ObjectType): Record { - const schema = type.getSchema(); - const obj: Record = schema.unknownFields ? >RandomJson.genObject() : {}; - // Use runtime check to avoid circular import with ObjectOptionalFieldType - for (const field of (type as any).fields) { - if (field.constructor.name === 'ObjectOptionalFieldType') if (Math.random() > 0.5) continue; - obj[field.key] = field.value.random(); - } - return obj; -} \ No newline at end of file diff --git a/src/random/or.ts b/src/random/or.ts deleted file mode 100644 index 99bc6ef3..00000000 --- a/src/random/or.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {OrType} from '../type/classes/OrType'; - -export function randomOr(type: OrType): unknown { - const types = (type as any).types; - const index = Math.floor(Math.random() * types.length); - return types[index].random(); -} \ No newline at end of file diff --git a/src/random/ref.ts b/src/random/ref.ts deleted file mode 100644 index 6f5fd6ef..00000000 --- a/src/random/ref.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type {RefType} from '../type/classes/RefType'; - -export function randomRef(type: RefType): unknown { - if (!type.system) throw new Error('NO_SYSTEM'); - const alias = type.system.resolve(type.getSchema().ref); - return alias.type.random(); -} \ No newline at end of file diff --git a/src/random/string.ts b/src/random/string.ts deleted file mode 100644 index 68e06768..00000000 --- a/src/random/string.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {RandomJson} from '@jsonjoy.com/util/lib/json-random'; -import type {StringType} from '../type/classes/StringType'; - -export function randomString(type: StringType): string { - let length = Math.round(Math.random() * 10); - const schema = type.getSchema(); - const {min, max} = schema; - if (min !== undefined && length < min) length = min + length; - if (max !== undefined && length > max) length = max; - return RandomJson.genString(length); -} \ No newline at end of file diff --git a/src/random/tuple.ts b/src/random/tuple.ts deleted file mode 100644 index f692d723..00000000 --- a/src/random/tuple.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {TupleType} from '../type/classes/TupleType'; - -export function randomTuple(type: TupleType): unknown[] { - return (type as any).types.map((subType: any) => subType.random()); -} \ No newline at end of file diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index 8a158681..e067f266 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -45,7 +45,7 @@ import type * as ts from '../../typescript/types'; import type {TypeExportContext} from '../../system/TypeExportContext'; import type {Validators} from './types'; import type * as jtd from '../../jtd/types'; -import {generateRandom} from '../../random/generator'; +import {random} from '../../random/generator'; export abstract class AbstractType implements BaseType, Printable { /** Default type system to use, if any. */ @@ -283,7 +283,7 @@ export abstract class AbstractType implements BaseType< } public random(): unknown { - return generateRandom(this); + return random(this); } public toTypeScriptAst(): ts.TsNode { From c64769b33f7891579c9e77c86f778cf316a412a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 09:06:08 +0000 Subject: [PATCH 6/8] Add comprehensive unit tests for random module - Create fixtures file with JSON Type schemas for reuse across modules - Add 20 comprehensive tests for individual generators and main router - Test validates generated values pass JSON Type validation - Test edge cases, constraints, and complex nested structures - Test deterministic behavior with controlled randomness - All 497 tests passing Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/__tests__/fixtures.ts | 79 ++++++++ src/random/__tests__/random.spec.ts | 285 ++++++++++++++++++++++++++++ 2 files changed, 364 insertions(+) create mode 100644 src/__tests__/fixtures.ts create mode 100644 src/random/__tests__/random.spec.ts diff --git a/src/__tests__/fixtures.ts b/src/__tests__/fixtures.ts new file mode 100644 index 00000000..454047d7 --- /dev/null +++ b/src/__tests__/fixtures.ts @@ -0,0 +1,79 @@ +/** + * Fixture schemas for testing random value generation. + * These schemas represent different JSON Type configurations that can be used + * across multiple test modules. + */ + +import {s} from '../schema'; + +/** + * Basic primitive type schemas + */ +export const primitiveSchemas = { + string: s.String(), + stringWithMinMax: s.String({min: 5, max: 10}), + number: s.Number(), + numberWithFormat: s.Number({format: 'u32'}), + numberWithRange: s.Number({gte: 0, lte: 100}), + boolean: s.Boolean(), + const: s.Const('fixed-value' as const), + any: s.Any(), +} as const; + +/** + * Complex composite type schemas + */ +export const compositeSchemas = { + simpleArray: s.Array(s.String()), + arrayWithBounds: s.Array(s.Number(), {min: 2, max: 5}), + simpleObject: s.Object([ + s.prop('id', s.String()), + s.prop('name', s.String()), + s.prop('active', s.Boolean()), + ]), + objectWithOptionalFields: s.Object([ + s.prop('id', s.String()), + s.propOpt('name', s.String()), + s.propOpt('count', s.Number()), + ]), + nestedObject: s.Object([ + s.prop('user', s.Object([ + s.prop('id', s.Number()), + s.prop('profile', s.Object([ + s.prop('name', s.String()), + s.prop('email', s.String()), + ])), + ])), + s.prop('tags', s.Array(s.String())), + ]), + tuple: s.Tuple(s.String(), s.Number(), s.Boolean()), + map: s.Map(s.String()), + mapWithComplexValue: s.Map(s.Object([ + s.prop('value', s.Number()), + s.prop('label', s.String()), + ])), + union: s.Or(s.String(), s.Number(), s.Boolean()), + complexUnion: s.Or( + s.String(), + s.Object([s.prop('type', s.Const('object' as const)), s.prop('data', s.Any())]), + s.Array(s.Number()) + ), + binary: s.bin, +} as const; + +/** + * All fixture schemas combined for comprehensive testing + */ +export const allSchemas = { + ...primitiveSchemas, + ...compositeSchemas, +} as const; + +/** + * Schema categories for organized testing + */ +export const schemaCategories = { + primitives: primitiveSchemas, + composites: compositeSchemas, + all: allSchemas, +} as const; \ No newline at end of file diff --git a/src/random/__tests__/random.spec.ts b/src/random/__tests__/random.spec.ts new file mode 100644 index 00000000..b51af810 --- /dev/null +++ b/src/random/__tests__/random.spec.ts @@ -0,0 +1,285 @@ +/** + * Unit tests for the src/random/ module. + * Tests that generated random values conform to their JSON Type schemas. + */ + +import {t} from '../../type'; +import {allSchemas, schemaCategories} from '../../__tests__/fixtures'; +import * as gen from '../generators'; +import {random} from '../generator'; + +describe('random generators', () => { + describe('individual generator functions', () => { + describe('primitives', () => { + test('str generates valid strings', () => { + const type = t.String(); + for (let i = 0; i < 10; i++) { + const value = gen.str(type); + expect(typeof value).toBe('string'); + type.validate(value); + } + }); + + test('str respects min/max constraints', () => { + const type = t.String({min: 5, max: 10}); + for (let i = 0; i < 10; i++) { + const value = gen.str(type); + expect(typeof value).toBe('string'); + expect(value.length).toBeGreaterThanOrEqual(5); + expect(value.length).toBeLessThanOrEqual(10); + type.validate(value); + } + }); + + test('num generates valid numbers', () => { + const type = t.Number(); + for (let i = 0; i < 10; i++) { + const value = gen.num(type); + expect(typeof value).toBe('number'); + type.validate(value); + } + }); + + test('num respects format constraints', () => { + const type = t.Number({format: 'u32'}); + for (let i = 0; i < 10; i++) { + const value = gen.num(type); + expect(typeof value).toBe('number'); + expect(Number.isInteger(value)).toBe(true); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThanOrEqual(0xffffffff); + type.validate(value); + } + }); + + test('bool generates valid booleans', () => { + const type = t.Boolean(); + for (let i = 0; i < 10; i++) { + const value = gen.bool(type); + expect(typeof value).toBe('boolean'); + type.validate(value); + } + }); + + test('const_ generates exact values', () => { + const type = t.Const('fixed-value' as const); + for (let i = 0; i < 10; i++) { + const value = gen.const_(type); + expect(value).toBe('fixed-value'); + type.validate(value); + } + }); + + test('any generates valid JSON values', () => { + const type = t.Any(); + for (let i = 0; i < 10; i++) { + const value = gen.any(type); + expect(value).toBeDefined(); + type.validate(value); + } + }); + + test('bin generates Uint8Array', () => { + const type = t.bin; + for (let i = 0; i < 10; i++) { + const value = gen.bin(type); + expect(value).toBeInstanceOf(Uint8Array); + type.validate(value); + } + }); + }); + + describe('composites', () => { + test('arr generates valid arrays', () => { + const type = t.Array(t.String()); + for (let i = 0; i < 10; i++) { + const value = gen.arr(type); + expect(Array.isArray(value)).toBe(true); + type.validate(value); + } + }); + + test('arr respects min/max constraints', () => { + const type = t.Array(t.String(), {min: 2, max: 5}); + for (let i = 0; i < 10; i++) { + const value = gen.arr(type); + expect(Array.isArray(value)).toBe(true); + expect(value.length).toBeGreaterThanOrEqual(2); + expect(value.length).toBeLessThanOrEqual(5); + type.validate(value); + } + }); + + test('obj generates valid objects', () => { + const type = t.Object( + t.prop('id', t.String()), + t.prop('count', t.Number()), + ); + for (let i = 0; i < 10; i++) { + const value = gen.obj(type); + expect(typeof value).toBe('object'); + expect(value).not.toBeNull(); + expect(value).not.toBeInstanceOf(Array); + expect(value).toHaveProperty('id'); + expect(value).toHaveProperty('count'); + type.validate(value); + } + }); + + test('tup generates valid tuples', () => { + const type = t.Tuple(t.String(), t.Number(), t.Boolean()); + for (let i = 0; i < 10; i++) { + const value = gen.tup(type); + expect(Array.isArray(value)).toBe(true); + expect(value).toHaveLength(3); + expect(typeof value[0]).toBe('string'); + expect(typeof value[1]).toBe('number'); + expect(typeof value[2]).toBe('boolean'); + type.validate(value); + } + }); + + test('map generates valid maps', () => { + const type = t.Map(t.String()); + for (let i = 0; i < 10; i++) { + const value = gen.map(type); + expect(typeof value).toBe('object'); + expect(value).not.toBeNull(); + expect(value).not.toBeInstanceOf(Array); + type.validate(value); + } + }); + + test('or generates values from union types', () => { + const type = t.Or(t.String(), t.Number()); + const generatedTypes = new Set(); + + for (let i = 0; i < 20; i++) { + const value = gen.or(type); + generatedTypes.add(typeof value); + type.validate(value); + } + + // Should generate at least one of each type over multiple iterations + expect(generatedTypes.size).toBeGreaterThan(0); + }); + + test('fn generates async functions', async () => { + const type = t.Function(t.num, t.String()); + const value = gen.fn(type); + expect(typeof value).toBe('function'); + + // Test that the function is async and returns the expected type + const result = await (value as () => Promise)(); + expect(typeof result).toBe('string'); + }); + }); + }); + + describe('main router function', () => { + test('dispatches to correct generators for all types', () => { + Object.entries(schemaCategories.primitives).forEach(([name, schema]) => { + const type = t.from(schema); + for (let i = 0; i < 5; i++) { + const value = random(type); + expect(() => type.validate(value)).not.toThrow(); + } + }); + + Object.entries(schemaCategories.composites).forEach(([name, schema]) => { + const type = t.from(schema); + for (let i = 0; i < 5; i++) { + const value = random(type); + expect(() => type.validate(value)).not.toThrow(); + } + }); + }); + }); + + describe('comprehensive schema validation', () => { + test('generated values pass validation for all fixture schemas', () => { + Object.entries(allSchemas).forEach(([name, schema]) => { + const type = t.from(schema); + + // Test multiple random generations for each schema + for (let i = 0; i < 10; i++) { + const randomValue = type.random(); + + // Test using both validate methods + expect(() => type.validate(randomValue)).not.toThrow(); + + // Test using compiled validator + const validator = type.compileValidator({errors: 'object'}); + const error = validator(randomValue); + expect(error).toBe(null); + } + }); + }); + + test('handles nested complex structures', () => { + const complexType = t.Object( + t.prop('users', t.Array(t.Object( + t.prop('id', t.Number()), + t.prop('profile', t.Object( + t.prop('name', t.String()), + t.prop('preferences', t.Map(t.Or(t.String(), t.Boolean()))), + )), + t.propOpt('tags', t.Array(t.String())), + ))), + t.prop('metadata', t.Map(t.Any())), + t.prop('config', t.Tuple(t.String(), t.Number(), t.Object( + t.prop('enabled', t.Boolean()), + ))), + ); + + for (let i = 0; i < 5; i++) { + const value = complexType.random(); + expect(() => complexType.validate(value)).not.toThrow(); + } + }); + + test('handles edge cases and constraints', () => { + // Empty array constraint + const emptyArrayType = t.Array(t.String(), {max: 0}); + const emptyArray = emptyArrayType.random(); + expect(emptyArray).toEqual([]); + emptyArrayType.validate(emptyArray); + + // Single item array constraint + const singleItemType = t.Array(t.Number(), {min: 1, max: 1}); + const singleItem = singleItemType.random(); + expect(singleItem).toHaveLength(1); + singleItemType.validate(singleItem); + + // Number with tight range + const tightRangeType = t.Number({gte: 5, lte: 5}); + const tightRangeValue = tightRangeType.random(); + expect(tightRangeValue).toBe(5); + tightRangeType.validate(tightRangeValue); + }); + }); + + describe('deterministic behavior with controlled randomness', () => { + test('generates consistent values with mocked Math.random', () => { + const originalRandom = Math.random; + let callCount = 0; + Math.random = () => { + callCount++; + return 0.5; // Always return 0.5 for predictable results + }; + + try { + const type = t.String({min: 5, max: 5}); + const value1 = type.random(); + const value2 = type.random(); + + // With fixed random, string generation should be consistent + expect(value1).toBe(value2); + expect(value1).toHaveLength(5); + type.validate(value1); + } finally { + Math.random = originalRandom; + } + }); + }); +}); \ No newline at end of file From 8589e57c35434e0b5058cc1d35d5d123b52e4bb2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 26 Jul 2025 13:39:21 +0000 Subject: [PATCH 7/8] Resolve merge conflicts with latest master - preserve capacity estimator methods and random functionality Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/__tests__/fixtures.ts | 29 ++++++--------- src/random/__tests__/random.spec.ts | 58 ++++++++++++++--------------- src/random/generator.ts | 4 +- src/random/generators.ts | 2 +- src/random/index.ts | 2 +- src/random/types.ts | 2 +- src/type/classes/AbstractType.ts | 5 +++ src/type/classes/AnyType.ts | 2 +- src/type/classes/ArrayType.ts | 3 +- src/type/classes/NumberType.ts | 2 - src/type/classes/ObjectType.ts | 2 - src/type/classes/OrType.ts | 2 - src/type/classes/RefType.ts | 2 - src/type/classes/StringType.ts | 2 - src/type/classes/TupleType.ts | 2 - 15 files changed, 52 insertions(+), 67 deletions(-) diff --git a/src/__tests__/fixtures.ts b/src/__tests__/fixtures.ts index 454047d7..223debf1 100644 --- a/src/__tests__/fixtures.ts +++ b/src/__tests__/fixtures.ts @@ -26,37 +26,30 @@ export const primitiveSchemas = { export const compositeSchemas = { simpleArray: s.Array(s.String()), arrayWithBounds: s.Array(s.Number(), {min: 2, max: 5}), - simpleObject: s.Object([ - s.prop('id', s.String()), - s.prop('name', s.String()), - s.prop('active', s.Boolean()), - ]), + simpleObject: s.Object([s.prop('id', s.String()), s.prop('name', s.String()), s.prop('active', s.Boolean())]), objectWithOptionalFields: s.Object([ s.prop('id', s.String()), s.propOpt('name', s.String()), s.propOpt('count', s.Number()), ]), nestedObject: s.Object([ - s.prop('user', s.Object([ - s.prop('id', s.Number()), - s.prop('profile', s.Object([ - s.prop('name', s.String()), - s.prop('email', s.String()), - ])), - ])), + s.prop( + 'user', + s.Object([ + s.prop('id', s.Number()), + s.prop('profile', s.Object([s.prop('name', s.String()), s.prop('email', s.String())])), + ]), + ), s.prop('tags', s.Array(s.String())), ]), tuple: s.Tuple(s.String(), s.Number(), s.Boolean()), map: s.Map(s.String()), - mapWithComplexValue: s.Map(s.Object([ - s.prop('value', s.Number()), - s.prop('label', s.String()), - ])), + mapWithComplexValue: s.Map(s.Object([s.prop('value', s.Number()), s.prop('label', s.String())])), union: s.Or(s.String(), s.Number(), s.Boolean()), complexUnion: s.Or( s.String(), s.Object([s.prop('type', s.Const('object' as const)), s.prop('data', s.Any())]), - s.Array(s.Number()) + s.Array(s.Number()), ), binary: s.bin, } as const; @@ -76,4 +69,4 @@ export const schemaCategories = { primitives: primitiveSchemas, composites: compositeSchemas, all: allSchemas, -} as const; \ No newline at end of file +} as const; diff --git a/src/random/__tests__/random.spec.ts b/src/random/__tests__/random.spec.ts index b51af810..e496d0df 100644 --- a/src/random/__tests__/random.spec.ts +++ b/src/random/__tests__/random.spec.ts @@ -111,10 +111,7 @@ describe('random generators', () => { }); test('obj generates valid objects', () => { - const type = t.Object( - t.prop('id', t.String()), - t.prop('count', t.Number()), - ); + const type = t.Object(t.prop('id', t.String()), t.prop('count', t.Number())); for (let i = 0; i < 10; i++) { const value = gen.obj(type); expect(typeof value).toBe('object'); @@ -153,13 +150,13 @@ describe('random generators', () => { test('or generates values from union types', () => { const type = t.Or(t.String(), t.Number()); const generatedTypes = new Set(); - + for (let i = 0; i < 20; i++) { const value = gen.or(type); generatedTypes.add(typeof value); type.validate(value); } - + // Should generate at least one of each type over multiple iterations expect(generatedTypes.size).toBeGreaterThan(0); }); @@ -168,7 +165,7 @@ describe('random generators', () => { const type = t.Function(t.num, t.String()); const value = gen.fn(type); expect(typeof value).toBe('function'); - + // Test that the function is async and returns the expected type const result = await (value as () => Promise)(); expect(typeof result).toBe('string'); @@ -178,58 +175,61 @@ describe('random generators', () => { describe('main router function', () => { test('dispatches to correct generators for all types', () => { - Object.entries(schemaCategories.primitives).forEach(([name, schema]) => { + for (const [name, schema] of Object.entries(schemaCategories.primitives)) { const type = t.from(schema); for (let i = 0; i < 5; i++) { const value = random(type); expect(() => type.validate(value)).not.toThrow(); } - }); + } - Object.entries(schemaCategories.composites).forEach(([name, schema]) => { + for (const [name, schema] of Object.entries(schemaCategories.composites)) { const type = t.from(schema); for (let i = 0; i < 5; i++) { const value = random(type); expect(() => type.validate(value)).not.toThrow(); } - }); + } }); }); describe('comprehensive schema validation', () => { test('generated values pass validation for all fixture schemas', () => { - Object.entries(allSchemas).forEach(([name, schema]) => { + for (const [name, schema] of Object.entries(allSchemas)) { const type = t.from(schema); - + // Test multiple random generations for each schema for (let i = 0; i < 10; i++) { const randomValue = type.random(); - + // Test using both validate methods expect(() => type.validate(randomValue)).not.toThrow(); - + // Test using compiled validator const validator = type.compileValidator({errors: 'object'}); const error = validator(randomValue); expect(error).toBe(null); } - }); + } }); test('handles nested complex structures', () => { const complexType = t.Object( - t.prop('users', t.Array(t.Object( - t.prop('id', t.Number()), - t.prop('profile', t.Object( - t.prop('name', t.String()), - t.prop('preferences', t.Map(t.Or(t.String(), t.Boolean()))), - )), - t.propOpt('tags', t.Array(t.String())), - ))), + t.prop( + 'users', + t.Array( + t.Object( + t.prop('id', t.Number()), + t.prop( + 'profile', + t.Object(t.prop('name', t.String()), t.prop('preferences', t.Map(t.Or(t.String(), t.Boolean())))), + ), + t.propOpt('tags', t.Array(t.String())), + ), + ), + ), t.prop('metadata', t.Map(t.Any())), - t.prop('config', t.Tuple(t.String(), t.Number(), t.Object( - t.prop('enabled', t.Boolean()), - ))), + t.prop('config', t.Tuple(t.String(), t.Number(), t.Object(t.prop('enabled', t.Boolean())))), ); for (let i = 0; i < 5; i++) { @@ -272,7 +272,7 @@ describe('random generators', () => { const type = t.String({min: 5, max: 5}); const value1 = type.random(); const value2 = type.random(); - + // With fixed random, string generation should be consistent expect(value1).toBe(value2); expect(value1).toHaveLength(5); @@ -282,4 +282,4 @@ describe('random generators', () => { } }); }); -}); \ No newline at end of file +}); diff --git a/src/random/generator.ts b/src/random/generator.ts index 3e44ab12..3d0bcba5 100644 --- a/src/random/generator.ts +++ b/src/random/generator.ts @@ -21,7 +21,7 @@ import * as gen from './generators'; */ export function random(type: AbstractType): unknown { const kind = type.getTypeName(); - + switch (kind) { case 'any': return gen.any(type as AnyType); @@ -54,4 +54,4 @@ export function random(type: AbstractType): unknown { // Fallback to generic random JSON for unknown types return gen.any(type as AnyType); } -} \ No newline at end of file +} diff --git a/src/random/generators.ts b/src/random/generators.ts index 3f5ac019..6a919fa2 100644 --- a/src/random/generators.ts +++ b/src/random/generators.ts @@ -148,4 +148,4 @@ export const str = (type: StringType): string => { export const tup = (type: TupleType): unknown[] => { return (type as any).types.map((subType: any) => subType.random()); -}; \ No newline at end of file +}; diff --git a/src/random/index.ts b/src/random/index.ts index 2d23486e..bff5e414 100644 --- a/src/random/index.ts +++ b/src/random/index.ts @@ -1,3 +1,3 @@ export * from './generator'; export * from './generators'; -export * from './types'; \ No newline at end of file +export * from './types'; diff --git a/src/random/types.ts b/src/random/types.ts index b17c16b1..7c417792 100644 --- a/src/random/types.ts +++ b/src/random/types.ts @@ -1,3 +1,3 @@ import type {AbstractType} from '../type/classes/AbstractType'; -export type RandomGeneratorFunction = (type: AbstractType) => unknown; \ No newline at end of file +export type RandomGeneratorFunction = (type: AbstractType) => unknown; diff --git a/src/type/classes/AbstractType.ts b/src/type/classes/AbstractType.ts index aa6f949d..958a7cc2 100644 --- a/src/type/classes/AbstractType.ts +++ b/src/type/classes/AbstractType.ts @@ -254,6 +254,11 @@ export abstract class AbstractType implements BaseType< throw new Error(`${this.toStringName()}.codegenJsonEncoder() not implemented`); } + public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { + // Use the centralized router function + generate(ctx, value, this as any); + } + public compileCapacityEstimator( options: Omit, ): CompiledCapacityEstimator { diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index afe94a13..b8f531cc 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -12,6 +12,7 @@ import {MessagePackEncoderCodegenContext} from '../../codegen/binary/MessagePack import {EncodingFormat} from '@jsonjoy.com/json-pack/lib/constants'; import type {BinaryJsonEncoder} from '@jsonjoy.com/json-pack/lib/types'; import type {CapacityEstimatorCodegenContext} from '../../codegen/capacity/CapacityEstimatorCodegenContext'; +import {maxEncodingCapacity} from '@jsonjoy.com/util/lib/json-size'; import {AbstractType} from './AbstractType'; import type * as jsonSchema from '../../json-schema'; import type * as ts from '../../typescript/types'; @@ -97,7 +98,6 @@ export class AnyType extends AbstractType { }, ); } - } public toTypeScriptAst(): ts.TsType { return {node: 'AnyKeyword'}; diff --git a/src/type/classes/ArrayType.ts b/src/type/classes/ArrayType.ts index 3e2d5ac7..f989deea 100644 --- a/src/type/classes/ArrayType.ts +++ b/src/type/classes/ArrayType.ts @@ -22,7 +22,7 @@ import type {CapacityEstimatorCodegenContext} from '../../codegen/capacity/Capac import {ConstType} from './ConstType'; import {BooleanType} from './BooleanType'; import {NumberType} from './NumberType'; -import {MaxEncodingOverhead} from '../../codegen/capacity/constants'; +import {MaxEncodingOverhead} from '@jsonjoy.com/util/lib/json-size'; import type * as jtd from '../../jtd/types'; export class ArrayType extends AbstractType>> { @@ -172,7 +172,6 @@ export class ArrayType extends AbstractType { ctx.inc(MaxEncodingOverhead.Number); } - - public toTypeScriptAst(): ts.TsNumberKeyword { return {node: 'NumberKeyword'}; } diff --git a/src/type/classes/ObjectType.ts b/src/type/classes/ObjectType.ts index 3e0502f6..ff3dd195 100644 --- a/src/type/classes/ObjectType.ts +++ b/src/type/classes/ObjectType.ts @@ -502,8 +502,6 @@ if (${rLength}) { } } - - public toTypeScriptAst(): ts.TsTypeLiteral { const node: ts.TsTypeLiteral = { node: 'TypeLiteral', diff --git a/src/type/classes/OrType.ts b/src/type/classes/OrType.ts index 2b9199a4..c6dc1e9b 100644 --- a/src/type/classes/OrType.ts +++ b/src/type/classes/OrType.ts @@ -154,8 +154,6 @@ export class OrType extends AbstractType extends AbstractType { ctx.codegen.js(`size += ${MaxEncodingOverhead.StringLengthMultiplier} * ${value.use()}.length;`); } - - public toTypeScriptAst(): ts.TsStringKeyword { return {node: 'StringKeyword'}; } diff --git a/src/type/classes/TupleType.ts b/src/type/classes/TupleType.ts index d8422b1a..5097c200 100644 --- a/src/type/classes/TupleType.ts +++ b/src/type/classes/TupleType.ts @@ -149,8 +149,6 @@ export class TupleType extends AbstractType Date: Sat, 26 Jul 2025 16:34:45 +0000 Subject: [PATCH 8/8] Remove codegenCapacityEstimator methods from all concrete type classes Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> --- src/type/classes/AnyType.ts | 23 ----------------------- src/type/classes/ArrayType.ts | 21 --------------------- src/type/classes/BinaryType.ts | 5 ----- src/type/classes/BooleanType.ts | 4 ---- src/type/classes/ConstType.ts | 4 ---- src/type/classes/MapType.ts | 24 ------------------------ src/type/classes/NumberType.ts | 4 ---- src/type/classes/ObjectType.ts | 22 ---------------------- src/type/classes/OrType.ts | 16 ---------------- src/type/classes/RefType.ts | 8 -------- src/type/classes/StringType.ts | 5 ----- src/type/classes/TupleType.ts | 17 ----------------- 12 files changed, 153 deletions(-) diff --git a/src/type/classes/AnyType.ts b/src/type/classes/AnyType.ts index b8f531cc..52118ae4 100644 --- a/src/type/classes/AnyType.ts +++ b/src/type/classes/AnyType.ts @@ -76,29 +76,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 toTypeScriptAst(): ts.TsType { return {node: 'AnyKeyword'}; } diff --git a/src/type/classes/ArrayType.ts b/src/type/classes/ArrayType.ts index f989deea..06a9d9e5 100644 --- a/src/type/classes/ArrayType.ts +++ b/src/type/classes/ArrayType.ts @@ -152,27 +152,6 @@ export class ArrayType extends AbstractType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Boolean); - } - public toTypeScriptAst(): ts.TsBooleanKeyword { return {node: 'BooleanKeyword'}; } diff --git a/src/type/classes/ConstType.ts b/src/type/classes/ConstType.ts index 00cf13cb..6ae150f6 100644 --- a/src/type/classes/ConstType.ts +++ b/src/type/classes/ConstType.ts @@ -75,10 +75,6 @@ export class ConstType extends AbstractType> { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(maxEncodingCapacity(this.value())); - } - public toTypeScriptAst() { const value = this.schema.value; if (value === null) { diff --git a/src/type/classes/MapType.ts b/src/type/classes/MapType.ts index 673aa7e9..8527454f 100644 --- a/src/type/classes/MapType.ts +++ b/src/type/classes/MapType.ts @@ -141,30 +141,6 @@ export class MapType extends AbstractType { this.codegenBinaryEncoder(ctx, value); } - public codegenCapacityEstimator(ctx: CapacityEstimatorCodegenContext, value: JsExpression): void { - ctx.inc(MaxEncodingOverhead.Number); - } - public toTypeScriptAst(): ts.TsNumberKeyword { return {node: 'NumberKeyword'}; } diff --git a/src/type/classes/ObjectType.ts b/src/type/classes/ObjectType.ts index ff3dd195..96196d28 100644 --- a/src/type/classes/ObjectType.ts +++ b/src/type/classes/ObjectType.ts @@ -480,28 +480,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 toTypeScriptAst(): ts.TsTypeLiteral { const node: ts.TsTypeLiteral = { node: 'TypeLiteral', diff --git a/src/type/classes/OrType.ts b/src/type/classes/OrType.ts index c6dc1e9b..166be8e8 100644 --- a/src/type/classes/OrType.ts +++ b/src/type/classes/OrType.ts @@ -138,22 +138,6 @@ export class OrType extends AbstractType [ - index, - () => { - type.codegenCapacityEstimator(ctx, value); - }, - ]), - ); - } - public toTypeScriptAst(): ts.TsUnionType { const node: ts.TsUnionType = { node: 'UnionType', diff --git a/src/type/classes/RefType.ts b/src/type/classes/RefType.ts index 0c8ac6f4..5e3ddd23 100644 --- a/src/type/classes/RefType.ts +++ b/src/type/classes/RefType.ts @@ -118,14 +118,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 toTypeScriptAst(): ts.TsStringKeyword { return {node: 'StringKeyword'}; } diff --git a/src/type/classes/TupleType.ts b/src/type/classes/TupleType.ts index 5097c200..bd630503 100644 --- a/src/type/classes/TupleType.ts +++ b/src/type/classes/TupleType.ts @@ -132,23 +132,6 @@ export class TupleType extends AbstractType