import { ILanguageConverter, IRNode, SignatureOccurrenceValue } from "../types.js"; import { walkIR } from "./walk.js"; export { walkIR }; export type RefTypeName = string; export interface RefTypeEntry { signature: SignatureOccurrenceValue["signature"]; name: RefTypeName; code: string; title?: string; description?: string; doc?: string; } export type RefTypes = RefTypeEntry[]; /** * Per-language configuration bundle. Each language subclass declares one * `languageProfile` field; the base class reads from it instead of calling * a method-per-knob. Optional fields fall through to the base default. */ export interface LanguageProfile { /** Language identifier emitted as `language` on the converter. */ language: string; /** * When true, `getReferencedType` short-circuits IR nodes whose `name` matches * the root name back to a direct self-reference (so e.g. a `Tree` schema with * `children: Array<{$ref: "#"}>` emits as `children: Array` rather than * extracting a duplicate `Child` type). Default: true. All current languages * use the default; this hook exists for hypothetical future targets that want * a different recursion shape. */ detectSelfReferenceToRoot?: boolean; /** * When true, `enhanceDiscriminatedUnions` ALSO walks `oneOf` unions (in addition * to `anyOf`). Default: false. Languages that emit a tagged-union form for `oneOf` * (Kotlin sealed interface, Swift enum w/ associated values) override to true. */ processOneOfAsDiscriminatedUnion?: boolean; /** * When true, `applyDiscriminatedUnionEnhancements` populates `discriminatorInfo` * for downstream consolidated enum emission. Default: false. TS overrides based * on `enumStyle === "enum" && !inlineTypes`. */ shouldPopulateDiscriminatorInfo?: boolean; /** * When true, `stripDiscriminatorField` removes the discriminator key from variant * payload bodies (because the language hoists it into a tag annotation or codable * plumbing). Default: false. Kotlin = `isKotlinx`. Swift = `isCodable`. */ shouldEraseDiscriminator?: boolean; /** * Custom ref-type naming configuration. Merged shallow over the BaseConverter default * (postfix list, depluralize, etc.). Useful for adding language-specific postfixes * (e.g. "Enum" in TS). */ refTypeNamingConfig?: Partial; /** * Parent-name component used by `applyDiscriminatedUnionEnhancements` when deriving * variant names. Default: last non-numeric segment of `ir.path`. Kotlin extends this * with a fallback to `ir.name || ir.title`; Swift always returns "" (variants are * nested in the enum, no parent suffix needed). */ getDiscriminatedVariantParentName?: (ir: IRNode) => string; /** * Override ref-type name resolution. `defaultResolver()` invokes the path-based * default. TS uses this to apply enum-style overrides. */ resolveRefTypeName?: (ir: IRNode, signature: string, defaultResolver: () => string) => string; /** * Language-specific signature dedup decision. Default: false. TS = `dedupSignatures.has(sig)`. */ shouldReuseExistingSignature?: (signature: string) => boolean; /** * Formats a root-level type alias when the schema root is an `x-named-type` * reference (e.g. Kotlin `typealias Root = Message`). Languages that render * the root through a generic alias wrapper — TypeScript's * `export type Root = ` — omit this and get the alias for free. * Languages whose roots are nominal declarations (Kotlin `data class`, Swift * `struct`) provide it so a bare root reference becomes a typealias instead. */ formatRootTypeAlias?: (rootName: string, target: string) => string; } export interface RefTypeNamingConfig { /** Base postfixes to try for name collision resolution */ postfixes: string[]; /** If true, singularize array item path segments (e.g. "entries" → "entry", "people" → "person") */ depluralize?: boolean; /** If true, handle "*" path endings with AnyKey/AnyProperty postfixes */ handleAnySymbol?: boolean; /** If true, strip leading "*" from proposed names */ stripLeadingAnySymbol?: boolean; /** * Controls the postfix added to array item type names. * - `false` (default) → no postfix, uses property name directly * - `string` → custom postfix (e.g. "Item" → ContactsItem, "Element" → ContactsElement) */ arrayItemNaming?: string | false; /** * Additional words that should not be singularized when `depluralize` is true. * Built-in uncountables: "data", "metadata". * @example ["criteria", "alumni", "corpus"] */ uncountableWords?: string[]; /** * Type names the language references as ambient globals in emitted output * (e.g. TS `Record<…>`, `Array<…>`). An extracted type must never be named * one of these, or the local declaration would shadow the global the same * output relies on, producing code that does not compile. Reserved names are * treated as already-taken during naming, so the normal postfix escalation * disambiguates them (e.g. `Record` → `RecordType`). Default: none. */ reservedTypeNames?: string[]; } export type GenerateTypeUtils = { getReferencedType(ir: IRNode): string | undefined; }; /** * The state-and-method surface that helper modules in `src/converter/` * need to operate on a BaseConverter instance. BaseConverter implements * this; helpers take it as their first argument. * * Using this interface (rather than the BaseConverter class itself) keeps * helper modules from depending on the abstract class shape — they only * see the concrete contract they need. * * @internal */ export interface BaseConverterContext { readonly refTypes: RefTypeEntry[]; readonly usedDeclarationNames: Set; rootName?: string; rootDoc?: string; baseOpts?: BaseConverterOpts; readonly variantNames: Map; readonly discriminatorInfo: Map; readonly dedupSignatures: Set; mergeCounter: number; /** Used internally by `getUniqueRefTypeName`; exposed for the helper to mutate. */ fallbackCounter: number; readonly languageProfile: LanguageProfile; /** Subclass-implemented; called by `getReferencedType` after registering an entry. */ generateObjectType(ir: IRNode, utils: GenerateTypeUtils): string; /** Returns a name not already in `usedDeclarationNames`, suffixing as needed. */ findAvailableName(base: string): string; /** Disambiguates `base` and registers it in `usedDeclarationNames` atomically. */ claimDeclarationName(base: string): string; /** True if `name` is a reserved global type name (see `reservedTypeNames`). */ isReservedTypeName(name: string): boolean; /** Locates a const-string discriminator property across union options. */ findDiscriminatorProperty(options: IRNode[], sharedPropNames: string[]): string | null; /** Computes shared and combined property names across union options. */ collectUnionPropertyNames(options: IRNode[]): { allPropNames: string[]; sharedPropNames: string[]; }; /** Returns a clone of `opt` with the discriminator property removed. */ stripDiscriminatorField(opt: IRNode, discriminator: string): IRNode; /** Returns the const string value of an enum/literal IR, if any. */ getConstStringValue(ir: IRNode | undefined): string | undefined; } export interface BaseConverterOpts { /** Overrides the IR root name (which defaults to schema.title or "Root"). */ rootTypeName?: string; /** * Controls the postfix added to array item type names. * - `false` (default) → no postfix, uses property name directly * - `string` → custom postfix (e.g. "Item" → ContactsItem, "Element" → ContactsElement) */ arrayItemNaming?: string | false; /** * If true (default), singularize array item path segments when generating type names. */ depluralize?: boolean; /** * Additional words that should not be singularized when `depluralize` is true. */ uncountableWords?: string[]; /** * How to handle unions that survive merging with no discriminator. * - `"throw"` (default) — throw with a path-bearing message * - `"fallback"` — emit a language-specific fallback type (added in later tasks) */ unsupportedUnions?: "throw" | "fallback"; /** * Shared registry of declaration names already emitted. When provided, * the converter uses this Set as its `usedDeclarationNames` and mutates * it as new types are emitted. Pass the same Set across multiple emit * calls to avoid duplicate-name compile errors when the resulting code * blocks are concatenated into one Kotlin/Swift namespace. * * Names that collide across calls fall through the standard collision- * resolution path (parent path escalation, postfix list, then a numeric * suffix). Pair with {@link namePrefix} for cleaner per-slot names. * * Most useful for Kotlin and Swift, which require named declarations for * all non-primitive types. Harmless for TypeScript, which can use * `inlineTypes: true` to flatten nested types instead. */ nameRegistry?: Set; /** * Optional synthetic component prepended to every extracted (nested) * type name during ref-type naming. With `namePrefix: "Body"`, a nested * `address` field extracts as `BodyAddress` instead of `Address`. * * Useful when emitting multiple sibling schemas that share an output * namespace — pair with {@link nameRegistry} and a per-slot * {@link rootTypeName} to avoid all cross-call collisions. * * Does not affect the root type name (use `rootTypeName` for that). * The prefix is sanitized via PascalCase, so non-identifier characters * are stripped. */ namePrefix?: string; } export declare abstract class BaseConverter implements Partial, BaseConverterContext { abstract readonly code: string; /** @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ abstract readonly languageProfile: LanguageProfile; abstract readonly rootTypeName: string; abstract readonly extractedTypeNames: string[]; abstract readonly imports: string[]; abstract readonly referencedNamedTypes: string[]; get language(): string; /** @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ refTypes: RefTypes; /** * Shared registry of all top-level declaration names (types + enums) for cross-namespace collision detection. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ usedDeclarationNames: Set; /** * Root schema name, used as fallback context for root-level array item naming. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ rootName?: string; /** * Optional doc note attached to the root type declaration. Populated by * subclasses (e.g. for additionalProperties annotations) since the root * type is not held in the refTypes registry. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ rootDoc?: string; /** * Shared options available to all language converters. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ baseOpts?: BaseConverterOpts; /** @internal Public for `BaseConverterContext`; treat as private. */ fallbackCounter: number; /** @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ mergeCounter: number; /** * Maps discriminator literal IR nodes to their consolidated enum info. * Used by generateLiteralType and preRegisterEnumNames to emit a single * consolidated enum and member references instead of single-value enums. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ discriminatorInfo: Map; /** * Maps variant IR nodes to their enhancement-assigned names. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ variantNames: Map; /** * Signatures eligible for cross-variant deduplication. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ dedupSignatures: Set; /** * Returns names of all extracted ref types in declaration order. * Subclasses may filter further (e.g. to exclude the root type). */ protected computeExtractedTypeNames(): string[]; /** * Collects the names of all external types referenced via `x-named-type`, * deduped and lexicographically sorted. Derived purely from the IR (the set * is independent of language emission), mirroring computeExtractedTypeNames. */ protected computeReferencedNamedTypes(ir: IRNode): string[]; /** Path-derived, collision-free ref type name. Delegates to `naming.ts`. */ protected getUniqueRefTypeName(signature: string, nodePath: string): RefTypeName; /** * Each language subclass must implement object-literal emission. * `getReferencedType` calls this when registering a new ref type. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ abstract generateObjectType(ir: IRNode, utils: GenerateTypeUtils): string; /** Resolves an IR node to its registered ref-type name. Delegates to `registry.ts`. */ protected getReferencedType(ir: IRNode): string | undefined; /** * Recursively walks the IR tree (bottom-up) and merges anyOf unions * whose options are all objects with compatible property types. * Delegates to `mergeUnions.ts`. */ protected mergeCompatibleUnions(ir: IRNode): IRNode; /** Delegates to `mergeUnions.ts`. */ protected tryMergeObjectUnion(ir: IRNode): IRNode | null; /** Delegates to `mergeUnions.ts`. */ protected tryMergeProperty(instances: IRNode[]): IRNode | null; /** Delegates to `mergeUnions.ts`. */ protected isDiscriminatedUnion(options: IRNode[], sharedPropNames: string[], allPropNames: string[]): boolean; /** * Walks the IR tree and applies discriminated union enhancements * to anyOf unions that survived merging. Delegates to `discriminatedUnions.ts`. */ protected enhanceDiscriminatedUnions(ir: IRNode): void; /** * Delegates to `discriminatedUnions.ts`. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ stripDiscriminatorField(opt: IRNode, discriminator: string): IRNode; /** * Returns `base` if it is not already taken in `usedDeclarationNames` and is * not a reserved global type name, otherwise appends an incrementing suffix * (`base2`, `base3`, …) until a free name is found. Subclasses use this for * collision-free declaration names where path-derived disambiguation is not * appropriate (e.g. enum names derived from a property name, discriminated- * union variant names, the root type name). Honoring `reservedTypeNames` here * keeps every declaration-name producer that routes through this helper from * shadowing a global the same output references (e.g. `Record<…>`, `Array<…>`). * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ findAvailableName(base: string): string; /** * Disambiguates `base` via `findAvailableName` AND records the result in * `usedDeclarationNames`, returning it. This is the single gate for simple * (non-path-derived) declaration names — root, enum, and discriminated-union * variant names — so disambiguation and registration happen atomically and no * caller can register a name that bypassed reserved-/used-name checks. (The * path-derived analog, `getUniqueRefTypeName`, self-registers the same way.) * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ claimDeclarationName(base: string): string; /** * True if `name` is a type the language references as an ambient global in * emitted output (see `RefTypeNamingConfig.reservedTypeNames`). Declaration- * name producers consult this so a generated declaration never shadows a * global the same output relies on. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ isReservedTypeName(name: string): boolean; private reservedTypeNamesSet?; /** Delegates to `discriminatedUnions.ts`. */ protected applyDiscriminatedUnionEnhancements(ir: IRNode): void; /** * Delegates to `discriminatedUnions.ts`. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ collectUnionPropertyNames(options: IRNode[]): { allPropNames: string[]; sharedPropNames: string[]; }; /** * Delegates to `discriminatedUnions.ts`. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ findDiscriminatorProperty(options: IRNode[], sharedPropNames: string[]): string | null; /** * Delegates to `discriminatedUnions.ts`. * @internal Public for `BaseConverterContext`; treat as protected for subclasses. */ getConstStringValue(ir: IRNode | undefined): string | undefined; }