mirror of
https://github.com/apple/pkl.git
synced 2026-03-23 09:31:06 +01:00
Initial commit
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import java.io.IOException
|
||||
import org.pkl.commons.cli.CliCommand
|
||||
import org.pkl.commons.cli.CliException
|
||||
import org.pkl.commons.createParentDirectories
|
||||
import org.pkl.commons.writeString
|
||||
import org.pkl.core.ModuleSource
|
||||
import org.pkl.core.module.ModuleKeyFactories
|
||||
|
||||
/** API for the Kotlin code generator CLI. */
|
||||
class CliKotlinCodeGenerator(private val options: CliKotlinCodeGeneratorOptions) :
|
||||
CliCommand(options.base) {
|
||||
|
||||
override fun doRun() {
|
||||
val builder = evaluatorBuilder()
|
||||
|
||||
try {
|
||||
builder.build().use { evaluator ->
|
||||
for (moduleUri in options.base.normalizedSourceModules) {
|
||||
val schema = evaluator.evaluateSchema(ModuleSource.uri(moduleUri))
|
||||
val codeGenerator = KotlinCodeGenerator(schema, options.toKotlinCodegenOptions())
|
||||
try {
|
||||
for ((fileName, fileContents) in codeGenerator.output) {
|
||||
val outputFile = options.outputDir.resolve(fileName)
|
||||
try {
|
||||
outputFile.createParentDirectories().writeString(fileContents)
|
||||
} catch (e: IOException) {
|
||||
throw CliException("I/O error writing file `$outputFile`.\nCause: ${e.message}")
|
||||
}
|
||||
}
|
||||
} catch (e: KotlinCodeGeneratorException) {
|
||||
throw CliException(e.message!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
ModuleKeyFactories.closeQuietly(builder.moduleKeyFactories)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import java.nio.file.Path
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
|
||||
/** Configuration options for [CliKotlinCodeGenerator]. */
|
||||
data class CliKotlinCodeGeneratorOptions(
|
||||
/** Base options shared between CLI commands. */
|
||||
val base: CliBaseOptions,
|
||||
|
||||
/** The directory where generated source code is placed. */
|
||||
val outputDir: Path,
|
||||
|
||||
/** The characters to use for indenting generated source code. */
|
||||
val indent: String = " ",
|
||||
|
||||
/** Whether to generate Kdoc based on doc comments for Pkl modules, classes, and properties. */
|
||||
val generateKdoc: Boolean = false,
|
||||
|
||||
/** Whether to generate config classes for use with Spring Boot. */
|
||||
val generateSpringBootConfig: Boolean = false,
|
||||
|
||||
/** Whether to make generated classes implement [java.io.Serializable] */
|
||||
val implementSerializable: Boolean = false
|
||||
) {
|
||||
fun toKotlinCodegenOptions(): KotlinCodegenOptions =
|
||||
KotlinCodegenOptions(indent, generateKdoc, generateSpringBootConfig)
|
||||
}
|
||||
@@ -0,0 +1,788 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import com.squareup.kotlinpoet.*
|
||||
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
|
||||
import java.io.StringWriter
|
||||
import java.net.URI
|
||||
import java.util.*
|
||||
import org.pkl.core.*
|
||||
import org.pkl.core.util.CodeGeneratorUtils
|
||||
|
||||
data class KotlinCodegenOptions(
|
||||
/** The characters to use for indenting generated Kotlin code. */
|
||||
val indent: String = " ",
|
||||
|
||||
/** Whether to generate KDoc based on doc comments for Pkl modules, classes, and properties. */
|
||||
val generateKdoc: Boolean = false,
|
||||
|
||||
/** Whether to generate config classes for use with Spring Boot. */
|
||||
val generateSpringBootConfig: Boolean = false,
|
||||
|
||||
/** Whether to make generated classes implement [java.io.Serializable] */
|
||||
val implementSerializable: Boolean = false
|
||||
)
|
||||
|
||||
class KotlinCodeGeneratorException(message: String) : RuntimeException(message)
|
||||
|
||||
/** Entrypoint for the Kotlin code generator API. */
|
||||
class KotlinCodeGenerator(
|
||||
/** The schema for the module to generate */
|
||||
private val moduleSchema: ModuleSchema,
|
||||
|
||||
/** The options to use for the code generator */
|
||||
private val options: KotlinCodegenOptions,
|
||||
) {
|
||||
companion object {
|
||||
// Prevent class name from being replaced with shaded name
|
||||
// when pkl-codegen-kotlin is shaded and embedded in pkl-tools
|
||||
// (requires circumventing kotlinc constant folding).
|
||||
private val KOTLIN_TEXT_PACKAGE_NAME = buildString {
|
||||
append("kot")
|
||||
append("lin.")
|
||||
append("text")
|
||||
}
|
||||
|
||||
// `StringBuilder::class.asClassName()` generates "java.lang.StringBuilder",
|
||||
// apparently because `StringBuilder` is an `expect class`.
|
||||
private val STRING_BUILDER = ClassName(KOTLIN_TEXT_PACKAGE_NAME, "StringBuilder")
|
||||
|
||||
private val STRING = String::class.asClassName()
|
||||
private val ANY_NULL = ANY.copy(nullable = true)
|
||||
private val NOTHING = Nothing::class.asClassName()
|
||||
private val KOTLIN_PAIR = kotlin.Pair::class.asClassName()
|
||||
private val COLLECTION = Collection::class.asClassName()
|
||||
private val LIST = List::class.asClassName()
|
||||
private val SET = Set::class.asClassName()
|
||||
private val MAP = Map::class.asClassName()
|
||||
private val DURATION = Duration::class.asClassName()
|
||||
private val DURATION_UNIT = DurationUnit::class.asClassName()
|
||||
private val DATA_SIZE = DataSize::class.asClassName()
|
||||
private val DATA_SIZE_UNIT = DataSizeUnit::class.asClassName()
|
||||
private val PMODULE = PModule::class.asClassName()
|
||||
private val PCLASS = PClass::class.asClassName()
|
||||
private val REGEX = Regex::class.asClassName()
|
||||
private val URI = URI::class.asClassName()
|
||||
private val VERSION = Version::class.asClassName()
|
||||
|
||||
private const val PROPERTY_PREFIX: String = "org.pkl.config.java.mapper."
|
||||
}
|
||||
|
||||
val output: Map<String, String>
|
||||
get() {
|
||||
return mapOf(kotlinFileName to kotlinFile, propertyFileName to propertiesFile)
|
||||
}
|
||||
|
||||
private val propertyFileName: String
|
||||
get() =
|
||||
"resources/META-INF/org/pkl/config/java/mapper/classes/${moduleSchema.moduleName}.properties"
|
||||
|
||||
private val propertiesFile: String
|
||||
get() {
|
||||
val props = Properties()
|
||||
props["$PROPERTY_PREFIX${moduleSchema.moduleClass.qualifiedName}"] =
|
||||
moduleSchema.moduleClass.toKotlinPoetName().reflectionName()
|
||||
for (pClass in moduleSchema.classes.values) {
|
||||
props["$PROPERTY_PREFIX${pClass.qualifiedName}"] =
|
||||
pClass.toKotlinPoetName().reflectionName()
|
||||
}
|
||||
return StringWriter()
|
||||
.apply { props.store(this, "Kotlin mappings for Pkl module `${moduleSchema.moduleName}`") }
|
||||
.toString()
|
||||
}
|
||||
|
||||
val kotlinFileName: String
|
||||
get() = buildString {
|
||||
append("kot")
|
||||
append("lin/${relativeOutputPathFor(moduleSchema.moduleName)}")
|
||||
}
|
||||
|
||||
val kotlinFile: String
|
||||
get() {
|
||||
if (moduleSchema.moduleUri.scheme == "pkl") {
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Cannot generate Kotlin code for a Pkl standard library module (`${moduleSchema.moduleUri}`)."
|
||||
)
|
||||
}
|
||||
|
||||
val pModuleClass = moduleSchema.moduleClass
|
||||
|
||||
val hasProperties = pModuleClass.properties.any { !it.value.isHidden }
|
||||
val isGenerateClass = hasProperties || pModuleClass.isOpen || pModuleClass.isAbstract
|
||||
val moduleType =
|
||||
if (isGenerateClass) {
|
||||
generateTypeSpec(pModuleClass, moduleSchema)
|
||||
} else {
|
||||
generateObjectSpec(pModuleClass)
|
||||
}
|
||||
|
||||
for (pClass in moduleSchema.classes.values) {
|
||||
moduleType.addType(generateTypeSpec(pClass, moduleSchema).ensureSerializable().build())
|
||||
}
|
||||
|
||||
// generate append method for module classes w/o parent class; reuse in subclasses and nested
|
||||
// classes
|
||||
val isGenerateAppendPropertyMethod =
|
||||
// check if we can inherit someone else's append method
|
||||
pModuleClass.superclass!!.info == PClassInfo.Module &&
|
||||
// check if anyone is (potentially) going to use our append method
|
||||
(pModuleClass.isOpen ||
|
||||
pModuleClass.isAbstract ||
|
||||
(hasProperties && !moduleType.modifiers.contains(KModifier.DATA)) ||
|
||||
moduleType.typeSpecs.any { !it.modifiers.contains(KModifier.DATA) })
|
||||
|
||||
if (isGenerateAppendPropertyMethod) {
|
||||
val appendPropertyMethodModifier =
|
||||
if (pModuleClass.isOpen || pModuleClass.isAbstract) {
|
||||
// alternative is `@JvmStatic protected`
|
||||
// (`protected` alone isn't sufficient as of Kotlin 1.6)
|
||||
KModifier.PUBLIC
|
||||
} else KModifier.PRIVATE
|
||||
if (isGenerateClass) {
|
||||
moduleType.addType(
|
||||
TypeSpec.companionObjectBuilder()
|
||||
.addFunction(
|
||||
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
|
||||
)
|
||||
.build()
|
||||
)
|
||||
} else { // kotlin object
|
||||
moduleType.addFunction(
|
||||
appendPropertyMethod().addModifiers(appendPropertyMethodModifier).build()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val moduleName = moduleSchema.moduleName
|
||||
val index = moduleName.lastIndexOf(".")
|
||||
val packageName = if (index == -1) "" else moduleName.substring(0, index)
|
||||
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
|
||||
|
||||
val fileSpec = FileSpec.builder(packageName, moduleTypeName).indent(options.indent)
|
||||
|
||||
for (typeAlias in moduleSchema.typeAliases.values) {
|
||||
if (typeAlias.aliasedType is PType.Alias) {
|
||||
// generate top-level type alias (Kotlin doesn't support nested type aliases)
|
||||
fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build())
|
||||
} else {
|
||||
val stringLiterals = mutableSetOf<String>()
|
||||
if (CodeGeneratorUtils.isRepresentableAsEnum(typeAlias.aliasedType, stringLiterals)) {
|
||||
// generate nested enum class
|
||||
moduleType.addType(generateEnumTypeSpec(typeAlias, stringLiterals).build())
|
||||
} else {
|
||||
// generate top-level type alias (Kotlin doesn't support nested type aliases)
|
||||
fileSpec.addTypeAlias(generateTypeAliasSpec(typeAlias).build())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileSpec.addType(moduleType.build())
|
||||
return fileSpec.build().toString()
|
||||
}
|
||||
|
||||
private fun relativeOutputPathFor(moduleName: String): String {
|
||||
val nameParts = moduleName.split(".")
|
||||
val dirPath = nameParts.dropLast(1).joinToString("/")
|
||||
val fileName = nameParts.last().replaceFirstChar { it.titlecaseChar() }
|
||||
return if (dirPath.isEmpty()) {
|
||||
"$fileName.kt"
|
||||
} else {
|
||||
"$dirPath/$fileName.kt"
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateObjectSpec(pClass: PClass): TypeSpec.Builder {
|
||||
val builder = TypeSpec.objectBuilder(pClass.toKotlinPoetName())
|
||||
val docComment = pClass.docComment
|
||||
if (docComment != null && options.generateKdoc) {
|
||||
builder.addKdoc(renderAsKdoc(docComment))
|
||||
}
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun generateTypeSpec(pClass: PClass, schema: ModuleSchema): TypeSpec.Builder {
|
||||
val isModuleClass = pClass == schema.moduleClass
|
||||
val kotlinPoetClassName = pClass.toKotlinPoetName()
|
||||
val superclass =
|
||||
pClass.superclass?.takeIf { it.info != PClassInfo.Typed && it.info != PClassInfo.Module }
|
||||
val superProperties = superclass?.allProperties?.filterValues { !it.isHidden } ?: mapOf()
|
||||
val properties = pClass.properties.filterValues { !it.isHidden }
|
||||
val allProperties = superProperties + properties
|
||||
|
||||
fun PClass.Property.isRegex(): Boolean =
|
||||
(this.type as? PType.Class)?.pClass?.info == PClassInfo.Regex
|
||||
|
||||
val containRegexProperty = properties.values.any { it.isRegex() }
|
||||
|
||||
fun generateConstructor(): FunSpec {
|
||||
val builder = FunSpec.constructorBuilder()
|
||||
for ((name, property) in allProperties) {
|
||||
builder.addParameter(name, property.type.toKotlinPoetName())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun generateCopyMethod(parameters: Map<String, PClass.Property>, isOverride: Boolean): FunSpec {
|
||||
val methodBuilder = FunSpec.builder("copy").returns(kotlinPoetClassName)
|
||||
|
||||
if (isOverride) {
|
||||
methodBuilder.addModifiers(KModifier.OVERRIDE)
|
||||
}
|
||||
if (pClass.isOpen || pClass.isAbstract) {
|
||||
methodBuilder.addModifiers(KModifier.OPEN)
|
||||
}
|
||||
|
||||
for ((name, property) in parameters) {
|
||||
val paramBuilder = ParameterSpec.builder(name, property.type.toKotlinPoetName())
|
||||
if (!isOverride) {
|
||||
paramBuilder.defaultValue("this.%N", name)
|
||||
}
|
||||
methodBuilder.addParameter(paramBuilder.build())
|
||||
}
|
||||
|
||||
val codeBuilder = CodeBlock.builder().add("return %T(", kotlinPoetClassName)
|
||||
var firstProperty = true
|
||||
for (name in allProperties.keys) {
|
||||
if (firstProperty) {
|
||||
codeBuilder.add("%N", name)
|
||||
firstProperty = false
|
||||
} else {
|
||||
codeBuilder.add(", %N", name)
|
||||
}
|
||||
}
|
||||
codeBuilder.add(")\n")
|
||||
|
||||
return methodBuilder.addCode(codeBuilder.build()).build()
|
||||
}
|
||||
|
||||
// besides generating copy method for current class,
|
||||
// override copy methods inherited from parent classes
|
||||
fun generateCopyMethods(typeBuilder: TypeSpec.Builder) {
|
||||
var prevParameterCount = Int.MAX_VALUE
|
||||
for (currClass in generateSequence(pClass) { it.superclass }) {
|
||||
if (currClass.isAbstract) continue
|
||||
|
||||
val currParameters = currClass.allProperties.filter { !it.value.isHidden }
|
||||
|
||||
// avoid generating multiple methods with same no. of parameters
|
||||
if (currParameters.size < prevParameterCount) {
|
||||
val isOverride = currClass !== pClass || superclass != null && properties.isEmpty()
|
||||
typeBuilder.addFunction(generateCopyMethod(currParameters, isOverride))
|
||||
prevParameterCount = currParameters.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateEqualsMethod(): FunSpec {
|
||||
val builder =
|
||||
FunSpec.builder("equals")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addParameter("other", ANY_NULL)
|
||||
.returns(BOOLEAN)
|
||||
.addStatement("if (this === other) return true")
|
||||
// generating this.javaClass instead of class literal avoids a SpotBugs warning
|
||||
.addStatement("if (this.javaClass != other?.javaClass) return false")
|
||||
.addStatement("other as %T", kotlinPoetClassName)
|
||||
|
||||
for ((propertyName, property) in allProperties) {
|
||||
val accessor = if (property.isRegex()) "%N.pattern" else "%N"
|
||||
builder.addStatement(
|
||||
"if (this.$accessor != other.$accessor) return false",
|
||||
propertyName,
|
||||
propertyName
|
||||
)
|
||||
}
|
||||
|
||||
builder.addStatement("return true")
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun generateHashCodeMethod(): FunSpec {
|
||||
val builder =
|
||||
FunSpec.builder("hashCode")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.returns(INT)
|
||||
.addStatement("var result = 1")
|
||||
|
||||
for (propertyName in allProperties.keys) {
|
||||
// use Objects.hashCode() because Kotlin's Any?.hashCode()
|
||||
// doesn't work for platform types (will get NPE if null)
|
||||
builder.addStatement(
|
||||
"result = 31 * result + %T.hashCode(this.%N)",
|
||||
Objects::class,
|
||||
propertyName
|
||||
)
|
||||
}
|
||||
|
||||
builder.addStatement("return result")
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun generateToStringMethod(): FunSpec {
|
||||
val builder = FunSpec.builder("toString").addModifiers(KModifier.OVERRIDE).returns(STRING)
|
||||
|
||||
var builderSize = 50
|
||||
val appendBuilder = CodeBlock.builder()
|
||||
for (propertyName in allProperties.keys) {
|
||||
builderSize += 50
|
||||
appendBuilder.addStatement(
|
||||
"appendProperty(builder, %S, this.%N)",
|
||||
propertyName,
|
||||
propertyName
|
||||
)
|
||||
}
|
||||
|
||||
builder
|
||||
.addStatement("val builder = %T(%L)", STRING_BUILDER, builderSize)
|
||||
.addStatement(
|
||||
// generate `::class.java.simpleName` instead of `::class.simpleName`
|
||||
// to avoid making user code depend on kotlin-reflect
|
||||
"builder.append(%T::class.java.simpleName).append(\" {\")",
|
||||
kotlinPoetClassName
|
||||
)
|
||||
.addCode(appendBuilder.build())
|
||||
// not using %S here because it generates `"\n" + "{"`
|
||||
// with a line break in the generated code after `+`
|
||||
.addStatement("builder.append(\"\\n}\")")
|
||||
.addStatement("return builder.toString()")
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun generateDeprecation(
|
||||
annotations: Collection<PObject>,
|
||||
addAnnotation: (AnnotationSpec) -> Unit
|
||||
) {
|
||||
annotations
|
||||
.firstOrNull { it.classInfo == PClassInfo.Deprecated }
|
||||
?.let { deprecation ->
|
||||
val builder = AnnotationSpec.builder(Deprecated::class)
|
||||
(deprecation["message"] as String?)?.let { builder.addMember("message = %S", it) }
|
||||
addAnnotation(builder.build())
|
||||
}
|
||||
}
|
||||
|
||||
fun generateProperty(propertyName: String, property: PClass.Property): PropertySpec {
|
||||
val typeName = property.type.toKotlinPoetName()
|
||||
val builder = PropertySpec.builder(propertyName, typeName).initializer("%L", propertyName)
|
||||
|
||||
generateDeprecation(property.annotations) { builder.addAnnotation(it) }
|
||||
|
||||
val docComment = property.docComment
|
||||
if (docComment != null && options.generateKdoc) {
|
||||
builder.addKdoc(renderAsKdoc(docComment))
|
||||
}
|
||||
if (propertyName in superProperties) {
|
||||
builder.addModifiers(KModifier.OVERRIDE)
|
||||
}
|
||||
if (pClass.isOpen || pClass.isAbstract) {
|
||||
builder.addModifiers(KModifier.OPEN)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
fun generateSpringBootAnnotations(builder: TypeSpec.Builder) {
|
||||
builder.addAnnotation(
|
||||
ClassName("org.springframework.boot.context.properties", "ConstructorBinding")
|
||||
)
|
||||
|
||||
if (isModuleClass) {
|
||||
builder.addAnnotation(
|
||||
ClassName("org.springframework.boot.context.properties", "ConfigurationProperties")
|
||||
)
|
||||
} else {
|
||||
// not very efficient to repeat computing module property base types for every class
|
||||
val modulePropertiesWithMatchingType =
|
||||
schema.moduleClass.allProperties.values.filter { property ->
|
||||
var propertyType = property.type
|
||||
while (propertyType is PType.Constrained || propertyType is PType.Nullable) {
|
||||
if (propertyType is PType.Constrained) {
|
||||
propertyType = propertyType.baseType
|
||||
} else if (propertyType is PType.Nullable) {
|
||||
propertyType = propertyType.baseType
|
||||
}
|
||||
}
|
||||
propertyType is PType.Class && propertyType.pClass == pClass
|
||||
}
|
||||
if (modulePropertiesWithMatchingType.size == 1) {
|
||||
// exactly one module property has this type -> make it available for direct injection
|
||||
// (potential improvement: make type available for direct injection if it occurs exactly
|
||||
// once in property tree)
|
||||
builder.addAnnotation(
|
||||
AnnotationSpec.builder(
|
||||
ClassName("org.springframework.boot.context.properties", "ConfigurationProperties")
|
||||
)
|
||||
// use "value" instead of "prefix" to entice JavaPoet to generate a single-line
|
||||
// annotation
|
||||
// that can easily be filtered out by JavaCodeGeneratorTest.`spring boot config`
|
||||
.addMember("%S", modulePropertiesWithMatchingType.first().simpleName)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateRegularClass(): TypeSpec.Builder {
|
||||
val builder = TypeSpec.classBuilder(kotlinPoetClassName)
|
||||
|
||||
if (options.generateSpringBootConfig) {
|
||||
generateSpringBootAnnotations(builder)
|
||||
}
|
||||
|
||||
builder.primaryConstructor(generateConstructor())
|
||||
|
||||
val docComment = pClass.docComment
|
||||
if (docComment != null && options.generateKdoc) {
|
||||
builder.addKdoc(renderAsKdoc(docComment))
|
||||
}
|
||||
|
||||
if (pClass.isAbstract) {
|
||||
builder.addModifiers(KModifier.ABSTRACT)
|
||||
} else if (pClass.isOpen) {
|
||||
builder.addModifiers(KModifier.OPEN)
|
||||
}
|
||||
|
||||
superclass?.let { superclass ->
|
||||
val superclassName = superclass.toKotlinPoetName()
|
||||
builder.superclass(superclassName)
|
||||
for (propertyName in superProperties.keys) {
|
||||
builder.addSuperclassConstructorParameter(propertyName)
|
||||
}
|
||||
}
|
||||
|
||||
for ((name, property) in properties) {
|
||||
builder.addProperty(generateProperty(name, property))
|
||||
}
|
||||
|
||||
generateCopyMethods(builder)
|
||||
|
||||
builder
|
||||
.addFunction(generateEqualsMethod())
|
||||
.addFunction(generateHashCodeMethod())
|
||||
.addFunction(generateToStringMethod())
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
fun generateDataClass(): TypeSpec.Builder {
|
||||
val builder = TypeSpec.classBuilder(kotlinPoetClassName).addModifiers(KModifier.DATA)
|
||||
|
||||
if (options.generateSpringBootConfig) {
|
||||
generateSpringBootAnnotations(builder)
|
||||
}
|
||||
|
||||
builder.primaryConstructor(generateConstructor())
|
||||
|
||||
generateDeprecation(pClass.annotations) { builder.addAnnotation(it) }
|
||||
|
||||
val docComment = pClass.docComment
|
||||
if (docComment != null && options.generateKdoc) {
|
||||
builder.addKdoc(renderAsKdoc(docComment))
|
||||
}
|
||||
|
||||
for ((name, property) in properties) {
|
||||
builder.addProperty(generateProperty(name, property))
|
||||
}
|
||||
|
||||
// Regex requires special approach when compared to another Regex
|
||||
// So we need to override `.equals` method even for kotlin's `data class`es if
|
||||
// any of the properties is of Regex type
|
||||
if (containRegexProperty) {
|
||||
builder.addFunction(generateEqualsMethod())
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
return if (superclass == null && !pClass.isAbstract && !pClass.isOpen) generateDataClass()
|
||||
else generateRegularClass()
|
||||
}
|
||||
|
||||
private fun TypeSpec.Builder.ensureSerializable(): TypeSpec.Builder {
|
||||
if (!options.implementSerializable) {
|
||||
return this
|
||||
}
|
||||
|
||||
if (!this.superinterfaces.containsKey(java.io.Serializable::class.java.asTypeName())) {
|
||||
this.addSuperinterface(java.io.Serializable::class.java)
|
||||
}
|
||||
|
||||
var useExistingCompanionBuilder = false
|
||||
val companionBuilder =
|
||||
this.typeSpecs
|
||||
.find { it.isCompanion }
|
||||
?.let {
|
||||
useExistingCompanionBuilder = true
|
||||
it.toBuilder(TypeSpec.Kind.OBJECT)
|
||||
}
|
||||
?: TypeSpec.companionObjectBuilder()
|
||||
|
||||
if (!companionBuilder.propertySpecs.any { it.name == "serialVersionUID" })
|
||||
companionBuilder.addProperty(
|
||||
PropertySpec.builder(
|
||||
"serialVersionUID",
|
||||
Long::class.java,
|
||||
KModifier.PRIVATE,
|
||||
KModifier.CONST
|
||||
)
|
||||
.initializer("0L")
|
||||
.build()
|
||||
)
|
||||
|
||||
if (!useExistingCompanionBuilder) {
|
||||
this.addType(companionBuilder.build())
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
private fun generateEnumTypeSpec(
|
||||
typeAlias: TypeAlias,
|
||||
stringLiterals: Set<String>
|
||||
): TypeSpec.Builder {
|
||||
val enumConstantToPklNames =
|
||||
stringLiterals
|
||||
.groupingBy { literal ->
|
||||
CodeGeneratorUtils.toEnumConstantName(literal)
|
||||
?: throw KotlinCodeGeneratorException(
|
||||
"Cannot generate Kotlin enum class for Pkl type alias `${typeAlias.displayName}` " +
|
||||
"because string literal type \"$literal\" cannot be converted to a valid enum constant name."
|
||||
)
|
||||
}
|
||||
.reduce { enumConstantName, firstLiteral, secondLiteral ->
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Cannot generate Kotlin enum class for Pkl type alias `${typeAlias.displayName}` " +
|
||||
"because string literal types \"$firstLiteral\" and \"$secondLiteral\" " +
|
||||
"would both be converted to enum constant name `$enumConstantName`."
|
||||
)
|
||||
}
|
||||
|
||||
val builder =
|
||||
TypeSpec.enumBuilder(typeAlias.simpleName)
|
||||
.primaryConstructor(
|
||||
FunSpec.constructorBuilder().addParameter("value", String::class).build()
|
||||
)
|
||||
.addProperty(PropertySpec.builder("value", String::class).initializer("value").build())
|
||||
.addFunction(
|
||||
FunSpec.builder("toString")
|
||||
.addModifiers(KModifier.OVERRIDE)
|
||||
.addStatement("return value")
|
||||
.build()
|
||||
)
|
||||
for ((enumConstantName, pklName) in enumConstantToPklNames) {
|
||||
builder.addEnumConstant(
|
||||
enumConstantName,
|
||||
TypeSpec.anonymousClassBuilder().addSuperclassConstructorParameter("%S", pklName).build()
|
||||
)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun generateTypeAliasSpec(typeAlias: TypeAlias): TypeAliasSpec.Builder {
|
||||
val builder =
|
||||
TypeAliasSpec.builder(typeAlias.simpleName, typeAlias.aliasedType.toKotlinPoetName())
|
||||
for (typeParameter in typeAlias.typeParameters) {
|
||||
builder.addTypeVariable(
|
||||
TypeVariableName(typeParameter.name, typeParameter.variance.toKotlinPoet())
|
||||
)
|
||||
}
|
||||
|
||||
val docComment = typeAlias.docComment
|
||||
if (docComment != null && options.generateKdoc) {
|
||||
builder.addKdoc(renderAsKdoc(docComment))
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun TypeParameter.Variance.toKotlinPoet(): KModifier? =
|
||||
when (this) {
|
||||
TypeParameter.Variance.COVARIANT -> KModifier.OUT
|
||||
TypeParameter.Variance.CONTRAVARIANT -> KModifier.IN
|
||||
else -> null
|
||||
}
|
||||
|
||||
// do the minimum work necessary to avoid kotlin compile errors
|
||||
// generating idiomatic KDoc would require parsing doc comments, converting member links, etc.
|
||||
private fun renderAsKdoc(docComment: String): String = docComment
|
||||
|
||||
private fun appendPropertyMethod() =
|
||||
FunSpec.builder("appendProperty")
|
||||
.addParameter("builder", STRING_BUILDER)
|
||||
.addParameter("name", STRING)
|
||||
.addParameter("value", ANY_NULL)
|
||||
.addStatement("builder.append(\"\\n \").append(name).append(\" = \")")
|
||||
.addStatement("val lines = value.toString().split(\"\\n\")")
|
||||
.addStatement("builder.append(lines[0])")
|
||||
.beginControlFlow("for (i in 1..lines.lastIndex)")
|
||||
.addStatement("builder.append(\"\\n \").append(lines[i])")
|
||||
.endControlFlow()
|
||||
|
||||
private fun PClass.toKotlinPoetName(): ClassName {
|
||||
val index = moduleName.lastIndexOf(".")
|
||||
val packageName = if (index == -1) "" else moduleName.substring(0, index)
|
||||
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
|
||||
return if (isModuleClass) {
|
||||
ClassName(packageName, moduleTypeName)
|
||||
} else {
|
||||
ClassName(packageName, moduleTypeName, simpleName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TypeAlias.toKotlinPoetName(): ClassName {
|
||||
val index = moduleName.lastIndexOf(".")
|
||||
val packageName = if (index == -1) "" else moduleName.substring(0, index)
|
||||
|
||||
return when {
|
||||
aliasedType is PType.Alias -> {
|
||||
// Kotlin type generated for [this] is a top-level type alias
|
||||
ClassName(packageName, simpleName)
|
||||
}
|
||||
CodeGeneratorUtils.isRepresentableAsEnum(aliasedType, null) -> {
|
||||
if (isStandardLibraryMember) {
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Standard library typealias `${qualifiedName}` is not supported by Kotlin code generator." +
|
||||
" If you think this is an omission, please let us know."
|
||||
)
|
||||
}
|
||||
// Kotlin type generated for [this] is a nested enum class
|
||||
val moduleTypeName = moduleName.substring(index + 1).replaceFirstChar { it.titlecaseChar() }
|
||||
ClassName(packageName, moduleTypeName, simpleName)
|
||||
}
|
||||
else -> {
|
||||
// Kotlin type generated for [this] is a top-level type alias
|
||||
ClassName(packageName, simpleName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PType.toKotlinPoetName(): TypeName =
|
||||
when (this) {
|
||||
PType.UNKNOWN -> ANY_NULL
|
||||
PType.NOTHING -> NOTHING
|
||||
is PType.StringLiteral -> STRING
|
||||
is PType.Class -> {
|
||||
// if in doubt, spell it out
|
||||
when (val classInfo = pClass.info) {
|
||||
PClassInfo.Any -> ANY_NULL
|
||||
PClassInfo.Typed,
|
||||
PClassInfo.Dynamic -> ANY
|
||||
PClassInfo.Boolean -> BOOLEAN
|
||||
PClassInfo.String -> STRING
|
||||
// seems more useful to generate `Double` than `kotlin.Number`
|
||||
PClassInfo.Number -> DOUBLE
|
||||
PClassInfo.Int -> LONG
|
||||
PClassInfo.Float -> DOUBLE
|
||||
PClassInfo.Duration -> DURATION
|
||||
PClassInfo.DataSize -> DATA_SIZE
|
||||
PClassInfo.Pair ->
|
||||
KOTLIN_PAIR.parameterizedBy(
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(),
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName()
|
||||
)
|
||||
PClassInfo.Collection ->
|
||||
COLLECTION.parameterizedBy(
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
|
||||
)
|
||||
PClassInfo.List,
|
||||
PClassInfo.Listing ->
|
||||
LIST.parameterizedBy(
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
|
||||
)
|
||||
PClassInfo.Set ->
|
||||
SET.parameterizedBy(
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName()
|
||||
)
|
||||
PClassInfo.Map,
|
||||
PClassInfo.Mapping ->
|
||||
MAP.parameterizedBy(
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[0].toKotlinPoetName(),
|
||||
if (typeArguments.isEmpty()) ANY_NULL else typeArguments[1].toKotlinPoetName()
|
||||
)
|
||||
PClassInfo.Module -> PMODULE
|
||||
PClassInfo.Class -> PCLASS
|
||||
PClassInfo.Regex -> REGEX
|
||||
PClassInfo.Version -> VERSION
|
||||
else ->
|
||||
when {
|
||||
!classInfo.isStandardLibraryClass -> pClass.toKotlinPoetName()
|
||||
else ->
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Standard library class `${pClass.qualifiedName}` is not supported by Kotlin code generator. " +
|
||||
"If you think this is an omission, please let us know."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is PType.Nullable -> baseType.toKotlinPoetName().copy(nullable = true)
|
||||
is PType.Constrained -> baseType.toKotlinPoetName()
|
||||
is PType.Alias ->
|
||||
when (typeAlias.qualifiedName) {
|
||||
"pkl.base#NonNull" -> ANY
|
||||
// Not currently generating Kotlin unsigned types
|
||||
// because it's not clear if the benefits outweigh the drawbacks:
|
||||
// - breaking change
|
||||
// - Kotlin unsigned types aren't intended for domain modeling
|
||||
// - diverts from Java code generator
|
||||
// - doesn't increase safety
|
||||
// - range already checked on Pkl side
|
||||
// - conversion to signed type doesn't perform range check
|
||||
"pkl.base#Int8" -> BYTE
|
||||
"pkl.base#Int16",
|
||||
"pkl.base#UInt8" -> SHORT
|
||||
"pkl.base#Int32",
|
||||
"pkl.base#UInt16" -> INT
|
||||
"pkl.base#UInt",
|
||||
"pkl.base#UInt32" -> LONG
|
||||
"pkl.base#DurationUnit" -> DURATION_UNIT
|
||||
"pkl.base#DataSizeUnit" -> DATA_SIZE_UNIT
|
||||
"pkl.base#Uri" -> URI
|
||||
else -> {
|
||||
val className = typeAlias.toKotlinPoetName()
|
||||
when {
|
||||
typeAlias.typeParameters.isEmpty() -> className
|
||||
typeArguments.isEmpty() -> {
|
||||
// no type arguments provided for a type alias with type parameters -> fill in
|
||||
// `Any?` (equivalent of `unknown`)
|
||||
val typeArgs = Array(typeAlias.typeParameters.size) { ANY_NULL }
|
||||
className.parameterizedBy(*typeArgs)
|
||||
}
|
||||
else -> className.parameterizedBy(*typeArguments.toKotlinPoet())
|
||||
}
|
||||
}
|
||||
}
|
||||
is PType.Function ->
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Pkl function types are not supported by the Kotlin code generator."
|
||||
)
|
||||
is PType.Union ->
|
||||
if (CodeGeneratorUtils.isRepresentableAsString(this)) STRING
|
||||
else
|
||||
throw KotlinCodeGeneratorException(
|
||||
"Pkl union types are not supported by the Kotlin code generator."
|
||||
)
|
||||
|
||||
// occurs on RHS of generic type aliases
|
||||
is PType.TypeVariable -> TypeVariableName(typeParameter.name)
|
||||
else -> throw AssertionError("Encountered unexpected PType subclass: $this")
|
||||
}
|
||||
|
||||
private fun List<PType>.toKotlinPoet(): Array<TypeName> =
|
||||
map { it.toKotlinPoetName() }.toTypedArray()
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
@file:JvmName("Main")
|
||||
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import com.github.ajalt.clikt.parameters.options.default
|
||||
import com.github.ajalt.clikt.parameters.options.flag
|
||||
import com.github.ajalt.clikt.parameters.options.option
|
||||
import com.github.ajalt.clikt.parameters.types.path
|
||||
import java.nio.file.Path
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.commons.cli.cliMain
|
||||
import org.pkl.commons.cli.commands.ModulesCommand
|
||||
import org.pkl.commons.toPath
|
||||
import org.pkl.core.Release
|
||||
|
||||
/** Main method for the Kotlin code generator CLI. */
|
||||
internal fun main(args: Array<String>) {
|
||||
cliMain { PklKotlinCodegenCommand().main(args) }
|
||||
}
|
||||
|
||||
class PklKotlinCodegenCommand :
|
||||
ModulesCommand(
|
||||
name = "pkl-codegen-kotlin",
|
||||
helpLink = Release.current().documentation().homepage(),
|
||||
) {
|
||||
|
||||
private val defaults = CliKotlinCodeGeneratorOptions(CliBaseOptions(), "".toPath())
|
||||
|
||||
private val outputDir: Path by
|
||||
option(
|
||||
names = arrayOf("-o", "--output-dir"),
|
||||
metavar = "<path>",
|
||||
help = "The directory where generated source code is placed."
|
||||
)
|
||||
.path()
|
||||
.default(defaults.outputDir)
|
||||
|
||||
private val indent: String by
|
||||
option(
|
||||
names = arrayOf("--indent"),
|
||||
metavar = "<chars>",
|
||||
help = "The characters to use for indenting generated source code."
|
||||
)
|
||||
.default(defaults.indent)
|
||||
|
||||
private val generateKdoc: Boolean by
|
||||
option(
|
||||
names = arrayOf("--generate-kdoc"),
|
||||
help =
|
||||
"Whether to generate Kdoc based on doc comments " +
|
||||
"for Pkl modules, classes, and properties."
|
||||
)
|
||||
.flag()
|
||||
|
||||
private val generateSpringboot: Boolean by
|
||||
option(
|
||||
names = arrayOf("--generate-spring-boot"),
|
||||
help = "Whether to generate config classes for use with Spring boot."
|
||||
)
|
||||
.flag()
|
||||
|
||||
private val implementSerializable: Boolean by
|
||||
option(
|
||||
names = arrayOf("--implement-serializable"),
|
||||
help = "Whether to make generated classes implement java.io.Serializable"
|
||||
)
|
||||
.flag()
|
||||
|
||||
override fun run() {
|
||||
val options =
|
||||
CliKotlinCodeGeneratorOptions(
|
||||
base = baseOptions.baseOptions(modules, projectOptions),
|
||||
outputDir = outputDir,
|
||||
indent = indent,
|
||||
generateKdoc = generateKdoc,
|
||||
generateSpringBootConfig = generateSpringboot,
|
||||
implementSerializable = implementSerializable
|
||||
)
|
||||
CliKotlinCodeGenerator(options).run()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import java.nio.file.Path
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.cli.CliBaseOptions
|
||||
import org.pkl.commons.readString
|
||||
|
||||
class CliKotlinCodeGeneratorTest {
|
||||
@Test
|
||||
fun `module inheritance`(@TempDir tempDir: Path) {
|
||||
val module1 =
|
||||
PklModule(
|
||||
"org.mod1",
|
||||
"""
|
||||
open module org.mod1
|
||||
|
||||
pigeon: Person
|
||||
|
||||
class Person {
|
||||
name: String
|
||||
age: Int
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
val module2 =
|
||||
PklModule(
|
||||
"org.mod2",
|
||||
"""
|
||||
module org.mod2
|
||||
|
||||
extends "mod1.pkl"
|
||||
|
||||
parrot: Person
|
||||
"""
|
||||
)
|
||||
|
||||
val module1File = module1.writeToDisk(tempDir.resolve("org/mod1.pkl"))
|
||||
val module2File = module2.writeToDisk(tempDir.resolve("org/mod2.pkl"))
|
||||
val outputDir = tempDir.resolve("output")
|
||||
|
||||
val generator =
|
||||
CliKotlinCodeGenerator(
|
||||
CliKotlinCodeGeneratorOptions(
|
||||
CliBaseOptions(listOf(module1File.toUri(), module2File.toUri())),
|
||||
outputDir
|
||||
)
|
||||
)
|
||||
|
||||
generator.run()
|
||||
|
||||
val module1KotlinFile = outputDir.resolve("kotlin/org/Mod1.kt")
|
||||
assertThat(module1KotlinFile).exists()
|
||||
|
||||
val module2KotlinFile = outputDir.resolve("kotlin/org/Mod2.kt")
|
||||
assertThat(module2KotlinFile).exists()
|
||||
|
||||
assertContains(
|
||||
"""
|
||||
open class Mod1(
|
||||
open val pigeon: Person
|
||||
) {
|
||||
"""
|
||||
.trimIndent(),
|
||||
module1KotlinFile.readString()
|
||||
)
|
||||
|
||||
assertContains(
|
||||
"""
|
||||
class Mod2(
|
||||
pigeon: Mod1.Person,
|
||||
val parrot: Mod1.Person
|
||||
) : Mod1(pigeon) {
|
||||
"""
|
||||
.trimIndent(),
|
||||
module2KotlinFile.readString()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `class name clashes`(@TempDir tempDir: Path) {
|
||||
val module1 =
|
||||
PklModule(
|
||||
"org.mod1",
|
||||
"""
|
||||
module org.mod1
|
||||
|
||||
class Person {
|
||||
name: String
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
val module2 =
|
||||
PklModule(
|
||||
"org.mod2",
|
||||
"""
|
||||
module org.mod2
|
||||
|
||||
import "mod1.pkl"
|
||||
|
||||
person1: mod1.Person
|
||||
person2: Person
|
||||
|
||||
class Person {
|
||||
age: Int
|
||||
}
|
||||
"""
|
||||
)
|
||||
|
||||
val module1PklFile = module1.writeToDisk(tempDir.resolve("org/mod1.pkl"))
|
||||
val module2PklFile = module2.writeToDisk(tempDir.resolve("org/mod2.pkl"))
|
||||
val outputDir = tempDir.resolve("output")
|
||||
|
||||
val generator =
|
||||
CliKotlinCodeGenerator(
|
||||
CliKotlinCodeGeneratorOptions(
|
||||
CliBaseOptions(listOf(module1PklFile.toUri(), module2PklFile.toUri())),
|
||||
outputDir
|
||||
)
|
||||
)
|
||||
|
||||
generator.run()
|
||||
|
||||
val module2KotlinFile = outputDir.resolve("kotlin/org/Mod2.kt")
|
||||
assertContains(
|
||||
"""
|
||||
data class Mod2(
|
||||
val person1: Mod1.Person,
|
||||
val person2: Person
|
||||
)
|
||||
"""
|
||||
.trimIndent(),
|
||||
module2KotlinFile.readString()
|
||||
)
|
||||
}
|
||||
|
||||
private fun assertContains(part: String, code: String) {
|
||||
val trimmedPart = part.trim().trimMargin()
|
||||
if (!code.contains(trimmedPart)) {
|
||||
// check for equality to get better error output (ide diff dialog)
|
||||
assertThat(code).isEqualTo(trimmedPart)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import javax.script.ScriptEngineManager
|
||||
import javax.script.ScriptException
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.text.RegexOption.MULTILINE
|
||||
import kotlin.text.RegexOption.UNIX_LINES
|
||||
import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
|
||||
|
||||
class CompilationFailedException(msg: String?, cause: Throwable? = null) :
|
||||
RuntimeException(msg, cause)
|
||||
|
||||
object InMemoryKotlinCompiler {
|
||||
init {
|
||||
// prevent "Unable to load JNA library" warning
|
||||
setIdeaIoUseFallback()
|
||||
}
|
||||
|
||||
// Implementation notes:
|
||||
// * all [sourceFiles] are currently combined into a single file
|
||||
// * implementation makes assumptions about structure of generated source files
|
||||
fun compile(sourceFiles: Map<String, String>): Map<String, KClass<*>> {
|
||||
fun String.findClasses(
|
||||
prefix: String = "",
|
||||
nameGroup: Int = 2,
|
||||
bodyGroup: Int = 4,
|
||||
regex: String =
|
||||
"^(data |open |enum )?class\\s+(\\w+) *(\\([^)]*\\))?.*$\\n((^ .*\\n|^$\\n)*)",
|
||||
transform: (String, String) -> Sequence<Pair<String, String>> = { name, body ->
|
||||
sequenceOf(Pair(name, prefix + name)) + body.findClasses("$prefix$name.")
|
||||
}
|
||||
): Sequence<Pair<String, String>> = // (simpleName1, qualifiedName1), ...
|
||||
Regex(regex, setOf(MULTILINE, UNIX_LINES)).findAll(this).flatMap {
|
||||
transform(it.groupValues[nameGroup], it.groupValues[bodyGroup].trimIndent())
|
||||
}
|
||||
|
||||
fun String.findOuterObjects(): Sequence<Pair<String, String>> = // (simpleName, qualifiedName)
|
||||
findClasses("", 1, 2, "^object\\s+(\\w+).*$\n((^ .*$\n|^$\n)*)") { name, body ->
|
||||
body.findClasses("$name.")
|
||||
}
|
||||
|
||||
val (importLines, remainder) =
|
||||
sourceFiles.entries
|
||||
.flatMap { (_, text) -> text.lines() }
|
||||
.partition { it.startsWith("import") }
|
||||
val importBlock = importLines.sorted().distinct()
|
||||
val (packageLines, code) = remainder.partition { it.startsWith("package") }
|
||||
val packageBlock = packageLines.distinct()
|
||||
assert(
|
||||
packageBlock.size <= 1
|
||||
) // everything is in the same package and/or there is no package line
|
||||
val sourceText = listOf(packageBlock, importBlock, code).flatten().joinToString("\n")
|
||||
|
||||
val (simpleNames, qualifiedNames) =
|
||||
sourceText.findClasses().plus(sourceText.findOuterObjects()).unzip()
|
||||
val instrumentation =
|
||||
"listOf<kotlin.reflect.KClass<*>>(${qualifiedNames.joinToString(",") { "$it::class" }})"
|
||||
|
||||
// create new engine for each compilation
|
||||
// (otherwise we sometimes get kotlin compiler exceptions)
|
||||
val engine = ScriptEngineManager().getEngineByExtension("kts")!!
|
||||
val classes =
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
engine.eval("$sourceText\n\n$instrumentation") as List<KClass<*>>
|
||||
} catch (e: ScriptException) {
|
||||
throw CompilationFailedException(e.message, e)
|
||||
}
|
||||
|
||||
return simpleNames.zip(classes).toMap()
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.codegen.kotlin
|
||||
|
||||
import java.nio.file.Path
|
||||
import org.pkl.commons.createParentDirectories
|
||||
import org.pkl.commons.writeString
|
||||
|
||||
data class PklModule(val name: String, val content: String) {
|
||||
fun writeToDisk(path: Path): Path {
|
||||
return path.createParentDirectories().writeString(content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
|
||||
@@ -0,0 +1,159 @@
|
||||
package my
|
||||
|
||||
import java.util.Objects
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.Long
|
||||
import kotlin.String
|
||||
import kotlin.text.StringBuilder
|
||||
import org.pkl.core.Duration
|
||||
|
||||
object Mod {
|
||||
private fun appendProperty(
|
||||
builder: StringBuilder,
|
||||
name: String,
|
||||
value: Any?
|
||||
) {
|
||||
builder.append("\n ").append(name).append(" = ")
|
||||
val lines = value.toString().split("\n")
|
||||
builder.append(lines[0])
|
||||
for (i in 1..lines.lastIndex) {
|
||||
builder.append("\n ").append(lines[i])
|
||||
}
|
||||
}
|
||||
|
||||
open class Foo(
|
||||
open val one: Long
|
||||
) {
|
||||
open fun copy(one: Long = this.one): Foo = Foo(one)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as Foo
|
||||
if (this.one != other.one) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.one)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(100)
|
||||
builder.append(Foo::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "one", this.one)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
open class None(
|
||||
one: Long
|
||||
) : Foo(one) {
|
||||
open override fun copy(one: Long): None = None(one)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as None
|
||||
if (this.one != other.one) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.one)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(100)
|
||||
builder.append(None::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "one", this.one)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
open class Bar(
|
||||
one: Long,
|
||||
open val two: String
|
||||
) : None(one) {
|
||||
open fun copy(one: Long = this.one, two: String = this.two): Bar = Bar(one, two)
|
||||
|
||||
open override fun copy(one: Long): Bar = Bar(one, two)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as Bar
|
||||
if (this.one != other.one) return false
|
||||
if (this.two != other.two) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.one)
|
||||
result = 31 * result + Objects.hashCode(this.two)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(150)
|
||||
builder.append(Bar::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "one", this.one)
|
||||
appendProperty(builder, "two", this.two)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
class Baz(
|
||||
one: Long,
|
||||
two: String,
|
||||
val three: Duration
|
||||
) : Bar(one, two) {
|
||||
fun copy(
|
||||
one: Long = this.one,
|
||||
two: String = this.two,
|
||||
three: Duration = this.three
|
||||
): Baz = Baz(one, two, three)
|
||||
|
||||
override fun copy(one: Long, two: String): Baz = Baz(one, two, three)
|
||||
|
||||
override fun copy(one: Long): Baz = Baz(one, two, three)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as Baz
|
||||
if (this.one != other.one) return false
|
||||
if (this.two != other.two) return false
|
||||
if (this.three != other.three) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.one)
|
||||
result = 31 * result + Objects.hashCode(this.two)
|
||||
result = 31 * result + Objects.hashCode(this.three)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(200)
|
||||
builder.append(Baz::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "one", this.one)
|
||||
appendProperty(builder, "two", this.two)
|
||||
appendProperty(builder, "three", this.three)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package my
|
||||
|
||||
import java.util.Objects
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Int
|
||||
import kotlin.String
|
||||
import kotlin.text.StringBuilder
|
||||
|
||||
/**
|
||||
* type alias comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
typealias Email = String
|
||||
|
||||
/**
|
||||
* module comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
data class Mod(
|
||||
/**
|
||||
* module property comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
val pigeon: Person
|
||||
) {
|
||||
/**
|
||||
* class comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
open class Product(
|
||||
/**
|
||||
* class property comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
open val price: String
|
||||
) {
|
||||
open fun copy(price: String = this.price): Product = Product(price)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as Product
|
||||
if (this.price != other.price) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.price)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(100)
|
||||
builder.append(Product::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "price", this.price)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* class comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
data class Person(
|
||||
/**
|
||||
* class property comment.
|
||||
* *emphasized* `code`.
|
||||
*/
|
||||
val name: String
|
||||
)
|
||||
|
||||
companion object {
|
||||
private fun appendProperty(
|
||||
builder: StringBuilder,
|
||||
name: String,
|
||||
value: Any?
|
||||
) {
|
||||
builder.append("\n ").append(name).append(" = ")
|
||||
val lines = value.toString().split("\n")
|
||||
builder.append(lines[0])
|
||||
for (i in 1..lines.lastIndex) {
|
||||
builder.append("\n ").append(lines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package my
|
||||
|
||||
import java.util.Objects
|
||||
import kotlin.Any
|
||||
import kotlin.Boolean
|
||||
import kotlin.Double
|
||||
import kotlin.Int
|
||||
import kotlin.Long
|
||||
import kotlin.Pair
|
||||
import kotlin.String
|
||||
import kotlin.collections.Collection
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.Map
|
||||
import kotlin.collections.Set
|
||||
import kotlin.text.Regex
|
||||
import kotlin.text.StringBuilder
|
||||
import org.pkl.core.DataSize
|
||||
import org.pkl.core.DataSizeUnit
|
||||
import org.pkl.core.Duration
|
||||
import org.pkl.core.DurationUnit
|
||||
|
||||
object Mod {
|
||||
private fun appendProperty(
|
||||
builder: StringBuilder,
|
||||
name: String,
|
||||
value: Any?
|
||||
) {
|
||||
builder.append("\n ").append(name).append(" = ")
|
||||
val lines = value.toString().split("\n")
|
||||
builder.append(lines[0])
|
||||
for (i in 1..lines.lastIndex) {
|
||||
builder.append("\n ").append(lines[i])
|
||||
}
|
||||
}
|
||||
|
||||
open class PropertyTypes(
|
||||
open val boolean: Boolean,
|
||||
open val int: Long,
|
||||
open val float: Double,
|
||||
open val string: String,
|
||||
open val duration: Duration,
|
||||
open val durationUnit: DurationUnit,
|
||||
open val dataSize: DataSize,
|
||||
open val dataSizeUnit: DataSizeUnit,
|
||||
open val nullable: String?,
|
||||
open val nullable2: String?,
|
||||
open val pair: Pair<Any?, Any?>,
|
||||
open val pair2: Pair<String, Other>,
|
||||
open val coll: Collection<Any?>,
|
||||
open val coll2: Collection<Other>,
|
||||
open val list: List<Any?>,
|
||||
open val list2: List<Other>,
|
||||
open val set: Set<Any?>,
|
||||
open val set2: Set<Other>,
|
||||
open val map: Map<Any?, Any?>,
|
||||
open val map2: Map<String, Other>,
|
||||
open val container: Map<Any?, Any?>,
|
||||
open val container2: Map<String, Other>,
|
||||
open val other: Other,
|
||||
open val regex: Regex,
|
||||
open val any: Any?,
|
||||
open val nonNull: Any,
|
||||
open val enum: Direction
|
||||
) {
|
||||
open fun copy(
|
||||
boolean: Boolean = this.boolean,
|
||||
int: Long = this.int,
|
||||
float: Double = this.float,
|
||||
string: String = this.string,
|
||||
duration: Duration = this.duration,
|
||||
durationUnit: DurationUnit = this.durationUnit,
|
||||
dataSize: DataSize = this.dataSize,
|
||||
dataSizeUnit: DataSizeUnit = this.dataSizeUnit,
|
||||
nullable: String? = this.nullable,
|
||||
nullable2: String? = this.nullable2,
|
||||
pair: Pair<Any?, Any?> = this.pair,
|
||||
pair2: Pair<String, Other> = this.pair2,
|
||||
coll: Collection<Any?> = this.coll,
|
||||
coll2: Collection<Other> = this.coll2,
|
||||
list: List<Any?> = this.list,
|
||||
list2: List<Other> = this.list2,
|
||||
set: Set<Any?> = this.set,
|
||||
set2: Set<Other> = this.set2,
|
||||
map: Map<Any?, Any?> = this.map,
|
||||
map2: Map<String, Other> = this.map2,
|
||||
container: Map<Any?, Any?> = this.container,
|
||||
container2: Map<String, Other> = this.container2,
|
||||
other: Other = this.other,
|
||||
regex: Regex = this.regex,
|
||||
any: Any? = this.any,
|
||||
nonNull: Any = this.nonNull,
|
||||
enum: Direction = this.enum
|
||||
): PropertyTypes = PropertyTypes(boolean, int, float, string, duration, durationUnit, dataSize,
|
||||
dataSizeUnit, nullable, nullable2, pair, pair2, coll, coll2, list, list2, set, set2, map,
|
||||
map2, container, container2, other, regex, any, nonNull, enum)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as PropertyTypes
|
||||
if (this.boolean != other.boolean) return false
|
||||
if (this.int != other.int) return false
|
||||
if (this.float != other.float) return false
|
||||
if (this.string != other.string) return false
|
||||
if (this.duration != other.duration) return false
|
||||
if (this.durationUnit != other.durationUnit) return false
|
||||
if (this.dataSize != other.dataSize) return false
|
||||
if (this.dataSizeUnit != other.dataSizeUnit) return false
|
||||
if (this.nullable != other.nullable) return false
|
||||
if (this.nullable2 != other.nullable2) return false
|
||||
if (this.pair != other.pair) return false
|
||||
if (this.pair2 != other.pair2) return false
|
||||
if (this.coll != other.coll) return false
|
||||
if (this.coll2 != other.coll2) return false
|
||||
if (this.list != other.list) return false
|
||||
if (this.list2 != other.list2) return false
|
||||
if (this.set != other.set) return false
|
||||
if (this.set2 != other.set2) return false
|
||||
if (this.map != other.map) return false
|
||||
if (this.map2 != other.map2) return false
|
||||
if (this.container != other.container) return false
|
||||
if (this.container2 != other.container2) return false
|
||||
if (this.other != other.other) return false
|
||||
if (this.regex.pattern != other.regex.pattern) return false
|
||||
if (this.any != other.any) return false
|
||||
if (this.nonNull != other.nonNull) return false
|
||||
if (this.enum != other.enum) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.boolean)
|
||||
result = 31 * result + Objects.hashCode(this.int)
|
||||
result = 31 * result + Objects.hashCode(this.float)
|
||||
result = 31 * result + Objects.hashCode(this.string)
|
||||
result = 31 * result + Objects.hashCode(this.duration)
|
||||
result = 31 * result + Objects.hashCode(this.durationUnit)
|
||||
result = 31 * result + Objects.hashCode(this.dataSize)
|
||||
result = 31 * result + Objects.hashCode(this.dataSizeUnit)
|
||||
result = 31 * result + Objects.hashCode(this.nullable)
|
||||
result = 31 * result + Objects.hashCode(this.nullable2)
|
||||
result = 31 * result + Objects.hashCode(this.pair)
|
||||
result = 31 * result + Objects.hashCode(this.pair2)
|
||||
result = 31 * result + Objects.hashCode(this.coll)
|
||||
result = 31 * result + Objects.hashCode(this.coll2)
|
||||
result = 31 * result + Objects.hashCode(this.list)
|
||||
result = 31 * result + Objects.hashCode(this.list2)
|
||||
result = 31 * result + Objects.hashCode(this.set)
|
||||
result = 31 * result + Objects.hashCode(this.set2)
|
||||
result = 31 * result + Objects.hashCode(this.map)
|
||||
result = 31 * result + Objects.hashCode(this.map2)
|
||||
result = 31 * result + Objects.hashCode(this.container)
|
||||
result = 31 * result + Objects.hashCode(this.container2)
|
||||
result = 31 * result + Objects.hashCode(this.other)
|
||||
result = 31 * result + Objects.hashCode(this.regex)
|
||||
result = 31 * result + Objects.hashCode(this.any)
|
||||
result = 31 * result + Objects.hashCode(this.nonNull)
|
||||
result = 31 * result + Objects.hashCode(this.enum)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(1400)
|
||||
builder.append(PropertyTypes::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "boolean", this.boolean)
|
||||
appendProperty(builder, "int", this.int)
|
||||
appendProperty(builder, "float", this.float)
|
||||
appendProperty(builder, "string", this.string)
|
||||
appendProperty(builder, "duration", this.duration)
|
||||
appendProperty(builder, "durationUnit", this.durationUnit)
|
||||
appendProperty(builder, "dataSize", this.dataSize)
|
||||
appendProperty(builder, "dataSizeUnit", this.dataSizeUnit)
|
||||
appendProperty(builder, "nullable", this.nullable)
|
||||
appendProperty(builder, "nullable2", this.nullable2)
|
||||
appendProperty(builder, "pair", this.pair)
|
||||
appendProperty(builder, "pair2", this.pair2)
|
||||
appendProperty(builder, "coll", this.coll)
|
||||
appendProperty(builder, "coll2", this.coll2)
|
||||
appendProperty(builder, "list", this.list)
|
||||
appendProperty(builder, "list2", this.list2)
|
||||
appendProperty(builder, "set", this.set)
|
||||
appendProperty(builder, "set2", this.set2)
|
||||
appendProperty(builder, "map", this.map)
|
||||
appendProperty(builder, "map2", this.map2)
|
||||
appendProperty(builder, "container", this.container)
|
||||
appendProperty(builder, "container2", this.container2)
|
||||
appendProperty(builder, "other", this.other)
|
||||
appendProperty(builder, "regex", this.regex)
|
||||
appendProperty(builder, "any", this.any)
|
||||
appendProperty(builder, "nonNull", this.nonNull)
|
||||
appendProperty(builder, "enum", this.enum)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
open class Other(
|
||||
open val name: String
|
||||
) {
|
||||
open fun copy(name: String = this.name): Other = Other(name)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (this.javaClass != other?.javaClass) return false
|
||||
other as Other
|
||||
if (this.name != other.name) return false
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = 1
|
||||
result = 31 * result + Objects.hashCode(this.name)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
val builder = StringBuilder(100)
|
||||
builder.append(Other::class.java.simpleName).append(" {")
|
||||
appendProperty(builder, "name", this.name)
|
||||
builder.append("\n}")
|
||||
return builder.toString()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Direction(
|
||||
val value: String
|
||||
) {
|
||||
NORTH("north"),
|
||||
|
||||
EAST("east"),
|
||||
|
||||
SOUTH("south"),
|
||||
|
||||
WEST("west");
|
||||
|
||||
override fun toString() = value
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user