diff --git a/schema/karakum-schema.json b/schema/karakum-schema.json index efd3dae..4d0434e 100644 --- a/schema/karakum-schema.json +++ b/schema/karakum-schema.json @@ -240,6 +240,20 @@ ], "title": "plugins" }, + "unionNameResolvers": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "string" + } + ], + "title": "unionNameResolvers" + }, "varianceModifiers": { "anyOf": [ { diff --git a/src/configuration/configuration.ts b/src/configuration/configuration.ts index d56d1af..9cfde0f 100644 --- a/src/configuration/configuration.ts +++ b/src/configuration/configuration.ts @@ -48,6 +48,8 @@ export interface SchemaConfiguration { * */ packageNameMapper?: Record + unionNameResolvers?: string | string[] + /** * @TJS-type object * @additionalProperties { "type": "array", "items": { "type": "string" } } @@ -118,6 +120,7 @@ export interface Configuration extends PartialConfiguration { moduleNameMapper: Record packageNameMapper: Record + unionNameResolvers: string[] importInjector: Record importMapper: Record> diff --git a/src/configuration/defaultizeConfiguration.ts b/src/configuration/defaultizeConfiguration.ts index b1838bf..d7e009d 100644 --- a/src/configuration/defaultizeConfiguration.ts +++ b/src/configuration/defaultizeConfiguration.ts @@ -21,6 +21,10 @@ const defaultNameResolverPatterns = [ "karakum/nameResolvers/*.js" ] +const defaultUnionNameResolverPatterns = [ + "karakum/unionNameResolvers/*.js" +] + const defaultInheritanceModifierPatterns = [ "karakum/inheritanceModifiers/*.js" ] @@ -109,6 +113,8 @@ export async function defaultizeConfiguration(configuration: PartialConfiguratio inheritanceModifiers: normalizeOption(configuration.inheritanceModifiers, defaultInheritanceModifierPatterns), + unionNameResolvers: normalizeOption(configuration.unionNameResolvers, defaultUnionNameResolverPatterns), + varianceModifiers: normalizeOption(configuration.varianceModifiers, defaultVarianceModifierPatterns), moduleNameMapper: configuration.moduleNameMapper ?? {}, diff --git a/src/converter/plugins/StringUnionTypePlugin.ts b/src/converter/plugins/StringUnionTypePlugin.ts index 268c8a5..b627a80 100644 --- a/src/converter/plugins/StringUnionTypePlugin.ts +++ b/src/converter/plugins/StringUnionTypePlugin.ts @@ -1,6 +1,7 @@ import ts, {LiteralTypeNode, StringLiteral, UnionTypeNode} from "typescript"; import {escapeIdentifier, notEscapedIdentifier} from "../../utils/strings.js"; import {CheckCoverageService, checkCoverageServiceKey} from "./CheckCoveragePlugin.js"; +import {UnionNameResolverService, unionNameResolverServiceKey} from "./UnionNameResolverPlugin.js"; import {ConverterContext} from "../context.js"; import {createAnonymousDeclarationPlugin} from "./AnonymousDeclarationPlugin.js"; import {flatUnionTypes, isNullableType} from "./NullableUnionTypePlugin.js"; @@ -48,6 +49,8 @@ export function convertStringUnionType( const checkCoverageService = context.lookupService(checkCoverageServiceKey) checkCoverageService?.cover(node) + const unionNameResolverService = context.lookupService(unionNameResolverServiceKey) + if (unionNameResolverService === undefined) throw new Error("UnionNameResolverService required") const typeScriptService = context.lookupService(typeScriptServiceKey) const namespaceInfoService = context.lookupService(namespaceInfoServiceKey) const injectionService = context.lookupService(injectionServiceKey) @@ -69,10 +72,16 @@ export function convertStringUnionType( checkCoverageService?.cover(literal) const value = literal.text - const valueAsIdentifier = notEscapedIdentifier(value) - const key = (value === "") || (valueAsIdentifier === "") - ? "_" - : valueAsIdentifier + + let key = unionNameResolverService.resolveUnionName(literal, context) + + if (!key) { + const valueAsIdentifier = notEscapedIdentifier(value) + key = (value === "") || (valueAsIdentifier === "") + ? "_" + : valueAsIdentifier + } + return [key, value] as const }) diff --git a/src/converter/plugins/UnionNameResolverPlugin.ts b/src/converter/plugins/UnionNameResolverPlugin.ts new file mode 100644 index 0000000..7be0f1e --- /dev/null +++ b/src/converter/plugins/UnionNameResolverPlugin.ts @@ -0,0 +1,56 @@ +import ts, {Node} from "typescript"; +import {ConverterPlugin} from "../plugin.js"; +import {ConverterContext} from "../context.js"; +import {Render} from "../render.js"; +import {UnionNameResolver} from "../unionNameResolver.js"; +import {GeneratedFile} from "../generated.js"; + +export const unionNameResolverServiceKey = Symbol() + +export class UnionNameResolverService { + private readonly unionNameResolvers: UnionNameResolver[] + private readonly resolvedNodes = new Map() + + constructor(unionNameResolvers: UnionNameResolver[]) { + this.unionNameResolvers = unionNameResolvers + } + + resolveUnionName(node: Node, context: ConverterContext): string | null { + const resolvedName = this.resolvedNodes.get(node) + if (resolvedName) return resolvedName + + for (const unionNameResolver of this.unionNameResolvers) { + const result = unionNameResolver(node, context) + + if (result !== null) { + this.resolvedNodes.set(node, result) + return result + } + } + + return null + } +} + +export class UnionNameResolverPlugin implements ConverterPlugin { + private readonly unionNameResolverService: UnionNameResolverService; + + constructor(unionNameResolvers: UnionNameResolver[]) { + this.unionNameResolverService = new UnionNameResolverService(unionNameResolvers) + } + + generate(): GeneratedFile[] { + return []; + } + + render(node: Node, context: ConverterContext, next: Render): string | null { + return null; + } + + traverse(node: ts.Node, context: ConverterContext): void { + } + + setup(context: ConverterContext): void { + context.registerService(unionNameResolverServiceKey, this.unionNameResolverService) + } +} diff --git a/src/converter/unionNameResolver.ts b/src/converter/unionNameResolver.ts new file mode 100644 index 0000000..7e1310b --- /dev/null +++ b/src/converter/unionNameResolver.ts @@ -0,0 +1,5 @@ +import {ConverterContext} from "./context.js"; +import {Node} from "typescript" + +export type UnionNameResolver = + (node: TNode, context: ConverterContext) => string | null diff --git a/src/defaultPlugins.ts b/src/defaultPlugins.ts index 2696273..1e545b4 100644 --- a/src/defaultPlugins.ts +++ b/src/defaultPlugins.ts @@ -2,6 +2,7 @@ import ts, {Node, Program} from "typescript"; import {Configuration} from "./configuration/configuration.js"; import {ConverterPlugin} from "./converter/plugin.js"; import {NameResolver} from "./converter/nameResolver.js"; +import {UnionNameResolver} from "./converter/unionNameResolver.js"; import {ConfigurationPlugin} from "./converter/plugins/ConfigurationPlugin.js"; import {CheckKindsPlugin} from "./converter/plugins/CheckKindsPlugin.js"; import {CheckCoveragePlugin} from "./converter/plugins/CheckCoveragePlugin.js"; @@ -44,6 +45,7 @@ import {convertTypeOperator} from "./converter/plugins/convertTypeOperator.js"; import {convertImportType} from "./converter/plugins/convertImportType.js"; import {convertPropertyAccessExpression} from "./converter/plugins/convertPropertyAccessExpression.js"; import {NameResolverPlugin} from "./converter/plugins/NameResolverPlugin.js"; +import {UnionNameResolverPlugin} from "./converter/plugins/UnionNameResolverPlugin.js"; import {InheritanceModifierPlugin} from "./converter/plugins/InheritanceModifierPlugin.js"; import {InheritanceModifier} from "./converter/inheritanceModifier.js"; import {mappedTypePlugin} from "./converter/plugins/MappedTypePlugin.js"; @@ -72,6 +74,7 @@ export const createPlugins = ( injections: Injection[], nameResolvers: NameResolver[], inheritanceModifiers: InheritanceModifier[], + unionNameResolvers: UnionNameResolver[], varianceModifiers: VarianceModifier[], program: Program, namespaceInfo: NamespaceInfo, @@ -82,6 +85,7 @@ export const createPlugins = ( new InjectionPlugin(injections), new NameResolverPlugin(nameResolvers), new InheritanceModifierPlugin(inheritanceModifiers), + new UnionNameResolverPlugin(unionNameResolvers), new VarianceModifierPlugin(varianceModifiers), new NamespaceInfoPlugin(namespaceInfo), new ImportInfoPlugin(program, importInfo), diff --git a/src/generate.ts b/src/generate.ts index a31eb9b..58ce4fb 100644 --- a/src/generate.ts +++ b/src/generate.ts @@ -14,6 +14,7 @@ import {minimatch} from "minimatch"; import {createRender} from "./converter/render.js"; import {NameResolver} from "./converter/nameResolver.js"; import {AnnotationPlugin} from "./converter/plugins/AnnotationPlugin.js"; +import {UnionNameResolver} from "./converter/unionNameResolver.js"; import {InheritanceModifier} from "./converter/inheritanceModifier.js"; import {Annotation} from "./converter/annotation.js"; import {collectNamespaceInfo} from "./structure/namespace/collectNamespaceInfo.js"; @@ -98,6 +99,7 @@ export async function generate(partialConfiguration: PartialConfiguration) { annotations, nameResolvers, inheritanceModifiers, + unionNameResolvers, varianceModifiers, compilerOptions, cwd, @@ -147,6 +149,12 @@ export async function generate(partialConfiguration: PartialConfiguration) { cwd, ) + const customUnionNameResolvers = await loadExtensions( + "Union Name Resolver", + unionNameResolvers, + cwd, + ) + const customVarianceModifiers = await loadExtensions( "Variance Modifier", varianceModifiers, @@ -222,6 +230,7 @@ export async function generate(partialConfiguration: PartialConfiguration) { customInjections, customNameResolvers, customInheritanceModifiers, + customUnionNameResolvers, customVarianceModifiers, program, namespaceInfo, diff --git a/src/index.ts b/src/index.ts index e514def..02faadb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,6 +26,9 @@ export type {NameResolver} from "./converter/nameResolver.js" // inheritance modifier export type {InheritanceModifier, InheritanceModifierContext} from "./converter/inheritanceModifier.js" +// union name resolver +export type {UnionNameResolver} from "./converter/unionNameResolver.js" + // variance modifier export type {VarianceModifier} from "./converter/varianceModifier.js" @@ -58,6 +61,10 @@ export { inheritanceModifierServiceKey, type InheritanceModifierService } from "./converter/plugins/InheritanceModifierPlugin.js" +export { + unionNameResolverServiceKey, + type UnionNameResolverService +} from "./converter/plugins/UnionNameResolverPlugin.js" export { varianceModifierServiceKey, type VarianceModifierService diff --git a/test/functional/base/generated/union/stringEnum.kt b/test/functional/base/generated/union/stringEnum.kt index dbe6afc..0814855 100644 --- a/test/functional/base/generated/union/stringEnum.kt +++ b/test/functional/base/generated/union/stringEnum.kt @@ -31,7 +31,41 @@ val __4: Operator @seskar.js.JsValue("<=") val __5: Operator @seskar.js.JsValue(">=") -val __6: Operator +val GTE: Operator +} +} + +sealed external interface Operator2 { +companion object { +@seskar.js.JsValue("") +val EMPTY: Operator2 +@seskar.js.JsValue("=") +val EQUAL: Operator2 +@seskar.js.JsValue("<") +val LT: Operator2 +@seskar.js.JsValue(">") +val GT: Operator2 +@seskar.js.JsValue("<=") +val LTE: Operator2 +@seskar.js.JsValue(">=") +val GTE: Operator2 +} +} + +sealed external interface Operator3 { +companion object { +@seskar.js.JsValue("") +val OPERATOR: Operator3 +@seskar.js.JsValue("=") +val OPERATOR_2: Operator3 +@seskar.js.JsValue("<") +val OPERATOR_3: Operator3 +@seskar.js.JsValue(">") +val OPERATOR_4: Operator3 +@seskar.js.JsValue("<=") +val OPERATOR_5: Operator3 +@seskar.js.JsValue(">=") +val OPERATOR_6: Operator3 } } sealed external interface SwitcherResult { diff --git a/test/functional/base/karakum/unionNameResolvers/resolveOperatorNames.js b/test/functional/base/karakum/unionNameResolvers/resolveOperatorNames.js new file mode 100644 index 0000000..d69a11a --- /dev/null +++ b/test/functional/base/karakum/unionNameResolvers/resolveOperatorNames.js @@ -0,0 +1,33 @@ +import ts from "typescript"; + +export default (node) => { + if (!ts.isStringLiteral(node)) return null + if (!ts.isLiteralTypeNode(node.parent)) return null + if (!ts.isUnionTypeNode(node.parent.parent)) return null + if (!ts.isTypeAliasDeclaration(node.parent.parent.parent)) return null + if (!ts.isIdentifier(node.parent.parent.parent.name)) return null + + const name = node.parent.parent.parent.name.text + if (name === "Operator2") { + switch (node.text) { + case "": + return "EMPTY" + case "=": + return "EQUAL" + case "<": + return "LT" + case ">": + return "GT" + case "<=": + return "LTE" + } + } else if (name === "Operator3") { + return "OPERATOR" + } + + if (name.startsWith("Operator") && (node.text === ">=")) { + return "GTE" + } + + return null +} diff --git a/test/functional/base/lib/union/stringEnum.d.ts b/test/functional/base/lib/union/stringEnum.d.ts index daa8af0..ab3b0fb 100644 --- a/test/functional/base/lib/union/stringEnum.d.ts +++ b/test/functional/base/lib/union/stringEnum.d.ts @@ -3,3 +3,7 @@ export declare type FormEncType = "application/x-www-form-urlencoded" | "multipa export declare function switcher(): "on" | "off" export declare type Operator = "" | "=" | "<" | ">" | "<=" | ">="; + +export declare type Operator2 = "" | "=" | "<" | ">" | "<=" | ">="; + +export declare type Operator3 = "" | "=" | "<" | ">" | "<=" | ">=";