Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to name the constants of a string union by configuration #23

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions schema/karakum-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@
],
"title": "plugins"
},
"unionNameResolvers": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "string"
}
],
"title": "unionNameResolvers"
},
"varianceModifiers": {
"anyOf": [
{
Expand Down
3 changes: 3 additions & 0 deletions src/configuration/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export interface SchemaConfiguration {
* */
packageNameMapper?: Record<string, string>

unionNameResolvers?: string | string[]

/**
* @TJS-type object
* @additionalProperties { "type": "array", "items": { "type": "string" } }
Expand Down Expand Up @@ -118,6 +120,7 @@ export interface Configuration extends PartialConfiguration {

moduleNameMapper: Record<string, string>
packageNameMapper: Record<string, string>
unionNameResolvers: string[]

importInjector: Record<string, string[]>
importMapper: Record<string, string | Record<string, string>>
Expand Down
6 changes: 6 additions & 0 deletions src/configuration/defaultizeConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ const defaultNameResolverPatterns = [
"karakum/nameResolvers/*.js"
]

const defaultUnionNameResolverPatterns = [
"karakum/unionNameResolvers/*.js"
]

const defaultInheritanceModifierPatterns = [
"karakum/inheritanceModifiers/*.js"
]
Expand Down Expand Up @@ -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 ?? {},
Expand Down
51 changes: 24 additions & 27 deletions src/converter/plugins/StringUnionTypePlugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ts, {LiteralTypeNode, StringLiteral, UnionTypeNode} from "typescript";
import {identifier} from "../../utils/strings.js";
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";
Expand Down Expand Up @@ -48,6 +49,8 @@ export function convertStringUnionType(
const checkCoverageService = context.lookupService<CheckCoverageService>(checkCoverageServiceKey)
checkCoverageService?.cover(node)

const unionNameResolverService = context.lookupService<UnionNameResolverService>(unionNameResolverServiceKey)
if (unionNameResolverService === undefined) throw new Error("UnionNameResolverService required")
const typeScriptService = context.lookupService<TypeScriptService>(typeScriptServiceKey)
const namespaceInfoService = context.lookupService<NamespaceInfoService>(namespaceInfoServiceKey)
const injectionService = context.lookupService<InjectionService>(injectionServiceKey)
Expand All @@ -69,26 +72,31 @@ export function convertStringUnionType(
checkCoverageService?.cover(literal)

const value = literal.text
const key = value === ""
? "`_`"
: identifier(value)

let key = unionNameResolverService.resolveUnionName(literal, context)

if (!key) {
const valueAsIdentifier = notEscapedIdentifier(value)
key = (value === "") || (valueAsIdentifier === "")
? "_"
: valueAsIdentifier
}

return [key, value] as const
})

const existingKeys = new Set<string>()
const uniqueEntries: [string, string][] = []
const duplicatedEntries: [string, string][] = []

for (const [key, value] of entries) {
if (!existingKeys.has(key)) {
uniqueEntries.push([key, value])
existingKeys.add(key)
const keyDisambiguators: Map<string, number> = new Map()
const disambiguatedEntries = entries.map(([key, value]) => {
const keyDisambiguator = (keyDisambiguators.get(key) ?? 0) + 1
keyDisambiguators.set(key, keyDisambiguator)
if (keyDisambiguator > 1) {
return [escapeIdentifier(`${key}_${keyDisambiguator}`), value] as const;
} else {
duplicatedEntries.push([key, value])
return [escapeIdentifier(key), value] as const;
}
}
});

const body = uniqueEntries
const body = disambiguatedEntries
.map(([key, value]) => (
`
@seskar.js.JsValue("${value}")
Expand All @@ -97,10 +105,6 @@ val ${key}: ${name}
))
.join("\n")

const comment = duplicatedEntries
.map(([key, value]) => `${key} for "${value}"`)
.join("\n")

const heritageInjections = injectionService?.resolveInjections(node, InjectionType.HERITAGE_CLAUSE, context, render)

const namespace = typeScriptService?.findClosest(node, ts.isModuleDeclaration)
Expand All @@ -118,14 +122,7 @@ val ${key}: ${name}
const declaration = `
sealed ${externalModifier}interface ${name}${ifPresent(injectedHeritageClauses, it => ` : ${it}`)} {
companion object {
${body}${ifPresent(comment, it => (
"\n" + `
/*
Duplicated names were generated:
${it}
*/
`.trim()
))}
${body}
}
}
`.trim()
Expand Down
56 changes: 56 additions & 0 deletions src/converter/plugins/UnionNameResolverPlugin.ts
Original file line number Diff line number Diff line change
@@ -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<Node, string>()

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)
}
}
5 changes: 5 additions & 0 deletions src/converter/unionNameResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {ConverterContext} from "./context.js";
import {Node} from "typescript"

export type UnionNameResolver<TNode extends Node = Node> =
(node: TNode, context: ConverterContext) => string | null
4 changes: 4 additions & 0 deletions src/defaultPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -72,6 +74,7 @@ export const createPlugins = (
injections: Injection[],
nameResolvers: NameResolver[],
inheritanceModifiers: InheritanceModifier[],
unionNameResolvers: UnionNameResolver[],
varianceModifiers: VarianceModifier[],
program: Program,
namespaceInfo: NamespaceInfo,
Expand All @@ -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),
Expand Down
9 changes: 9 additions & 0 deletions src/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -98,6 +99,7 @@ export async function generate(partialConfiguration: PartialConfiguration) {
annotations,
nameResolvers,
inheritanceModifiers,
unionNameResolvers,
varianceModifiers,
compilerOptions,
cwd,
Expand Down Expand Up @@ -147,6 +149,12 @@ export async function generate(partialConfiguration: PartialConfiguration) {
cwd,
)

const customUnionNameResolvers = await loadExtensions<UnionNameResolver>(
"Union Name Resolver",
unionNameResolvers,
cwd,
)

const customVarianceModifiers = await loadExtensions<VarianceModifier>(
"Variance Modifier",
varianceModifiers,
Expand Down Expand Up @@ -222,6 +230,7 @@ export async function generate(partialConfiguration: PartialConfiguration) {
customInjections,
customNameResolvers,
customInheritanceModifiers,
customUnionNameResolvers,
customVarianceModifiers,
program,
namespaceInfo,
Expand Down
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/utils/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,22 @@ export function escapeIdentifier(string: string) {
return `\`${string}\``
}

if (/^_+$/.test(string)) {
return `\`${string}\``
}

return string
}

export function notEscapedIdentifier(string: string) {
return camelize(
string.replace(/\W/g, "-")
)
}

export function identifier(string: string) {
return escapeIdentifier(
camelize(
string.replace(/\W/g, "-")
)
notEscapedIdentifier(string)
)
}

Expand Down
6 changes: 2 additions & 4 deletions test/functional/base/generated/union/duplicates.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,7 @@ sealed external interface Enc {
companion object {
@seskar.js.JsValue("utf8")
val utf8: Enc
/*
Duplicated names were generated:
utf8 for "utf-8"
*/
@seskar.js.JsValue("utf-8")
val utf8_2: Enc
}
}
51 changes: 51 additions & 0 deletions test/functional/base/generated/union/stringEnum.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,57 @@ val multipartFormData: FormEncType
}

external fun switcher(): SwitcherResult

sealed external interface Operator {
companion object {
@seskar.js.JsValue("")
val `_`: Operator
@seskar.js.JsValue("=")
val __2: Operator
@seskar.js.JsValue("<")
val __3: Operator
@seskar.js.JsValue(">")
val __4: Operator
@seskar.js.JsValue("<=")
val __5: Operator
@seskar.js.JsValue(">=")
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 {
companion object {
@seskar.js.JsValue("on")
Expand Down
Loading