Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1 @@
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

View File

@@ -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()
}
}
}

View File

@@ -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])
}
}
}
}

View File

@@ -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
}
}