@amritk/generate-examples
Advanced tools
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * 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 references recursively, mirroring the generate-parsers pipeline. | ||
| * | ||
| * 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 declare const buildExampleSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>; |
| 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; | ||
| }; | ||
| /** | ||
| * 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 declare const collectExampleImports: (schema: JSONSchema, options?: CollectExampleImportsOptions) => string[]; | ||
| export {}; |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * 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 declare const deriveExample: (schema: JSONSchema, rootSchema?: Record<string, unknown>, seen?: ReadonlySet<string>) => unknown; | ||
| /** | ||
| * 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 declare const serializeValue: (value: unknown) => string; | ||
| /** | ||
| * 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 declare const generateExampleConst: (schema: JSONSchema, typeName: string, rootSchema?: Record<string, unknown>) => string; |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * 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 declare const generateArbitrary: (schema: JSONSchema, typeName: string, suffix?: string) => string; |
| import type { JSONSchema } from 'json-schema-typed/draft-2020-12'; | ||
| /** | ||
| * 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 declare const generateExampleFile: (schema: JSONSchema, typeName: string, options?: GenerateExampleFileOptions) => string; | ||
| export {}; |
| 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'; |
+1353
| // ../helpers/dist/build-dynamic-ref-map.js | ||
| var isSchemaObject = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var buildDynamicRefMap = (rootSchema) => { | ||
| const map = {}; | ||
| if (!isSchemaObject(rootSchema) || !("$defs" in rootSchema)) { | ||
| return map; | ||
| } | ||
| const defs = rootSchema.$defs; | ||
| for (const [key, value] of Object.entries(defs)) { | ||
| if (typeof value === "object" && value !== null && "$dynamicAnchor" in value && typeof value["$dynamicAnchor"] === "string") { | ||
| const anchor = value["$dynamicAnchor"]; | ||
| map[`#${anchor}`] = `#/$defs/${key}`; | ||
| } | ||
| } | ||
| return map; | ||
| }; | ||
| // ../helpers/dist/extract-refs.js | ||
| var isResolvableRef = (ref) => { | ||
| if (ref === "#") | ||
| return false; | ||
| if (ref.startsWith("#")) | ||
| return true; | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) | ||
| return true; | ||
| return false; | ||
| }; | ||
| var extractRefs = (schema) => { | ||
| const refs = new Set; | ||
| const traverse = (obj) => { | ||
| if (typeof obj !== "object" || obj === null) { | ||
| return; | ||
| } | ||
| if (Array.isArray(obj)) { | ||
| for (const item of obj) { | ||
| traverse(item); | ||
| } | ||
| return; | ||
| } | ||
| const record = obj; | ||
| if ("$ref" in record && typeof record["$ref"] === "string" && isResolvableRef(record["$ref"])) { | ||
| refs.add(record["$ref"]); | ||
| } | ||
| for (const key in record) { | ||
| traverse(record[key]); | ||
| } | ||
| }; | ||
| traverse(schema); | ||
| return refs; | ||
| }; | ||
| // ../helpers/dist/ref-to-filename.js | ||
| var toKebabCase = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase(fragLast)}`; | ||
| }; | ||
| var refToFilename = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| // ../helpers/dist/ref-to-name.js | ||
| var toKebabCase2 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename2 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase2(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase2(fragLast)}`; | ||
| }; | ||
| var refToFilename2 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename2(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase2(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var kebabToPascal = (kebab, suffix) => { | ||
| const words = kebab.split("-"); | ||
| let pascalCase = ""; | ||
| for (const word of words) { | ||
| pascalCase += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| return pascalCase + suffix; | ||
| }; | ||
| var refToName = (ref, suffix = "") => kebabToPascal(refToFilename2(ref), suffix); | ||
| // ../helpers/dist/resolve-dynamic-refs.js | ||
| var resolveDynamicRefs = (schema, dynamicRefMap) => { | ||
| if (typeof schema !== "object" || schema === null) { | ||
| return schema; | ||
| } | ||
| if (Object.keys(dynamicRefMap).length === 0) { | ||
| return schema; | ||
| } | ||
| const clone = JSON.parse(JSON.stringify(schema)); | ||
| const walk = (obj) => { | ||
| if (typeof obj !== "object" || obj === null || Array.isArray(obj)) { | ||
| return; | ||
| } | ||
| const record = obj; | ||
| if ("$dynamicRef" in record && typeof record["$dynamicRef"] === "string") { | ||
| const resolved = dynamicRefMap[record["$dynamicRef"]]; | ||
| if (resolved) { | ||
| record["$ref"] = resolved; | ||
| delete record["$dynamicRef"]; | ||
| } | ||
| } | ||
| for (const key in record) { | ||
| walk(record[key]); | ||
| } | ||
| }; | ||
| walk(clone); | ||
| return clone; | ||
| }; | ||
| // ../helpers/dist/resolve-ref.js | ||
| var navigatePointer = (pointer, schema) => { | ||
| const parts = pointer.split("/").filter(Boolean); | ||
| let current = schema; | ||
| for (const part of parts) { | ||
| const decodedPart = part.replace(/~1/g, "/").replace(/~0/g, "~"); | ||
| if (current && typeof current === "object" && decodedPart in current) { | ||
| const next = current[decodedPart]; | ||
| if (typeof next === "object" && next !== null) { | ||
| current = next; | ||
| } else { | ||
| return; | ||
| } | ||
| } else { | ||
| return; | ||
| } | ||
| } | ||
| return current; | ||
| }; | ||
| var resolveRef = (ref, rootSchema) => { | ||
| if (ref.startsWith("#")) { | ||
| return navigatePointer(ref.slice(1), rootSchema); | ||
| } | ||
| const hashIndex = ref.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? ref : ref.slice(0, hashIndex); | ||
| const rawFragment = hashIndex === -1 ? "" : ref.slice(hashIndex + 1); | ||
| const fragment = rawFragment === "" || rawFragment === "/" ? "" : rawFragment; | ||
| const defs = rootSchema["$defs"]; | ||
| if (typeof defs !== "object" || defs === null) | ||
| return; | ||
| const defsRecord = defs; | ||
| const base = defsRecord[baseUri]; | ||
| if (typeof base !== "object" || base === null) | ||
| return; | ||
| if (!fragment) | ||
| return base; | ||
| const normalizedFragment = fragment.replace(/^\/definitions\//, "/$defs/"); | ||
| return navigatePointer(normalizedFragment, base); | ||
| }; | ||
| // ../helpers/dist/upgrade-draft07-schema.js | ||
| var toKebabCase3 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename3 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase3(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase3(fragLast)}`; | ||
| }; | ||
| var refToFilename3 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename3(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase3(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var isDraft07Schema = (schema) => typeof schema["$schema"] === "string" && schema["$schema"].includes("draft-07"); | ||
| var rewriteRefs = (obj, refMap, selfRef) => { | ||
| if (typeof obj !== "object" || obj === null) | ||
| return obj; | ||
| if (Array.isArray(obj)) | ||
| return obj.map((item) => rewriteRefs(item, refMap, selfRef)); | ||
| const record = obj; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (key === "$ref" && typeof value === "string") { | ||
| if (refMap.has(value)) { | ||
| result[key] = refMap.get(value); | ||
| } else if (value === "#" && selfRef) { | ||
| result[key] = selfRef; | ||
| } else { | ||
| result[key] = value; | ||
| } | ||
| } else { | ||
| result[key] = rewriteRefs(value, refMap, selfRef); | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| var hoistNestedDefs = (defs) => { | ||
| const hoisted = {}; | ||
| for (const [parentName, parentSchema] of Object.entries(defs)) { | ||
| if (typeof parentSchema !== "object" || parentSchema === null) { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| } | ||
| const parentObj = parentSchema; | ||
| const nestedDefs = parentObj["$defs"]; | ||
| if (!nestedDefs || typeof nestedDefs !== "object") { | ||
| hoisted[parentName] = parentSchema; | ||
| continue; | ||
| } | ||
| const parentPrefix = parentName.startsWith("http://") || parentName.startsWith("https://") ? refToFilename3(parentName) : parentName; | ||
| const localToHoisted = new Map; | ||
| for (const localName of Object.keys(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase3(localName)}`; | ||
| localToHoisted.set(`#/$defs/${localName}`, `#/$defs/${hoistedName}`); | ||
| } | ||
| const selfRef = `#/$defs/${parentPrefix}`; | ||
| const rewrittenParent = rewriteRefs(parentObj, localToHoisted, selfRef); | ||
| hoisted[parentName] = rewrittenParent; | ||
| for (const [localName, localSchema] of Object.entries(nestedDefs)) { | ||
| const hoistedName = `${parentPrefix}-${toKebabCase3(localName)}`; | ||
| hoisted[hoistedName] = rewriteRefs(localSchema, localToHoisted, selfRef); | ||
| } | ||
| } | ||
| return hoisted; | ||
| }; | ||
| var upgradeDraft07Schema = (schema) => { | ||
| if (!isDraft07Schema(schema)) | ||
| return schema; | ||
| const { definitions, $schema: _, ...rest } = schema; | ||
| const rawDefs = definitions ?? {}; | ||
| const renamedDefs = {}; | ||
| for (const [key, value] of Object.entries(rawDefs)) { | ||
| renamedDefs[key] = renameNestedDefs(value); | ||
| } | ||
| const hoistedDefs = hoistNestedDefs(renamedDefs); | ||
| for (const key of Object.keys(hoistedDefs)) { | ||
| if (key.startsWith("http://") || key.startsWith("https://")) { | ||
| const shortName = refToFilename3(key); | ||
| if (shortName && !(shortName in hoistedDefs)) { | ||
| hoistedDefs[shortName] = hoistedDefs[key]; | ||
| } | ||
| } | ||
| } | ||
| return { | ||
| ...rest, | ||
| $defs: hoistedDefs | ||
| }; | ||
| }; | ||
| var renameNestedDefs = (obj) => { | ||
| if (typeof obj !== "object" || obj === null) | ||
| return obj; | ||
| if (Array.isArray(obj)) | ||
| return obj.map(renameNestedDefs); | ||
| const record = obj; | ||
| const result = {}; | ||
| for (const [key, value] of Object.entries(record)) { | ||
| if (key === "$ref" && typeof value === "string" && value.startsWith("#/definitions/")) { | ||
| result[key] = value.replace("#/definitions/", "#/$defs/"); | ||
| } else { | ||
| const outKey = key === "definitions" ? "$defs" : key; | ||
| result[outKey] = renameNestedDefs(value); | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| // ../helpers/dist/generate-type-definition.js | ||
| var isSchemaObject2 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var isObjectSchema = (schema) => { | ||
| return isSchemaObject2(schema) && (("type" in schema) && schema.type === "object" || ("properties" in schema)); | ||
| }; | ||
| var MJST_EXTENSION_KEY = "x-mjst"; | ||
| var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/; | ||
| var SUPPORTED_PRIMITIVES = new Set(["bigint"]); | ||
| var SAFE_BRAND = /^[\w$ -]+$/; | ||
| var readExtensionString = (schema, field) => { | ||
| if (!isSchemaObject2(schema)) | ||
| return; | ||
| const extension = schema[MJST_EXTENSION_KEY]; | ||
| if (typeof extension !== "object" || extension === null) | ||
| return; | ||
| const value = extension[field]; | ||
| return typeof value === "string" ? value : undefined; | ||
| }; | ||
| var getMjstInstanceOf = (schema) => { | ||
| const instanceOf = readExtensionString(schema, "instanceOf"); | ||
| return instanceOf !== undefined && IDENTIFIER.test(instanceOf) ? instanceOf : undefined; | ||
| }; | ||
| var getMjstPrimitive = (schema) => { | ||
| const primitive = readExtensionString(schema, "primitive"); | ||
| return primitive !== undefined && SUPPORTED_PRIMITIVES.has(primitive) ? primitive : undefined; | ||
| }; | ||
| var getMjstBrand = (schema) => { | ||
| const brand = readExtensionString(schema, "brand"); | ||
| return brand !== undefined && SAFE_BRAND.test(brand) ? brand : undefined; | ||
| }; | ||
| var toKebabCase4 = (value) => value.replace(/OAuth/g, "Oauth").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase(); | ||
| var uriRefToFilename4 = (uri) => { | ||
| const hashIndex = uri.indexOf("#"); | ||
| const baseUri = hashIndex === -1 ? uri : uri.slice(0, hashIndex); | ||
| const fragment = hashIndex === -1 ? "" : uri.slice(hashIndex + 1); | ||
| const withoutProtocol = baseUri.replace(/^https?:\/\/[^/]+\//, ""); | ||
| const withoutExt = withoutProtocol.replace(/\.json$/, ""); | ||
| const rawSegments = withoutExt.split("/"); | ||
| const SKIP_KEYS = new Set(["definitions", "$defs"]); | ||
| const segments = []; | ||
| for (let i = 0;i < rawSegments.length; i++) { | ||
| const s = rawSegments[i]; | ||
| if (SKIP_KEYS.has(s)) | ||
| continue; | ||
| const prevRaw = rawSegments[i - 1]; | ||
| if (/^\d+\.\d+/.test(s) && prevRaw !== undefined && SKIP_KEYS.has(prevRaw)) | ||
| continue; | ||
| segments.push(s); | ||
| } | ||
| const baseName = segments.map((s) => toKebabCase4(s).replace(/\./g, "-")).join("-"); | ||
| if (!fragment) | ||
| return baseName; | ||
| const fragSegments = fragment.split("/").filter((s) => s && !SKIP_KEYS.has(s) && s !== "properties"); | ||
| const fragLast = fragSegments[fragSegments.length - 1]; | ||
| if (!fragLast) | ||
| return baseName; | ||
| return `${baseName}-${toKebabCase4(fragLast)}`; | ||
| }; | ||
| var refToFilename4 = (ref) => { | ||
| if (ref.startsWith("http://") || ref.startsWith("https://")) { | ||
| return uriRefToFilename4(ref); | ||
| } | ||
| const segments = ref.split("/"); | ||
| let filename = segments[segments.length - 1]; | ||
| if (/[A-Z]/.test(filename)) { | ||
| filename = toKebabCase4(filename); | ||
| } | ||
| return filename; | ||
| }; | ||
| var kebabToPascal2 = (kebab, suffix) => { | ||
| const words = kebab.split("-"); | ||
| let pascalCase = ""; | ||
| for (const word of words) { | ||
| pascalCase += word.charAt(0).toUpperCase() + word.slice(1); | ||
| } | ||
| return pascalCase + suffix; | ||
| }; | ||
| var refToName2 = (ref, suffix = "") => kebabToPascal2(refToFilename4(ref), suffix); | ||
| var JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; | ||
| var safeKey = (key) => { | ||
| if (JS_IDENTIFIER.test(key)) { | ||
| return key; | ||
| } | ||
| return `'${key}'`; | ||
| }; | ||
| var getConditionalObjectSchema = (schema) => { | ||
| if (!isSchemaObject2(schema)) { | ||
| return null; | ||
| } | ||
| if (!("if" in schema) || !("then" in schema)) { | ||
| return null; | ||
| } | ||
| const ifSchema = schema.if; | ||
| const thenSchema = schema.then; | ||
| if (!isSchemaObject2(ifSchema) || !isSchemaObject2(thenSchema)) { | ||
| return null; | ||
| } | ||
| const ifProperties = ifSchema.properties; | ||
| const thenProperties = thenSchema.properties; | ||
| const hasIfProperties = ifProperties && typeof ifProperties === "object"; | ||
| const hasThenProperties = thenProperties && typeof thenProperties === "object"; | ||
| if (!hasIfProperties && !hasThenProperties) { | ||
| return null; | ||
| } | ||
| const properties = { | ||
| ...hasIfProperties ? ifProperties : {}, | ||
| ...hasThenProperties ? thenProperties : {} | ||
| }; | ||
| const required = new Set; | ||
| if (Array.isArray(ifSchema.required)) { | ||
| for (const key of ifSchema.required) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (hasIfProperties) { | ||
| for (const key in ifProperties) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (Array.isArray(thenSchema.required)) { | ||
| for (const key of thenSchema.required) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| if (hasThenProperties) { | ||
| for (const key in thenProperties) { | ||
| required.add(key); | ||
| } | ||
| } | ||
| const thenRef = typeof thenSchema.$ref === "string" ? thenSchema.$ref : null; | ||
| return { | ||
| schema: { | ||
| type: "object", | ||
| properties, | ||
| ...required.size > 0 ? { required: Array.from(required) } : {} | ||
| }, | ||
| thenRef | ||
| }; | ||
| }; | ||
| var isObjectLikeSchema = (schema) => { | ||
| if (!isSchemaObject2(schema)) { | ||
| return false; | ||
| } | ||
| if (isObjectSchema(schema)) { | ||
| return true; | ||
| } | ||
| return "patternProperties" in schema || "additionalProperties" in schema || "if" in schema && "then" in schema; | ||
| }; | ||
| var getBooleanSubSchemaType = (schema) => { | ||
| return schema ? "unknown" : "never"; | ||
| }; | ||
| var buildJsDocBlock = (title, description, commentUrl) => { | ||
| let block = `/** | ||
| `; | ||
| block += `* ${title} | ||
| `; | ||
| block += `* | ||
| `; | ||
| block += `* ${description} | ||
| `; | ||
| if (commentUrl?.startsWith("http")) { | ||
| block += `* | ||
| `; | ||
| block += `* @see {@link ${commentUrl}} | ||
| `; | ||
| } | ||
| block += `*/ | ||
| `; | ||
| return block; | ||
| }; | ||
| var getTypeScriptType = (schema, options = {}) => { | ||
| const base = getUnbrandedType(schema, options); | ||
| const brand = getMjstBrand(schema); | ||
| return brand ? `(${base} & { readonly __brand: '${brand}' })` : base; | ||
| }; | ||
| var recordType = (keyType, valueType, options) => options.readonly ? `Readonly<Record<${keyType}, ${valueType}>>` : `Record<${keyType}, ${valueType}>`; | ||
| var getUnbrandedType = (schema, options = {}) => { | ||
| if (typeof schema === "boolean") { | ||
| return getBooleanSubSchemaType(schema); | ||
| } | ||
| if (typeof schema !== "object" || schema === null) { | ||
| return "unknown"; | ||
| } | ||
| const instanceOf = getMjstInstanceOf(schema); | ||
| if (instanceOf) { | ||
| return instanceOf; | ||
| } | ||
| const primitive = getMjstPrimitive(schema); | ||
| if (primitive) { | ||
| return primitive; | ||
| } | ||
| if (schema.$ref) { | ||
| if (!schema.$ref.startsWith("#")) { | ||
| return "unknown"; | ||
| } | ||
| return refToName2(schema.$ref, options.typeSuffix); | ||
| } | ||
| if (schema.$dynamicRef) { | ||
| if (schema.$dynamicRef === "#meta") { | ||
| return `Schema${options.typeSuffix ?? ""}`; | ||
| } | ||
| return refToName2(schema.$dynamicRef, options.typeSuffix); | ||
| } | ||
| if (schema.const !== undefined) { | ||
| return JSON.stringify(schema.const); | ||
| } | ||
| if (schema.enum && schema.enum.length > 0) { | ||
| if (schema.enum.length === 1) { | ||
| return JSON.stringify(schema.enum[0]); | ||
| } | ||
| let enumUnion = JSON.stringify(schema.enum[0]); | ||
| for (let i = 1;i < schema.enum.length; i++) { | ||
| enumUnion += " | " + JSON.stringify(schema.enum[i]); | ||
| } | ||
| return enumUnion; | ||
| } | ||
| if (schema.enum && schema.enum.length > 1) { | ||
| let multiEnumUnion = JSON.stringify(schema.enum[0]); | ||
| for (let i = 1;i < schema.enum.length; i++) { | ||
| multiEnumUnion += " | " + JSON.stringify(schema.enum[i]); | ||
| } | ||
| return multiEnumUnion; | ||
| } | ||
| if (schema.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.length > 0) { | ||
| let oneOfUnion = getTypeScriptType(schema.oneOf[0], options); | ||
| for (let i = 1;i < schema.oneOf.length; i++) { | ||
| oneOfUnion += " | " + getTypeScriptType(schema.oneOf[i], options); | ||
| } | ||
| return oneOfUnion; | ||
| } | ||
| if (schema.anyOf && Array.isArray(schema.anyOf) && schema.anyOf.length > 0) { | ||
| let anyOfUnion = getTypeScriptType(schema.anyOf[0], options); | ||
| for (let i = 1;i < schema.anyOf.length; i++) { | ||
| anyOfUnion += " | " + getTypeScriptType(schema.anyOf[i], options); | ||
| } | ||
| return anyOfUnion; | ||
| } | ||
| if (schema.allOf && Array.isArray(schema.allOf) && schema.allOf.length > 0) { | ||
| let intersectionTypes = getTypeScriptType(schema.allOf[0], options); | ||
| for (let i = 1;i < schema.allOf.length; i++) { | ||
| intersectionTypes += " & " + getTypeScriptType(schema.allOf[i], options); | ||
| } | ||
| return intersectionTypes; | ||
| } | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| if (conditionalResult) { | ||
| const baseType = getTypeScriptType(conditionalResult.schema, options); | ||
| if (conditionalResult.thenRef) { | ||
| return `(${baseType}) & ${refToName2(conditionalResult.thenRef, options.typeSuffix)}`; | ||
| } | ||
| return baseType; | ||
| } | ||
| if (!schema.type) { | ||
| if (schema.additionalProperties !== undefined) { | ||
| if (typeof schema.additionalProperties === "boolean") { | ||
| return recordType("string", getBooleanSubSchemaType(schema.additionalProperties), options); | ||
| } | ||
| return recordType("string", getTypeScriptType(schema.additionalProperties, options), options); | ||
| } | ||
| if (schema.patternProperties && typeof schema.patternProperties === "object") { | ||
| const firstEntry = Object.entries(schema.patternProperties)[0]; | ||
| if (firstEntry) { | ||
| const [pattern, value] = firstEntry; | ||
| if (value !== undefined) { | ||
| const valueType = typeof value === "boolean" ? getBooleanSubSchemaType(value) : getTypeScriptType(value, options); | ||
| if (pattern === "^x-") { | ||
| return recordType("`x-${string}`", valueType, options); | ||
| } | ||
| return recordType("string", valueType, options); | ||
| } | ||
| } | ||
| } | ||
| if (schema.default !== undefined) { | ||
| if (typeof schema.default === "string") { | ||
| return "string"; | ||
| } | ||
| if (typeof schema.default === "number") { | ||
| return "number"; | ||
| } | ||
| if (typeof schema.default === "boolean") { | ||
| return "boolean"; | ||
| } | ||
| } | ||
| return "unknown"; | ||
| } | ||
| if (Array.isArray(schema.type)) { | ||
| const mapType = (t) => { | ||
| switch (t) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "null": | ||
| return "null"; | ||
| case "array": | ||
| return "unknown[]"; | ||
| case "object": | ||
| return "Record<string, unknown>"; | ||
| default: | ||
| return "unknown"; | ||
| } | ||
| }; | ||
| let typeUnion = mapType(schema.type[0]); | ||
| for (let i = 1;i < schema.type.length; i++) { | ||
| typeUnion += " | " + mapType(schema.type[i]); | ||
| } | ||
| return typeUnion; | ||
| } | ||
| switch (schema.type) { | ||
| case "string": | ||
| return "string"; | ||
| case "number": | ||
| case "integer": | ||
| return "number"; | ||
| case "boolean": | ||
| return "boolean"; | ||
| case "array": | ||
| if (schema.items) { | ||
| const itemType = getTypeScriptType(schema.items, options); | ||
| const wrappedItemType = itemType.includes(" | ") ? `(${itemType})` : itemType; | ||
| return options.readonly ? `readonly ${wrappedItemType}[]` : `${wrappedItemType}[]`; | ||
| } | ||
| return options.readonly ? "readonly unknown[]" : "unknown[]"; | ||
| case "object": | ||
| if (schema.properties) { | ||
| const readonlyPrefix = options.readonly ? "readonly " : ""; | ||
| const hasDescriptions = Object.values(schema.properties).some((p) => isSchemaObject2(p) && (typeof p.description === "string" || typeof p.$comment === "string")); | ||
| if (hasDescriptions) { | ||
| let properties2 = ""; | ||
| let first2 = true; | ||
| for (const key in schema.properties) { | ||
| const propSchema = schema.properties[key]; | ||
| const isRequired = schema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined; | ||
| if (!first2) | ||
| properties2 += ` | ||
| `; | ||
| first2 = false; | ||
| if (inlineDescription) { | ||
| properties2 += " /** " + inlineDescription + ` */ | ||
| ` + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";"; | ||
| } else { | ||
| properties2 += " " + readonlyPrefix + safeKey(key) + optional + ": " + propType + ";"; | ||
| } | ||
| } | ||
| return `{ | ||
| ` + properties2 + ` | ||
| }`; | ||
| } | ||
| let properties = ""; | ||
| let first = true; | ||
| for (const key in schema.properties) { | ||
| const propSchema = schema.properties[key]; | ||
| const isRequired = schema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| if (!first) | ||
| properties += "; "; | ||
| properties += readonlyPrefix + safeKey(key) + optional + ": " + propType; | ||
| first = false; | ||
| } | ||
| return "{ " + properties + " }"; | ||
| } | ||
| if (schema.additionalProperties && typeof schema.additionalProperties === "object") { | ||
| const additionalPropType = getTypeScriptType(schema.additionalProperties, options); | ||
| return recordType("string", additionalPropType, options); | ||
| } | ||
| if (schema.patternProperties && typeof schema.patternProperties === "object") { | ||
| const firstEntry = Object.entries(schema.patternProperties)[0]; | ||
| if (firstEntry) { | ||
| const [pattern, patternVal] = firstEntry; | ||
| if (patternVal) { | ||
| const valueType = getTypeScriptType(patternVal, options); | ||
| if (pattern === "^x-") { | ||
| return recordType("`x-${string}`", valueType, options); | ||
| } | ||
| return recordType("string", valueType, options); | ||
| } | ||
| } | ||
| } | ||
| return "object"; | ||
| default: | ||
| return "unknown"; | ||
| } | ||
| }; | ||
| var generateTypeDefinition = (schema, typeName, options = {}) => { | ||
| const readonlyPrefix = options.readonly ? "readonly " : ""; | ||
| if (!isObjectLikeSchema(schema)) { | ||
| const tsType = getTypeScriptType(schema, options); | ||
| let result = ""; | ||
| const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| result += buildJsDocBlock(typeName, topLevelComment); | ||
| } | ||
| result += `export type ${typeName} = ${tsType};`; | ||
| return result; | ||
| } | ||
| if (isObjectLikeSchema(schema)) { | ||
| const conditionalResult = getConditionalObjectSchema(schema); | ||
| const normalizedSchema = conditionalResult?.schema ?? schema; | ||
| const conditionalThenRef = conditionalResult?.thenRef ?? null; | ||
| let jsDocTitle; | ||
| let jsDocDescription; | ||
| const topLevelComment = isSchemaObject2(schema) && typeof schema.description === "string" && schema.description || isSchemaObject2(schema) && typeof schema.$comment === "string" && schema.$comment || undefined; | ||
| if (topLevelComment) { | ||
| jsDocTitle = typeName; | ||
| jsDocDescription = topLevelComment; | ||
| } | ||
| const hasProperties2 = normalizedSchema.properties && Object.keys(normalizedSchema.properties).length > 0; | ||
| const hasAdditionalProperties2 = normalizedSchema.additionalProperties && typeof normalizedSchema.additionalProperties === "object"; | ||
| const hasPatternProperties = normalizedSchema.patternProperties && typeof normalizedSchema.patternProperties === "object" && Object.keys(normalizedSchema.patternProperties).length > 0; | ||
| if (!hasProperties2 && hasPatternProperties && normalizedSchema.patternProperties) { | ||
| const firstEntry = Object.entries(normalizedSchema.patternProperties)[0]; | ||
| const firstPattern = firstEntry?.[0]; | ||
| const firstPatternProperty = firstEntry?.[1]; | ||
| if (firstPatternProperty === undefined) { | ||
| return `export type ${typeName} = Record<string, unknown>;`; | ||
| } | ||
| const patternPropType = typeof firstPatternProperty === "boolean" ? getBooleanSubSchemaType(firstPatternProperty) : getTypeScriptType(firstPatternProperty, options); | ||
| const keyType = firstPattern === "^x-" ? "`x-${string}`" : "string"; | ||
| let result2 = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result2 += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result2 += `export type ${typeName} = ${recordType(keyType, patternPropType, options)};`; | ||
| return result2; | ||
| } | ||
| if (!hasProperties2 && hasAdditionalProperties2 && normalizedSchema.additionalProperties) { | ||
| const additionalPropType = getTypeScriptType(normalizedSchema.additionalProperties, options); | ||
| let result2 = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result2 += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| result2 += `export type ${typeName} = { | ||
| ${readonlyPrefix}[key: string]: ${additionalPropType}; | ||
| };`; | ||
| return result2; | ||
| } | ||
| const schemaProps = normalizedSchema.properties ?? {}; | ||
| let properties = ""; | ||
| let isFirstProp = true; | ||
| for (const key in schemaProps) { | ||
| const propSchema = schemaProps[key]; | ||
| const isRequired = normalizedSchema.required?.includes(key) ?? false; | ||
| const optional = isRequired ? "" : "?"; | ||
| const propType = getTypeScriptType(propSchema, options); | ||
| const quotedKey = readonlyPrefix + safeKey(key); | ||
| if (!isFirstProp) | ||
| properties += ` | ||
| `; | ||
| isFirstProp = false; | ||
| const inlineDescription = isSchemaObject2(propSchema) && typeof propSchema.description === "string" ? propSchema.description : isSchemaObject2(propSchema) && typeof propSchema.$comment === "string" ? propSchema.$comment : undefined; | ||
| if (inlineDescription) { | ||
| properties += " /** " + inlineDescription + ` */ | ||
| ` + quotedKey + optional + ": " + propType + ";"; | ||
| } else { | ||
| properties += " " + quotedKey + optional + ": " + propType + ";"; | ||
| } | ||
| } | ||
| const allOfIntersections = []; | ||
| if (isSchemaObject2(schema) && Array.isArray(schema.allOf)) { | ||
| for (const entry of schema.allOf) { | ||
| if (isSchemaObject2(entry) && entry.$ref) { | ||
| allOfIntersections.push(refToName2(entry.$ref, options.typeSuffix)); | ||
| } | ||
| } | ||
| } | ||
| if (isSchemaObject2(schema) && typeof schema.$ref === "string" && schema.$ref.startsWith("#")) { | ||
| allOfIntersections.push(refToName2(schema.$ref, options.typeSuffix)); | ||
| } | ||
| let result = ""; | ||
| if (jsDocTitle && jsDocDescription) { | ||
| result += buildJsDocBlock(jsDocTitle, jsDocDescription); | ||
| } | ||
| let typeBody = `{ | ||
| ` + properties + ` | ||
| }`; | ||
| if (conditionalThenRef) { | ||
| typeBody += " & " + refToName2(conditionalThenRef, options.typeSuffix); | ||
| } | ||
| for (const intersectionType of allOfIntersections) { | ||
| typeBody += " & " + intersectionType; | ||
| } | ||
| result += "export type " + typeName + " = " + typeBody + ";"; | ||
| return result; | ||
| } | ||
| return "export type " + typeName + " = unknown;"; | ||
| }; | ||
| // ../helpers/dist/schema-guards.js | ||
| var hasRef = (value) => { | ||
| return typeof value === "object" && value !== null && !Array.isArray(value) && "$ref" in value && typeof value.$ref === "string"; | ||
| }; | ||
| var isSchemaObject3 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var hasType = (schema) => { | ||
| return isSchemaObject3(schema) && "type" in schema && typeof schema.type === "string"; | ||
| }; | ||
| var hasProperties = (schema) => { | ||
| return isSchemaObject3(schema) && "properties" in schema && typeof schema.properties === "object" && schema.properties !== null; | ||
| }; | ||
| var hasEnum = (schema) => { | ||
| return isSchemaObject3(schema) && "enum" in schema && Array.isArray(schema.enum); | ||
| }; | ||
| var hasConst = (schema) => { | ||
| return isSchemaObject3(schema) && "const" in schema; | ||
| }; | ||
| var hasPattern = (schema) => { | ||
| return isSchemaObject3(schema) && "pattern" in schema && typeof schema.pattern === "string"; | ||
| }; | ||
| var hasFormat = (schema) => { | ||
| return isSchemaObject3(schema) && "format" in schema && typeof schema.format === "string"; | ||
| }; | ||
| var hasDefault = (schema) => { | ||
| return isSchemaObject3(schema) && "default" in schema; | ||
| }; | ||
| var hasExamples = (schema) => { | ||
| return isSchemaObject3(schema) && "examples" in schema && Array.isArray(schema.examples); | ||
| }; | ||
| var hasOneOf = (schema) => { | ||
| return isSchemaObject3(schema) && "oneOf" in schema && Array.isArray(schema.oneOf); | ||
| }; | ||
| var hasAnyOf = (schema) => { | ||
| return isSchemaObject3(schema) && "anyOf" in schema && Array.isArray(schema.anyOf); | ||
| }; | ||
| var hasAllOf = (schema) => { | ||
| return isSchemaObject3(schema) && "allOf" in schema && Array.isArray(schema.allOf); | ||
| }; | ||
| var hasRequired = (schema) => { | ||
| return isSchemaObject3(schema) && "required" in schema && Array.isArray(schema.required); | ||
| }; | ||
| var hasItems = (schema) => { | ||
| return isSchemaObject3(schema) && "items" in schema && typeof schema.items === "object" && schema.items !== null && typeof schema.items !== "boolean"; | ||
| }; | ||
| var hasAdditionalProperties = (schema) => { | ||
| return isSchemaObject3(schema) && "additionalProperties" in schema; | ||
| }; | ||
| var hasMinLength = (schema) => { | ||
| return isSchemaObject3(schema) && "minLength" in schema && typeof schema.minLength === "number"; | ||
| }; | ||
| var hasMaxLength = (schema) => { | ||
| return isSchemaObject3(schema) && "maxLength" in schema && typeof schema.maxLength === "number"; | ||
| }; | ||
| var hasMinimum = (schema) => { | ||
| return isSchemaObject3(schema) && "minimum" in schema && typeof schema.minimum === "number"; | ||
| }; | ||
| var hasMaximum = (schema) => { | ||
| return isSchemaObject3(schema) && "maximum" in schema && typeof schema.maximum === "number"; | ||
| }; | ||
| var hasExclusiveMinimum = (schema) => { | ||
| return isSchemaObject3(schema) && "exclusiveMinimum" in schema && typeof schema.exclusiveMinimum === "number"; | ||
| }; | ||
| var hasExclusiveMaximum = (schema) => { | ||
| return isSchemaObject3(schema) && "exclusiveMaximum" in schema && typeof schema.exclusiveMaximum === "number"; | ||
| }; | ||
| var hasMultipleOf = (schema) => { | ||
| return isSchemaObject3(schema) && "multipleOf" in schema && typeof schema.multipleOf === "number"; | ||
| }; | ||
| var hasMinItems = (schema) => { | ||
| return isSchemaObject3(schema) && "minItems" in schema && typeof schema.minItems === "number"; | ||
| }; | ||
| var hasMaxItems = (schema) => { | ||
| return isSchemaObject3(schema) && "maxItems" in schema && typeof schema.maxItems === "number"; | ||
| }; | ||
| var hasUniqueItems = (schema) => { | ||
| return isSchemaObject3(schema) && "uniqueItems" in schema && typeof schema.uniqueItems === "boolean"; | ||
| }; | ||
| // src/generators/collect-example-imports.ts | ||
| var buildImport = (ref, suffix) => { | ||
| const filename = refToFilename(ref); | ||
| const typeName = refToName(ref, suffix); | ||
| return `import { type ${typeName}, ${typeName}Arbitrary } from './${filename}'`; | ||
| }; | ||
| var collectDirectRefs = (schema) => { | ||
| if (typeof schema === "boolean" || schema === null) | ||
| return []; | ||
| const refs = []; | ||
| 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) : []; | ||
| for (const prop of propSchemas) { | ||
| if (hasRef(prop)) | ||
| refs.push(prop.$ref); | ||
| if (hasItems(prop) && hasRef(prop.items)) | ||
| refs.push(prop.items.$ref); | ||
| if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) { | ||
| refs.push(prop.additionalProperties.$ref); | ||
| } | ||
| } | ||
| if (hasItems(schema) && hasRef(schema.items)) { | ||
| refs.push(schema.items.$ref); | ||
| } | ||
| if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) { | ||
| refs.push(schema.additionalProperties.$ref); | ||
| } | ||
| for (const branch of [ | ||
| ...hasOneOf(schema) ? schema.oneOf : [], | ||
| ...hasAnyOf(schema) ? schema.anyOf : [], | ||
| ...hasAllOf(schema) ? schema.allOf : [] | ||
| ]) { | ||
| if (hasRef(branch)) | ||
| refs.push(branch.$ref); | ||
| } | ||
| return refs; | ||
| }; | ||
| var collectExampleImports = (schema, options) => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null; | ||
| const rootSchema = options?.rootSchema; | ||
| const typeSuffix = options?.typeSuffix ?? ""; | ||
| const refs = collectDirectRefs(schema); | ||
| const seen = new Set; | ||
| const imports = []; | ||
| for (const ref of refs) { | ||
| const filename = refToFilename(ref); | ||
| if (seen.has(filename)) | ||
| continue; | ||
| if (selfFilename && filename === selfFilename) | ||
| continue; | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| continue; | ||
| } | ||
| seen.add(filename); | ||
| imports.push(buildImport(ref, typeSuffix)); | ||
| } | ||
| return imports; | ||
| }; | ||
| // ../helpers/dist/mjst-extension.js | ||
| var isSchemaObject4 = (schema) => { | ||
| return typeof schema === "object" && schema !== null && typeof schema !== "boolean"; | ||
| }; | ||
| var MJST_EXTENSION_KEY2 = "x-mjst"; | ||
| var IDENTIFIER2 = /^[A-Za-z_$][A-Za-z0-9_$]*$/; | ||
| var SUPPORTED_PRIMITIVES2 = new Set(["bigint"]); | ||
| var readExtensionString2 = (schema, field) => { | ||
| if (!isSchemaObject4(schema)) | ||
| return; | ||
| const extension = schema[MJST_EXTENSION_KEY2]; | ||
| if (typeof extension !== "object" || extension === null) | ||
| return; | ||
| const value = extension[field]; | ||
| return typeof value === "string" ? value : undefined; | ||
| }; | ||
| var getMjstInstanceOf2 = (schema) => { | ||
| const instanceOf = readExtensionString2(schema, "instanceOf"); | ||
| return instanceOf !== undefined && IDENTIFIER2.test(instanceOf) ? instanceOf : undefined; | ||
| }; | ||
| var getMjstPrimitive2 = (schema) => { | ||
| const primitive = readExtensionString2(schema, "primitive"); | ||
| return primitive !== undefined && SUPPORTED_PRIMITIVES2.has(primitive) ? primitive : undefined; | ||
| }; | ||
| // src/generators/derive-example.ts | ||
| var lowerFirst = (name) => name.charAt(0).toLowerCase() + name.slice(1); | ||
| var exampleName = (typeName) => `${lowerFirst(typeName)}Example`; | ||
| var exampleString = (schema) => { | ||
| 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; | ||
| }; | ||
| var deriveExample = (schema, rootSchema, seen = new Set) => { | ||
| if (!isSchemaObject3(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, rootSchema, new Set([...seen, ref])); | ||
| } | ||
| const instanceOf = getMjstInstanceOf2(schema); | ||
| if (instanceOf === "Date") | ||
| return new Date(0); | ||
| const primitive = getMjstPrimitive2(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); | ||
| 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 = {}; | ||
| if (hasProperties(schema)) { | ||
| for (const [key, propSchema] of Object.entries(schema.properties)) { | ||
| out[key] = deriveExample(propSchema, rootSchema, seen); | ||
| } | ||
| } | ||
| return out; | ||
| } | ||
| default: | ||
| return null; | ||
| } | ||
| }; | ||
| var serializeValue = (value) => { | ||
| 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); | ||
| }; | ||
| var generateExampleConst = (schema, typeName, rootSchema) => { | ||
| const value = deriveExample(schema, rootSchema); | ||
| return `export const ${exampleName(typeName)}: ${typeName} = ${serializeValue(value)}`; | ||
| }; | ||
| // src/generators/generate-arbitrary.ts | ||
| var arbitraryName = (typeName) => `${typeName}Arbitrary`; | ||
| var stringExpr = (schema) => { | ||
| 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 = []; | ||
| 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()"; | ||
| }; | ||
| var integerExpr = (schema) => { | ||
| const opts = []; | ||
| 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; | ||
| }; | ||
| var numberExpr = (schema) => { | ||
| const opts = ["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; | ||
| }; | ||
| var arrayExpr = (schema, suffix) => { | ||
| const items = hasItems(schema) && isSchemaObject3(schema.items) ? arbitraryExpr(schema.items, suffix) : "fc.anything()"; | ||
| const opts = []; | ||
| 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})`; | ||
| }; | ||
| var objectExpr = (schema, suffix) => { | ||
| 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(", ")} }`; | ||
| 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}] })`; | ||
| }; | ||
| var oneofExpr = (branches, suffix) => { | ||
| const exprs = branches.map((branch) => arbitraryExpr(branch, suffix)); | ||
| return `fc.oneof(${exprs.join(", ")})`; | ||
| }; | ||
| var scalarExpr = (type, schema, suffix) => { | ||
| 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()"; | ||
| } | ||
| }; | ||
| var arbitraryExpr = (schema, suffix) => { | ||
| if (!isSchemaObject3(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.map((value) => JSON.stringify(value)).join(", "); | ||
| return `fc.constantFrom(${values})`; | ||
| } | ||
| const instanceOf = getMjstInstanceOf2(schema); | ||
| if (instanceOf === "Date") | ||
| return "fc.date({ noInvalidDate: true })"; | ||
| if (instanceOf) | ||
| return "fc.anything()"; | ||
| const primitive = getMjstPrimitive2(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); | ||
| if (hasType(schema)) | ||
| return scalarExpr(schema.type, schema, suffix); | ||
| return "fc.anything()"; | ||
| }; | ||
| var generateArbitrary = (schema, typeName, suffix = "") => { | ||
| const expr = arbitraryExpr(schema, suffix); | ||
| return `export const ${arbitraryName(typeName)}: fc.Arbitrary<${typeName}> = ${expr}`; | ||
| }; | ||
| // src/generators/generate-files.ts | ||
| var generateExampleFile = (schema, typeName, options) => { | ||
| 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' | ||
| `; | ||
| for (const imp of refImports) { | ||
| result += imp + ` | ||
| `; | ||
| } | ||
| result += ` | ||
| `; | ||
| result += typeDefinition + ` | ||
| ` + arbitrary + ` | ||
| ` + example + ` | ||
| `; | ||
| return result; | ||
| }; | ||
| // src/generators/build-schema.ts | ||
| var buildExampleSchema = async (rootSchema, rootTypeName, typeSuffix = "") => { | ||
| rootSchema = upgradeDraft07Schema(rootSchema); | ||
| const files = []; | ||
| const processedRefs = new Set; | ||
| const processedFilenames = new Set; | ||
| const refsToProcess = []; | ||
| const dynamicRefMap = buildDynamicRefMap(rootSchema); | ||
| const processedRootSchema = resolveDynamicRefs(rootSchema, dynamicRefMap); | ||
| const rootContent = generateExampleFile(processedRootSchema, rootTypeName, { | ||
| rootSchema, | ||
| typeSuffix | ||
| }); | ||
| const rootFilename = rootTypeName.toLowerCase(); | ||
| if (rootFilename !== "index") { | ||
| processedFilenames.add(rootFilename); | ||
| files.push({ filename: `${rootFilename}.ts`, content: rootContent }); | ||
| } | ||
| const rootRefs = extractRefs(rootSchema); | ||
| refsToProcess.push(...rootRefs); | ||
| while (refsToProcess.length > 0) { | ||
| const ref = refsToProcess.shift(); | ||
| if (!ref || processedRefs.has(ref)) | ||
| continue; | ||
| processedRefs.add(ref); | ||
| const resolvedSchema = resolveRef(ref, rootSchema); | ||
| if (!resolvedSchema) { | ||
| console.warn(`Warning: Could not resolve ref: ${ref}`); | ||
| continue; | ||
| } | ||
| const typeName = refToName(ref, typeSuffix); | ||
| const filename = refToFilename(ref); | ||
| const processedSchema = resolveDynamicRefs(resolvedSchema, dynamicRefMap); | ||
| const content = generateExampleFile(processedSchema, typeName, { | ||
| selfRef: ref, | ||
| rootSchema, | ||
| typeSuffix | ||
| }); | ||
| if (filename !== "index" && !processedFilenames.has(filename)) { | ||
| processedFilenames.add(filename); | ||
| files.push({ filename: `${filename}.ts`, content }); | ||
| } | ||
| for (const nestedRef of extractRefs(resolvedSchema)) { | ||
| if (!processedRefs.has(nestedRef)) | ||
| refsToProcess.push(nestedRef); | ||
| } | ||
| } | ||
| const TYPE_EXPORT_RE = /^export type (\w+)/gm; | ||
| const CONST_EXPORT_RE = /^export const (\w+)/gm; | ||
| const sortedFiles = [...files].sort((a, b) => a.filename.localeCompare(b.filename)); | ||
| let indexContent = ""; | ||
| for (const file of sortedFiles) { | ||
| const moduleName = file.filename.replace(/\.ts$/, ""); | ||
| const typeNames = []; | ||
| const constNames = []; | ||
| for (const match of file.content.matchAll(TYPE_EXPORT_RE)) | ||
| typeNames.push(match[1]); | ||
| for (const match of file.content.matchAll(CONST_EXPORT_RE)) | ||
| constNames.push(match[1]); | ||
| if (typeNames.length === 0 && constNames.length === 0) | ||
| continue; | ||
| const typeExports = typeNames.map((n) => `type ${n}`); | ||
| indexContent += `export { ${[...typeExports, ...constNames].join(", ")} } from './${moduleName}'; | ||
| `; | ||
| } | ||
| files.push({ filename: "index.ts", content: indexContent }); | ||
| return files; | ||
| }; | ||
| export { | ||
| serializeValue, | ||
| generateExampleConst, | ||
| generateArbitrary, | ||
| deriveExample, | ||
| buildExampleSchema | ||
| }; |
+3
-3
| { | ||
| "name": "@amritk/generate-examples", | ||
| "version": "0.0.0", | ||
| "version": "0.1.1", | ||
| "description": "Generate fast-check arbitraries and example values from JSON Schemas.", | ||
@@ -51,4 +51,4 @@ "module": "./dist/index.js", | ||
| "dependencies": { | ||
| "json-schema-typed": "catalog:", | ||
| "@amritk/helpers": "workspace:*" | ||
| "json-schema-typed": "^8.0.1", | ||
| "@amritk/helpers": "0.6.2" | ||
| }, | ||
@@ -55,0 +55,0 @@ "peerDependencies": { |
95896
141.18%18
63.64%2279
188.48%+ Added
+ Added
Updated
Updated