@amritk/generate-examples
Advanced tools
@@ -34,2 +34,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const buildExampleSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>; | ||
| //# sourceMappingURL=build-schema.d.ts.map |
@@ -35,2 +35,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export {}; | ||
| //# sourceMappingURL=collect-example-imports.d.ts.map |
@@ -30,2 +30,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const generateExampleConst: (schema: JSONSchema, typeName: string, rootSchema?: Record<string, unknown>) => string; | ||
| //# sourceMappingURL=derive-example.d.ts.map |
@@ -23,2 +23,10 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'; | ||
| return '1970-01-01'; | ||
| case 'time': | ||
| return '00:00:00.000Z'; | ||
| case 'hostname': | ||
| return 'example.com'; | ||
| case 'ipv4': | ||
| return '127.0.0.1'; | ||
| case 'ipv6': | ||
| return '::1'; | ||
| } | ||
@@ -74,7 +82,14 @@ } | ||
| return deriveExample(schema.anyOf[0], rootSchema, seen); | ||
| // `hasType` only matches a single string `type`; multi-type schemas fall | ||
| // through to `null`. | ||
| if (!hasType(schema)) | ||
| return null; | ||
| switch (schema.type) { | ||
| if (hasType(schema)) | ||
| return deriveForType(schema.type, schema, rootSchema, seen); | ||
| // Multi-type schemas (`type: ['string', 'null']`) derive from their first | ||
| // member type; `hasType` only matches a single string `type`. | ||
| if (Array.isArray(schema.type) && schema.type.length > 0) { | ||
| return deriveForType(schema.type[0], schema, rootSchema, seen); | ||
| } | ||
| return null; | ||
| }; | ||
| /** Derives a canonical value for a single declared `type`. */ | ||
| const deriveForType = (type, schema, rootSchema, seen) => { | ||
| switch (type) { | ||
| case 'string': | ||
@@ -81,0 +96,0 @@ return exampleString(schema); |
@@ -12,2 +12,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export declare const generateArbitrary: (schema: JSONSchema, typeName: string, suffix?: string) => string; | ||
| //# sourceMappingURL=generate-arbitrary.d.ts.map |
@@ -24,2 +24,10 @@ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension'; | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))'; | ||
| case 'time': | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(11))'; | ||
| case 'hostname': | ||
| return 'fc.domain()'; | ||
| case 'ipv4': | ||
| return 'fc.ipV4()'; | ||
| case 'ipv6': | ||
| return 'fc.ipV6()'; | ||
| } | ||
@@ -148,6 +156,10 @@ } | ||
| return oneofExpr(schema.anyOf, suffix); | ||
| // `hasType` only matches a single string `type`; multi-type schemas | ||
| // (`type: ['string', 'null']`) fall through to the permissive fallback. | ||
| if (hasType(schema)) | ||
| return scalarExpr(schema.type, schema, suffix); | ||
| // Multi-type schemas (`type: ['string', 'null']`) become a oneof over each | ||
| // member type; `hasType` only matches a single string `type`. | ||
| if (Array.isArray(schema.type)) { | ||
| const exprs = schema.type.map((type) => scalarExpr(type, schema, suffix)); | ||
| return exprs.length === 1 ? exprs[0] : `fc.oneof(${exprs.join(', ')})`; | ||
| } | ||
| return 'fc.anything()'; | ||
@@ -154,0 +166,0 @@ }; |
@@ -43,2 +43,1 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| export {}; | ||
| //# sourceMappingURL=generate-files.d.ts.map |
+0
-1
@@ -5,2 +5,1 @@ export type { GeneratedFile } from './generators/build-schema.js'; | ||
| export { generateArbitrary } from './generators/generate-arbitrary.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
+2
-4
| { | ||
| "name": "@amritk/generate-examples", | ||
| "version": "0.2.2", | ||
| "version": "0.3.0", | ||
| "description": "Generate fast-check arbitraries and example values from JSON Schemas.", | ||
@@ -29,4 +29,3 @@ "module": "./dist/index.js", | ||
| "files": [ | ||
| "dist", | ||
| "src" | ||
| "dist" | ||
| ], | ||
@@ -46,3 +45,2 @@ "publishConfig": { | ||
| ".": { | ||
| "development": "./src/index.ts", | ||
| "default": "./dist/index.js", | ||
@@ -49,0 +47,0 @@ "types": "./dist/index.d.ts" |
+7
-5
@@ -123,9 +123,11 @@ <div align="center"> | ||
| `type` (string/number/integer/boolean/null/array/object), `properties`, | ||
| `type` — including multi-type unions like `['string', 'null']` — | ||
| (string/number/integer/boolean/null/array/object), `properties`, | ||
| `required`, `items`, `minItems`/`maxItems`, `uniqueItems`, | ||
| `minLength`/`maxLength`, `pattern`, `format` (`email`, `uuid`, `uri`/`url`, | ||
| `date`, `date-time`), `minimum`/`maximum`, `exclusiveMinimum`/`exclusiveMaximum`, | ||
| `multipleOf`, `enum`, `const`, `oneOf`/`anyOf`, `$ref`, and the `x-mjst` | ||
| extension (`Date`, `bigint`). Unsupported constructs degrade to `fc.anything()` | ||
| in arbitraries and `null` in static examples. | ||
| `date`, `date-time`, `time`, `hostname`, `ipv4`, `ipv6`), `minimum`/`maximum`, | ||
| `exclusiveMinimum`/`exclusiveMaximum`, `multipleOf`, `enum`, `const`, | ||
| `oneOf`/`anyOf`, `$ref`, and the `x-mjst` extension (`Date`, `bigint`). | ||
| Unsupported constructs degrade to `fc.anything()` in arbitraries and `null` in | ||
| static examples. | ||
@@ -132,0 +134,0 @@ > [!TIP] |
| {"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,eACjB,UAAU,gBACR,MAAM,0BAEnB,OAAO,CAAC,aAAa,EAAE,CAmBzB,CAAA"} |
| {"version":3,"file":"collect-example-imports.d.ts","sourceRoot":"","sources":["../../src/generators/collect-example-imports.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AA0DD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,WAAY,UAAU,YAAY,4BAA4B,KAAG,MAAM,EA0BxG,CAAA"} |
| {"version":3,"file":"derive-example.d.ts","sourceRoot":"","sources":["../../src/generators/derive-example.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAgCjE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,WAChB,UAAU,eACL,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,SAC9B,WAAW,CAAC,MAAM,CAAC,KACxB,OAuDF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,cAAc,UAAW,OAAO,KAAG,MAW/C,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,WACvB,UAAU,YACR,MAAM,eACH,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACnC,MAGF,CAAA"} |
| {"version":3,"file":"generate-arbitrary.d.ts","sourceRoot":"","sources":["../../src/generators/generate-arbitrary.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AA2JjE;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAGrF,CAAA"} |
| {"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAMjE;;GAEG;AACH,KAAK,0BAA0B,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,WACtB,UAAU,YACR,MAAM,YACN,0BAA0B,KACnC,MAsBF,CAAA"} |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAA;AACjG,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAA"} |
| import { describe, expect, it } from 'vitest' | ||
| import { buildExampleSchema, type GeneratedFile } from './build-schema' | ||
| /** Returns the content of a generated file, failing the test if it is absent. */ | ||
| const contentOf = (files: GeneratedFile[], filename: string): string => { | ||
| const file = files.find((f) => f.filename === filename) | ||
| if (!file) throw new Error(`expected a generated file named ${filename}`) | ||
| return file.content | ||
| } | ||
| describe('buildExampleSchema', () => { | ||
| it('emits a file per schema and an index barrel', async () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { name: { type: 'string' as const } }, | ||
| required: ['name'], | ||
| } | ||
| const files = await buildExampleSchema(schema, 'User') | ||
| const names = files.map((f) => f.filename) | ||
| expect(names).toContain('user.ts') | ||
| expect(names).toContain('index.ts') | ||
| const user = contentOf(files, 'user.ts') | ||
| expect(user).toContain("import * as fc from 'fast-check'") | ||
| expect(user).toContain('export type User =') | ||
| expect(user).toContain('export const UserArbitrary: fc.Arbitrary<User> =') | ||
| expect(user).toContain('export const userExample: User =') | ||
| }) | ||
| it('follows $refs into their own files and imports their arbitraries', async () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { address: { $ref: '#/$defs/address' } }, | ||
| required: ['address'], | ||
| $defs: { | ||
| address: { | ||
| type: 'object' as const, | ||
| properties: { city: { type: 'string' as const } }, | ||
| required: ['city'], | ||
| }, | ||
| }, | ||
| } | ||
| const files = await buildExampleSchema(schema, 'User') | ||
| const names = files.map((f) => f.filename) | ||
| expect(names).toContain('address.ts') | ||
| const user = contentOf(files, 'user.ts') | ||
| expect(user).toContain("import { type Address, AddressArbitrary } from './address'") | ||
| expect(user).toContain('"address": AddressArbitrary') | ||
| // The concrete example inlines the ref's value rather than referencing a const. | ||
| expect(user).toContain('"address": { "city": "string" }') | ||
| }) | ||
| it('re-exports types, arbitraries and examples from the index barrel', async () => { | ||
| const schema = { type: 'object' as const, properties: { id: { type: 'string' as const } } } | ||
| const files = await buildExampleSchema(schema, 'Thing') | ||
| const index = contentOf(files, 'index.ts') | ||
| expect(index).toContain("export { type Thing, ThingArbitrary, thingExample } from './thing';") | ||
| }) | ||
| }) |
| import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel' | ||
| import { walkRefGraph } from '@amritk/helpers/walk-ref-graph' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| import { generateExampleFile } from './generate-files' | ||
| /** | ||
| * Represents a generated TypeScript file with its filename and content. | ||
| */ | ||
| export type GeneratedFile = { | ||
| filename: string | ||
| content: string | ||
| } | ||
| /** | ||
| * Builds all TypeScript example files from a JSON Schema by traversing all | ||
| * `$ref` / `$dynamicRef` references recursively (via the shared | ||
| * `@amritk/helpers/walk-ref-graph` walker). | ||
| * | ||
| * Each generated file exports: | ||
| * - A TypeScript type definition | ||
| * - A `fast-check` arbitrary (`FooArbitrary`) that produces schema-valid values | ||
| * - A concrete example value (`fooExample`) | ||
| * | ||
| * An `index.ts` re-exports everything. The generated output imports `fast-check`, | ||
| * which consumers must install as a (dev) dependency. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to build from | ||
| * @param rootTypeName - The name for the root type (e.g. "Document") | ||
| * @param typeSuffix - Suffix appended to every `$ref`-derived name (default `''`) | ||
| * @returns An array of generated TypeScript files | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const files = await buildExampleSchema(schema, 'Document') | ||
| * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'index.ts', ... }] | ||
| * ``` | ||
| */ | ||
| export const buildExampleSchema = async ( | ||
| rootSchema: JSONSchema, | ||
| rootTypeName: string, | ||
| typeSuffix = '', | ||
| ): Promise<GeneratedFile[]> => { | ||
| const files: GeneratedFile[] = [] | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => { | ||
| // `index` is reserved for the barrel below, so never let a definition of | ||
| // that name overwrite it. | ||
| if (node.filename === 'index') return | ||
| const content = generateExampleFile(node.schema, node.typeName, { | ||
| rootSchema: node.rootSchema, | ||
| typeSuffix, | ||
| ...(node.ref !== undefined ? { selfRef: node.ref } : {}), | ||
| }) | ||
| files.push({ filename: `${node.filename}.ts`, content }) | ||
| }) | ||
| files.push({ filename: 'index.ts', content: generateIndexBarrel(files) }) | ||
| return files | ||
| } |
| import { refToFilename } from '@amritk/helpers/ref-to-filename' | ||
| import { refToName } from '@amritk/helpers/ref-to-name' | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref' | ||
| import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from '@amritk/helpers/schema-guards' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| /** | ||
| * Options for controlling how example imports are collected. | ||
| */ | ||
| type CollectExampleImportsOptions = { | ||
| /** | ||
| * The $ref path of the schema being generated (e.g. `#/$defs/address`). | ||
| * Prevents a file from importing itself. | ||
| */ | ||
| readonly selfRef?: string | undefined | ||
| /** | ||
| * The root schema document. URI refs that cannot be resolved within it | ||
| * are excluded from the import list (they were never generated as files). | ||
| */ | ||
| readonly rootSchema?: Record<string, unknown> | undefined | ||
| /** | ||
| * Suffix appended to every type/arbitrary name derived from a `$ref`. Must | ||
| * match the suffix used when generating the referenced files. Defaults to `''`. | ||
| */ | ||
| readonly typeSuffix?: string | ||
| } | ||
| /** | ||
| * Generates an import statement for a single $ref, importing both the generated | ||
| * type and its arbitrary from the ref's generated file. | ||
| */ | ||
| const buildImport = (ref: string, suffix: string): string => { | ||
| const filename = refToFilename(ref) | ||
| const typeName = refToName(ref, suffix) | ||
| return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'` | ||
| } | ||
| /** | ||
| * Walks one level of the schema and yields all direct $ref strings that should | ||
| * become imports: properties, additionalProperties, items, and union branches. | ||
| */ | ||
| const collectDirectRefs = (schema: JSONSchema): string[] => { | ||
| if (typeof schema === 'boolean' || schema === null) return [] | ||
| const refs: string[] = [] | ||
| if (hasRef(schema)) { | ||
| refs.push(schema.$ref) | ||
| return refs | ||
| } | ||
| const propSchemas = | ||
| 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null | ||
| ? Object.values(schema.properties as Record<string, JSONSchema>) | ||
| : [] | ||
| for (const prop of propSchemas) { | ||
| if (hasRef(prop)) refs.push((prop as { $ref: string }).$ref) | ||
| if (hasItems(prop) && hasRef(prop.items)) refs.push((prop.items as { $ref: string }).$ref) | ||
| if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties as JSONSchema)) { | ||
| refs.push((prop.additionalProperties as { $ref: string }).$ref) | ||
| } | ||
| } | ||
| if (hasItems(schema) && hasRef(schema.items)) { | ||
| refs.push((schema.items as { $ref: string }).$ref) | ||
| } | ||
| if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties as JSONSchema)) { | ||
| refs.push((schema.additionalProperties as { $ref: string }).$ref) | ||
| } | ||
| for (const branch of [ | ||
| ...(hasOneOf(schema) ? schema.oneOf : []), | ||
| ...(hasAnyOf(schema) ? schema.anyOf : []), | ||
| ...(hasAllOf(schema) ? schema.allOf : []), | ||
| ]) { | ||
| if (hasRef(branch)) refs.push((branch as { $ref: string }).$ref) | ||
| } | ||
| return refs | ||
| } | ||
| /** | ||
| * Collects import statements for all $ref dependencies of a schema. Each import | ||
| * brings in both the generated TypeScript type and the arbitrary for that ref. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { properties: { address: { $ref: '#/$defs/address' } } } | ||
| * collectExampleImports(schema) | ||
| * // ["import { type Address, AddressArbitrary } from './address'"] | ||
| * ``` | ||
| */ | ||
| export const collectExampleImports = (schema: JSONSchema, options?: CollectExampleImportsOptions): string[] => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null | ||
| const rootSchema = options?.rootSchema | ||
| const typeSuffix = options?.typeSuffix ?? '' | ||
| const refs = collectDirectRefs(schema) | ||
| const seen = new Set<string>() | ||
| const imports: string[] = [] | ||
| for (const ref of refs) { | ||
| const filename = refToFilename(ref) | ||
| if (seen.has(filename)) continue | ||
| if (selfFilename && filename === selfFilename) continue | ||
| // Skip refs that don't resolve in this schema (external / never generated) | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema) | ||
| if (!resolved) continue | ||
| } | ||
| seen.add(filename) | ||
| imports.push(buildImport(ref, typeSuffix)) | ||
| } | ||
| return imports | ||
| } |
| import { describe, expect, it } from 'vitest' | ||
| import { deriveExample, generateExampleConst, serializeValue } from './derive-example' | ||
| describe('deriveExample', () => { | ||
| it('prefers const, then examples, then default, then enum', () => { | ||
| expect(deriveExample({ const: 5 })).toBe(5) | ||
| expect(deriveExample({ examples: ['a', 'b'] } as never)).toBe('a') | ||
| expect(deriveExample({ default: true } as never)).toBe(true) | ||
| expect(deriveExample({ enum: ['x', 'y'] })).toBe('x') | ||
| }) | ||
| it('produces canonical values per type', () => { | ||
| expect(deriveExample({ type: 'string' })).toBe('string') | ||
| expect(deriveExample({ type: 'integer' })).toBe(0) | ||
| expect(deriveExample({ type: 'number', minimum: 3 })).toBe(3) | ||
| expect(deriveExample({ type: 'boolean' })).toBe(true) | ||
| expect(deriveExample({ type: 'null' })).toBe(null) | ||
| }) | ||
| it('honours string formats and length', () => { | ||
| expect(deriveExample({ type: 'string', format: 'email' })).toBe('user@example.com') | ||
| expect(deriveExample({ type: 'string', minLength: 10 })).toHaveLength(10) | ||
| }) | ||
| it('builds nested objects including all declared properties', () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { id: { type: 'string' as const }, count: { type: 'integer' as const } }, | ||
| required: ['id'], | ||
| } | ||
| expect(deriveExample(schema)).toEqual({ id: 'string', count: 0 }) | ||
| }) | ||
| it('builds arrays honouring minItems', () => { | ||
| expect(deriveExample({ type: 'array', items: { type: 'string' }, minItems: 2 })).toEqual(['string', 'string']) | ||
| }) | ||
| it('resolves $ref values against the root schema', () => { | ||
| const root = { $defs: { id: { type: 'string', const: 'abc' } } } | ||
| expect(deriveExample({ $ref: '#/$defs/id' }, root)).toBe('abc') | ||
| }) | ||
| it('short-circuits recursive $refs to null', () => { | ||
| const root = { | ||
| $defs: { node: { type: 'object', properties: { next: { $ref: '#/$defs/node' } } } }, | ||
| } | ||
| expect(deriveExample({ $ref: '#/$defs/node' }, root)).toEqual({ next: null }) | ||
| }) | ||
| }) | ||
| describe('serializeValue', () => { | ||
| it('serializes bigint and Date as runtime expressions', () => { | ||
| expect(serializeValue(0n)).toBe('0n') | ||
| expect(serializeValue(new Date(0))).toBe('new Date("1970-01-01T00:00:00.000Z")') | ||
| }) | ||
| it('omits undefined object properties', () => { | ||
| expect(serializeValue({ a: 1, b: undefined })).toBe('{ "a": 1 }') | ||
| }) | ||
| it('serializes nested arrays and objects', () => { | ||
| expect(serializeValue({ items: [1, 2] })).toBe('{ "items": [1, 2] }') | ||
| }) | ||
| }) | ||
| describe('generateExampleConst', () => { | ||
| it('emits a typed const with a derived value', () => { | ||
| const schema = { type: 'object' as const, properties: { name: { type: 'string' as const } } } | ||
| expect(generateExampleConst(schema, 'Info')).toBe('export const infoExample: Info = { "name": "string" }') | ||
| }) | ||
| }) |
| import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension' | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref' | ||
| import { | ||
| hasAnyOf, | ||
| hasConst, | ||
| hasDefault, | ||
| hasEnum, | ||
| hasExamples, | ||
| hasFormat, | ||
| hasItems, | ||
| hasMaxLength, | ||
| hasMinItems, | ||
| hasMinimum, | ||
| hasMinLength, | ||
| hasOneOf, | ||
| hasProperties, | ||
| hasRef, | ||
| hasType, | ||
| isSchemaObject, | ||
| } from '@amritk/helpers/schema-guards' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| /** Lowercases the first character of a name. e.g. "User" → "user" */ | ||
| const lowerFirst = (name: string): string => name.charAt(0).toLowerCase() + name.slice(1) | ||
| /** Derives the example const name from a type name. e.g. "User" → "userExample" */ | ||
| const exampleName = (typeName: string): string => `${lowerFirst(typeName)}Example` | ||
| /** Returns a representative string honouring `format` and length constraints. */ | ||
| const exampleString = (schema: JSONSchema): string => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case 'email': | ||
| return 'user@example.com' | ||
| case 'uuid': | ||
| return '00000000-0000-0000-0000-000000000000' | ||
| case 'uri': | ||
| case 'url': | ||
| return 'https://example.com' | ||
| case 'date-time': | ||
| return '1970-01-01T00:00:00.000Z' | ||
| case 'date': | ||
| return '1970-01-01' | ||
| } | ||
| } | ||
| let value = 'string' | ||
| if (hasMinLength(schema) && value.length < schema.minLength) value = value.padEnd(schema.minLength, 'x') | ||
| if (hasMaxLength(schema) && value.length > schema.maxLength) value = value.slice(0, schema.maxLength) | ||
| return value | ||
| } | ||
| /** | ||
| * Derives a single concrete, schema-valid value from a JSON Schema. | ||
| * | ||
| * Prefers explicit hints in this order: `const`, `examples[0]`, `default`, | ||
| * `enum[0]`; otherwise produces a canonical value for the declared type. | ||
| * `$ref`s are resolved and inlined by value; recursive refs short-circuit to | ||
| * `null` (tracked via `seen`). | ||
| * | ||
| * Note: values constrained only by `pattern` are not guaranteed to match the | ||
| * pattern — use the generated arbitrary when pattern fidelity matters. | ||
| */ | ||
| export const deriveExample = ( | ||
| schema: JSONSchema, | ||
| rootSchema?: Record<string, unknown>, | ||
| seen: ReadonlySet<string> = new Set(), | ||
| ): unknown => { | ||
| if (!isSchemaObject(schema)) return null | ||
| if (hasConst(schema)) return schema.const | ||
| if (hasExamples(schema) && Array.isArray(schema.examples) && schema.examples.length > 0) return schema.examples[0] | ||
| if (hasDefault(schema)) return schema.default | ||
| if (hasEnum(schema) && schema.enum.length > 0) return schema.enum[0] | ||
| if (hasRef(schema)) { | ||
| const ref = schema.$ref | ||
| if (seen.has(ref) || !rootSchema) return null | ||
| const resolved = resolveRef(ref, rootSchema) | ||
| if (!resolved) return null | ||
| return deriveExample(resolved as JSONSchema, rootSchema, new Set([...seen, ref])) | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema) | ||
| if (instanceOf === 'Date') return new Date(0) | ||
| const primitive = getMjstPrimitive(schema) | ||
| if (primitive === 'bigint') return 0n | ||
| if (hasOneOf(schema) && schema.oneOf[0] !== undefined) return deriveExample(schema.oneOf[0], rootSchema, seen) | ||
| if (hasAnyOf(schema) && schema.anyOf[0] !== undefined) return deriveExample(schema.anyOf[0], rootSchema, seen) | ||
| // `hasType` only matches a single string `type`; multi-type schemas fall | ||
| // through to `null`. | ||
| if (!hasType(schema)) return null | ||
| switch (schema.type) { | ||
| case 'string': | ||
| return exampleString(schema) | ||
| case 'number': | ||
| case 'integer': | ||
| return hasMinimum(schema) ? schema.minimum : 0 | ||
| case 'boolean': | ||
| return true | ||
| case 'null': | ||
| return null | ||
| case 'array': { | ||
| const item = hasItems(schema) ? deriveExample(schema.items, rootSchema, seen) : null | ||
| const count = hasMinItems(schema) ? Math.max(schema.minItems, 1) : 1 | ||
| return Array.from({ length: count }, () => item) | ||
| } | ||
| case 'object': { | ||
| const out: Record<string, unknown> = {} | ||
| if (hasProperties(schema)) { | ||
| for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
| out[key] = deriveExample(propSchema, rootSchema, seen) | ||
| } | ||
| } | ||
| return out | ||
| } | ||
| default: | ||
| return null | ||
| } | ||
| } | ||
| /** | ||
| * Serializes a derived value into a TypeScript source expression. Handles the | ||
| * non-JSON values `deriveExample` can produce (`Date`, `bigint`) in addition to | ||
| * plain JSON. | ||
| */ | ||
| export const serializeValue = (value: unknown): string => { | ||
| if (typeof value === 'bigint') return `${value}n` | ||
| if (value instanceof Date) return `new Date(${JSON.stringify(value.toISOString())})` | ||
| if (Array.isArray(value)) return `[${value.map(serializeValue).join(', ')}]` | ||
| if (value !== null && typeof value === 'object') { | ||
| const entries = Object.entries(value) | ||
| .filter(([, v]) => v !== undefined) | ||
| .map(([key, v]) => `${JSON.stringify(key)}: ${serializeValue(v)}`) | ||
| return `{ ${entries.join(', ')} }` | ||
| } | ||
| return JSON.stringify(value) | ||
| } | ||
| /** | ||
| * Generates an exported const holding a concrete, schema-valid example value. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * generateExampleConst({ type: 'object', properties: { name: { type: 'string' } } }, 'Info') | ||
| * // export const infoExample: Info = { "name": "string" } | ||
| * ``` | ||
| */ | ||
| export const generateExampleConst = ( | ||
| schema: JSONSchema, | ||
| typeName: string, | ||
| rootSchema?: Record<string, unknown>, | ||
| ): string => { | ||
| const value = deriveExample(schema, rootSchema) | ||
| return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}` | ||
| } |
| import { describe, expect, it } from 'vitest' | ||
| import { generateArbitrary } from './generate-arbitrary' | ||
| describe('generate-arbitrary', () => { | ||
| it('generates a record arbitrary for an object schema', () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { name: { type: 'string' as const }, age: { type: 'integer' as const } }, | ||
| required: ['name'], | ||
| } | ||
| const code = generateArbitrary(schema, 'User') | ||
| expect(code).toContain('export const UserArbitrary: fc.Arbitrary<User> =') | ||
| expect(code).toContain('fc.record(') | ||
| expect(code).toContain('"name": fc.string()') | ||
| expect(code).toContain('"age": fc.integer()') | ||
| expect(code).toContain('requiredKeys: ["name"]') | ||
| }) | ||
| it('omits requiredKeys when every property is required', () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { id: { type: 'string' as const } }, | ||
| required: ['id'], | ||
| } | ||
| const code = generateArbitrary(schema, 'Doc') | ||
| expect(code).toContain('fc.record({ "id": fc.string() })') | ||
| expect(code).not.toContain('requiredKeys') | ||
| }) | ||
| it('honours string length constraints', () => { | ||
| const schema = { type: 'string' as const, minLength: 2, maxLength: 8 } | ||
| expect(generateArbitrary(schema, 'Code')).toContain('fc.string({ minLength: 2, maxLength: 8 })') | ||
| }) | ||
| it('maps string formats to dedicated arbitraries', () => { | ||
| expect(generateArbitrary({ type: 'string', format: 'email' }, 'E')).toContain('fc.emailAddress()') | ||
| expect(generateArbitrary({ type: 'string', format: 'uuid' }, 'U')).toContain('fc.uuid()') | ||
| expect(generateArbitrary({ type: 'string', format: 'date-time' }, 'D')).toContain( | ||
| 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())', | ||
| ) | ||
| }) | ||
| it('uses stringMatching for pattern constraints', () => { | ||
| const schema = { type: 'string' as const, pattern: '^[a-z]+$' } | ||
| expect(generateArbitrary(schema, 'Slug')).toContain('fc.stringMatching(/^[a-z]+$/)') | ||
| }) | ||
| it('honours integer range and multipleOf', () => { | ||
| const schema = { type: 'integer' as const, minimum: 0, maximum: 10, multipleOf: 2 } | ||
| const code = generateArbitrary(schema, 'Even') | ||
| expect(code).toContain('fc.integer({ min: 0, max: 10 })') | ||
| expect(code).toContain('.filter((n) => n % 2 === 0)') | ||
| }) | ||
| it('adjusts exclusive integer bounds', () => { | ||
| const schema = { type: 'integer' as const, exclusiveMinimum: 0, exclusiveMaximum: 10 } | ||
| expect(generateArbitrary(schema, 'N')).toContain('fc.integer({ min: 1, max: 9 })') | ||
| }) | ||
| it('uses excluded bounds for numbers', () => { | ||
| const schema = { type: 'number' as const, exclusiveMinimum: 0, maximum: 1 } | ||
| const code = generateArbitrary(schema, 'Ratio') | ||
| expect(code).toContain('min: 0, minExcluded: true') | ||
| expect(code).toContain('max: 1') | ||
| }) | ||
| it('generates fc.constantFrom for enums and fc.constant for const', () => { | ||
| expect(generateArbitrary({ enum: ['a', 'b'] }, 'Choice')).toContain('fc.constantFrom("a", "b")') | ||
| expect(generateArbitrary({ const: 42 }, 'Answer')).toContain('fc.constant(42)') | ||
| }) | ||
| it('references the imported arbitrary for $ref', () => { | ||
| const schema = { | ||
| type: 'object' as const, | ||
| properties: { address: { $ref: '#/$defs/address' } }, | ||
| required: ['address'], | ||
| } | ||
| expect(generateArbitrary(schema, 'User')).toContain('"address": AddressArbitrary') | ||
| }) | ||
| it('generates fc.array with bounds and uniqueArray for unique items', () => { | ||
| expect(generateArbitrary({ type: 'array', items: { type: 'string' }, minItems: 1 }, 'List')).toContain( | ||
| 'fc.array(fc.string(), { minLength: 1 })', | ||
| ) | ||
| expect(generateArbitrary({ type: 'array', items: { type: 'number' }, uniqueItems: true }, 'Set')).toContain( | ||
| 'fc.uniqueArray(fc.double', | ||
| ) | ||
| }) | ||
| it('generates fc.oneof for unions', () => { | ||
| const schema = { oneOf: [{ type: 'string' as const }, { type: 'number' as const }] } | ||
| expect(generateArbitrary(schema, 'StringOrNumber')).toContain('fc.oneof(fc.string(), fc.double(') | ||
| }) | ||
| it('maps x-mjst Date and bigint to fc.date and fc.bigInt', () => { | ||
| expect(generateArbitrary({ 'x-mjst': { instanceOf: 'Date' } } as never, 'When')).toContain('fc.date(') | ||
| expect(generateArbitrary({ 'x-mjst': { primitive: 'bigint' } } as never, 'Big')).toContain('fc.bigInt()') | ||
| }) | ||
| }) |
| import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension' | ||
| import { refToName } from '@amritk/helpers/ref-to-name' | ||
| import { | ||
| hasAnyOf, | ||
| hasConst, | ||
| hasEnum, | ||
| hasExclusiveMaximum, | ||
| hasExclusiveMinimum, | ||
| hasFormat, | ||
| hasItems, | ||
| hasMaxItems, | ||
| hasMaximum, | ||
| hasMaxLength, | ||
| hasMinItems, | ||
| hasMinimum, | ||
| hasMinLength, | ||
| hasMultipleOf, | ||
| hasOneOf, | ||
| hasPattern, | ||
| hasProperties, | ||
| hasRef, | ||
| hasRequired, | ||
| hasType, | ||
| hasUniqueItems, | ||
| isSchemaObject, | ||
| } from '@amritk/helpers/schema-guards' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| /** | ||
| * Derives the arbitrary const name from a type name. | ||
| * e.g. "User" → "UserArbitrary" | ||
| */ | ||
| const arbitraryName = (typeName: string): string => `${typeName}Arbitrary` | ||
| /** Builds a `fc.string({ ... })` expression honouring format and length constraints. */ | ||
| const stringExpr = (schema: JSONSchema): string => { | ||
| if (hasFormat(schema)) { | ||
| switch (schema.format) { | ||
| case 'email': | ||
| return 'fc.emailAddress()' | ||
| case 'uuid': | ||
| return 'fc.uuid()' | ||
| case 'uri': | ||
| case 'url': | ||
| return 'fc.webUrl()' | ||
| case 'date-time': | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString())' | ||
| case 'date': | ||
| return 'fc.date({ noInvalidDate: true }).map((d) => d.toISOString().slice(0, 10))' | ||
| } | ||
| } | ||
| if (hasPattern(schema)) return `fc.stringMatching(/${schema.pattern}/)` | ||
| const opts: string[] = [] | ||
| if (hasMinLength(schema)) opts.push(`minLength: ${schema.minLength}`) | ||
| if (hasMaxLength(schema)) opts.push(`maxLength: ${schema.maxLength}`) | ||
| return opts.length > 0 ? `fc.string({ ${opts.join(', ')} })` : 'fc.string()' | ||
| } | ||
| /** Builds a `fc.integer({ ... })` expression honouring range and multiple-of constraints. */ | ||
| const integerExpr = (schema: JSONSchema): string => { | ||
| const opts: string[] = [] | ||
| if (hasMinimum(schema)) opts.push(`min: ${schema.minimum}`) | ||
| else if (hasExclusiveMinimum(schema)) opts.push(`min: ${Number(schema.exclusiveMinimum) + 1}`) | ||
| if (hasMaximum(schema)) opts.push(`max: ${schema.maximum}`) | ||
| else if (hasExclusiveMaximum(schema)) opts.push(`max: ${Number(schema.exclusiveMaximum) - 1}`) | ||
| const base = opts.length > 0 ? `fc.integer({ ${opts.join(', ')} })` : 'fc.integer()' | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base | ||
| } | ||
| /** Builds a `fc.double({ ... })` expression honouring range and multiple-of constraints. */ | ||
| const numberExpr = (schema: JSONSchema): string => { | ||
| const opts: string[] = ['noNaN: true', 'noDefaultInfinity: true'] | ||
| if (hasMinimum(schema)) opts.push(`min: ${schema.minimum}`) | ||
| else if (hasExclusiveMinimum(schema)) opts.push(`min: ${schema.exclusiveMinimum}`, 'minExcluded: true') | ||
| if (hasMaximum(schema)) opts.push(`max: ${schema.maximum}`) | ||
| else if (hasExclusiveMaximum(schema)) opts.push(`max: ${schema.exclusiveMaximum}`, 'maxExcluded: true') | ||
| const base = `fc.double({ ${opts.join(', ')} })` | ||
| return hasMultipleOf(schema) ? `${base}.filter((n) => n % ${schema.multipleOf} === 0)` : base | ||
| } | ||
| /** Builds a `fc.array(...)` / `fc.uniqueArray(...)` expression for an array schema. */ | ||
| const arrayExpr = (schema: JSONSchema, suffix: string): string => { | ||
| const items = hasItems(schema) && isSchemaObject(schema.items) ? arbitraryExpr(schema.items, suffix) : 'fc.anything()' | ||
| const opts: string[] = [] | ||
| if (hasMinItems(schema)) opts.push(`minLength: ${schema.minItems}`) | ||
| if (hasMaxItems(schema)) opts.push(`maxLength: ${schema.maxItems}`) | ||
| const fn = hasUniqueItems(schema) && schema.uniqueItems === true ? 'fc.uniqueArray' : 'fc.array' | ||
| return opts.length > 0 ? `${fn}(${items}, { ${opts.join(', ')} })` : `${fn}(${items})` | ||
| } | ||
| /** Builds a `fc.record(...)` expression for an object schema. */ | ||
| const objectExpr = (schema: JSONSchema, suffix: string): string => { | ||
| if (!hasProperties(schema)) return 'fc.object()' | ||
| const required = new Set(hasRequired(schema) ? schema.required : []) | ||
| const keys = Object.keys(schema.properties) | ||
| const entries = Object.entries(schema.properties).map( | ||
| ([key, propSchema]) => `${JSON.stringify(key)}: ${arbitraryExpr(propSchema, suffix)}`, | ||
| ) | ||
| if (entries.length === 0) return 'fc.record({})' | ||
| const model = `{ ${entries.join(', ')} }` | ||
| // fc.record treats all keys as required by default. Only emit requiredKeys | ||
| // when at least one property is optional. | ||
| if (keys.every((key) => required.has(key))) return `fc.record(${model})` | ||
| const requiredKeys = [...required].map((key) => JSON.stringify(key)).join(', ') | ||
| return `fc.record(${model}, { requiredKeys: [${requiredKeys}] })` | ||
| } | ||
| /** Builds a `fc.oneof(...)` expression from a list of branch schemas. */ | ||
| const oneofExpr = (branches: readonly JSONSchema[], suffix: string): string => { | ||
| const exprs = branches.map((branch) => arbitraryExpr(branch, suffix)) | ||
| return `fc.oneof(${exprs.join(', ')})` | ||
| } | ||
| /** Builds the fast-check expression for a single (non-union) JSON Schema type. */ | ||
| const scalarExpr = (type: string, schema: JSONSchema, suffix: string): string => { | ||
| switch (type) { | ||
| case 'string': | ||
| return stringExpr(schema) | ||
| case 'integer': | ||
| return integerExpr(schema) | ||
| case 'number': | ||
| return numberExpr(schema) | ||
| case 'boolean': | ||
| return 'fc.boolean()' | ||
| case 'null': | ||
| return 'fc.constant(null)' | ||
| case 'array': | ||
| return arrayExpr(schema, suffix) | ||
| case 'object': | ||
| return objectExpr(schema, suffix) | ||
| default: | ||
| return 'fc.anything()' | ||
| } | ||
| } | ||
| /** | ||
| * Recursively builds the fast-check arbitrary expression for a schema node. | ||
| * `$ref`s resolve to the referenced file's exported arbitrary; everything else | ||
| * maps to the appropriate `fc.*` combinator. | ||
| */ | ||
| const arbitraryExpr = (schema: JSONSchema, suffix: string): string => { | ||
| if (!isSchemaObject(schema)) return 'fc.anything()' | ||
| if (hasRef(schema)) return arbitraryName(refToName(schema.$ref, suffix)) | ||
| if (hasConst(schema)) return `fc.constant(${JSON.stringify(schema.const)})` | ||
| if (hasEnum(schema)) { | ||
| const values = (schema.enum as unknown[]).map((value) => JSON.stringify(value)).join(', ') | ||
| return `fc.constantFrom(${values})` | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema) | ||
| if (instanceOf === 'Date') return 'fc.date({ noInvalidDate: true })' | ||
| if (instanceOf) return 'fc.anything()' | ||
| const primitive = getMjstPrimitive(schema) | ||
| if (primitive === 'bigint') return 'fc.bigInt()' | ||
| if (primitive) return 'fc.anything()' | ||
| if (hasOneOf(schema)) return oneofExpr(schema.oneOf, suffix) | ||
| if (hasAnyOf(schema)) return oneofExpr(schema.anyOf, suffix) | ||
| // `hasType` only matches a single string `type`; multi-type schemas | ||
| // (`type: ['string', 'null']`) fall through to the permissive fallback. | ||
| if (hasType(schema)) return scalarExpr(schema.type, schema, suffix) | ||
| return 'fc.anything()' | ||
| } | ||
| /** | ||
| * Generates a `fast-check` arbitrary that produces schema-valid values. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * generateArbitrary({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info') | ||
| * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "name": fc.string() }) | ||
| * ``` | ||
| */ | ||
| export const generateArbitrary = (schema: JSONSchema, typeName: string, suffix = ''): string => { | ||
| const expr = arbitraryExpr(schema, suffix) | ||
| return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}` | ||
| } |
| import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition' | ||
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12' | ||
| import { collectExampleImports } from './collect-example-imports' | ||
| import { generateExampleConst } from './derive-example' | ||
| import { generateArbitrary } from './generate-arbitrary' | ||
| /** | ||
| * Options for controlling what gets generated in an example file. | ||
| */ | ||
| type GenerateExampleFileOptions = { | ||
| /** | ||
| * The $ref path of the schema being generated (e.g. `#/$defs/address`). | ||
| * Prevents the file from importing itself. | ||
| */ | ||
| readonly selfRef?: string | ||
| /** | ||
| * The root schema document. Used to resolve `$ref`s when deriving a concrete | ||
| * example value, and to filter out unresolvable refs from the import list. | ||
| */ | ||
| readonly rootSchema?: Record<string, unknown> | ||
| /** | ||
| * Suffix appended to every type/arbitrary name derived from a `$ref`. | ||
| * Defaults to `''` (no suffix). | ||
| */ | ||
| readonly typeSuffix?: string | ||
| } | ||
| /** | ||
| * Generates a complete TypeScript example file from a JSON Schema. | ||
| * | ||
| * The file contains: | ||
| * - An import of `fast-check` and imports for any `$ref` types and arbitraries | ||
| * - The exported TypeScript type definition | ||
| * - An exported `fast-check` arbitrary (`FooArbitrary`) | ||
| * - An exported concrete example value (`fooExample`) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] } | ||
| * generateExampleFile(schema, 'Info') | ||
| * // import * as fc from 'fast-check' | ||
| * // export type Info = { title: string } | ||
| * // export const InfoArbitrary: fc.Arbitrary<Info> = fc.record({ "title": fc.string() }) | ||
| * // export const infoExample: Info = { "title": "string" } | ||
| * ``` | ||
| */ | ||
| export const generateExampleFile = ( | ||
| schema: JSONSchema, | ||
| typeName: string, | ||
| options?: GenerateExampleFileOptions, | ||
| ): string => { | ||
| const typeSuffix = options?.typeSuffix ?? '' | ||
| const refImports = collectExampleImports(schema, { | ||
| selfRef: options?.selfRef, | ||
| rootSchema: options?.rootSchema, | ||
| typeSuffix, | ||
| }) | ||
| const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix }) | ||
| const arbitrary = generateArbitrary(schema, typeName, typeSuffix) | ||
| const example = generateExampleConst(schema, typeName, options?.rootSchema) | ||
| let result = `import * as fc from 'fast-check'\n` | ||
| for (const imp of refImports) { | ||
| result += imp + '\n' | ||
| } | ||
| result += '\n' | ||
| result += typeDefinition + '\n\n' + arbitrary + '\n\n' + example + '\n' | ||
| return result | ||
| } |
| export type { GeneratedFile } from './generators/build-schema' | ||
| export { buildExampleSchema } from './generators/build-schema' | ||
| export { deriveExample, generateExampleConst, serializeValue } from './generators/derive-example' | ||
| export { generateArbitrary } from './generators/generate-arbitrary' |
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
142
1.43%33823
-49.51%14
-51.72%659
-51.93%