From 27ca4993a3a1e0f26465dabc597b2ef984e4e0c3 Mon Sep 17 00:00:00 2001 From: jmfayard Date: Sun, 30 Sep 2018 19:02:15 +0200 Subject: [PATCH] Experiment with kotlin-poet without builders I thought the builder pattern was mostly useless in kotlin, which has named parameters, default values and works well with an immutable functional style like the one used by kotlin poet. This is an experiment with the kotlinpoet codebase to test this hypothesis. Of course I don't advocate removing the builders apis, that would be bad for people calling kotlinpoet from java or with an existing code base. I'm only adding stuff, and delegating to those builders, even adding a configuration lambda for all the corner cases where a builder can be useful I modified the examples in the README to see what the changes would look like --- README.md | 105 +++++++----- .../com/squareup/kotlinpoet/BuilderLess.kt | 155 ++++++++++++++++++ 2 files changed, 223 insertions(+), 37 deletions(-) create mode 100644 src/main/java/com/squareup/kotlinpoet/BuilderLess.kt diff --git a/README.md b/README.md index 04b55ed5ee..1b04b4a7fb 100644 --- a/README.md +++ b/README.md @@ -27,26 +27,32 @@ fun main(vararg args: String) { And this is the code to generate it with KotlinPoet: ```kotlin -val greeterClass = ClassName("", "Greeter") -val file = FileSpec.builder("", "HelloWorld") - .addType(TypeSpec.classBuilder("Greeter") - .primaryConstructor(FunSpec.constructorBuilder() - .addParameter("name", String::class) - .build()) - .addProperty(PropertySpec.builder("name", String::class) - .initializer("name") - .build()) - .addFunction(FunSpec.builder("greet") - .addStatement("println(%S)", "Hello, \$name") - .build()) - .build()) - .addFunction(FunSpec.builder("main") - .addParameter("args", String::class, VARARG) - .addStatement("%T(args[0]).greet()", greeterClass) - .build()) +fun main(args: Array) { + + val greetFunSpec = FunSpec.builder("greet") + .addStatement("println(%S)", "Hello, \$name") + .build() + + val greeterTypeSpec = TypeSpec.of( + className = "Greeter", + constructorProperties = listOf(PropertySpec.property("name", typeName())), + functions = listOf(greetFunSpec) + ) + + val mainFunSpec = FunSpec.builder("main") + .addParameter("args", String::class, KModifier.VARARG) + .addStatement("%T(args[0]).greet()", ClassName("", "Greeter")) .build() -file.writeTo(System.out) + val file = FileSpec.of( + packageName = "", + fileName = "HelloWorld", + types = listOf(greeterTypeSpec), + functions = listOf(mainFunSpec) + ) + file.writeTo(System.out) +} + ``` The [KDoc][kdoc] catalogs the complete KotlinPoet API, which is inspired by [JavaPoet][javapoet]. @@ -135,16 +141,22 @@ returns its own name: ```kotlin fun main(args: Array) { - val helloWorld = TypeSpec.classBuilder("HelloWorld") - .addFunction(whatsMyNameYo("slimShady")) - .addFunction(whatsMyNameYo("eminem")) - .addFunction(whatsMyNameYo("marshallMathers")) - .build() - - val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld") - .addType(helloWorld) - .build() - + + val helloWorld = TypeSpec.of( + className = "HelloWorld", + functions = listOf( + whatsMyNameYo("slimShady"), + whatsMyNameYo("eminem"), + whatsMyNameYo("marshallMathers") + ) + ) + + val kotlinFile = FileSpec.of( + packageName = "com.example.helloworld", + fileName = "HelloWorld", + types = listOf(helloWorld) + ) + kotlinFile.writeTo(System.out) } @@ -279,15 +291,11 @@ and you'll have to add the import statement manually to get those extensions. KotlinPoet also supports Kotlin's nullable types. Simply add the extension `asNullable()` to any type to make it nullable. For example: ```kotlin -val java = PropertySpec.varBuilder("java", String::class.asTypeName().asNullable()) - .addModifiers(KModifier.PRIVATE) - .initializer("null") - .build() - -val helloWorld = TypeSpec.classBuilder("HelloWorld") - .addProperty(java) - .addProperty("kotlin", String::class, KModifier.PRIVATE) - .build() +val java = PropertySpec.nullableVarProperty("java", typeName(), + listOf(KModifier.PRIVATE), CodeBlock.of("null")) + +val kotlin = PropertySpec.property("kotlin", typeName(), + listOf(KModifier.PRIVATE)) ``` generates: @@ -466,6 +474,8 @@ FunSpec.builder("add") .build() ``` + + ### Constructors `FunSpec` is a slight misnomer; it can also be used for constructors: @@ -494,6 +504,27 @@ class HelloWorld { } ``` + +Here is a simpler way to produce + +```kotlin +data class User(val greeting: String = "", val id: Int = -1) +``` + + +```kotlin +val greeting = PropertySpec.property("greeting", typeName()) + +val id = PropertySpec.property("id", typeName()) + +val userTypeSpec = TypeSpec.of( + className = "User", + constructorProperties = listOf(greeting, id), + modifiers = listOf(KModifier.DATA) +) +``` + + For the most part, constructors work just like methods. When emitting code, KotlinPoet will place constructors before methods in the output file. diff --git a/src/main/java/com/squareup/kotlinpoet/BuilderLess.kt b/src/main/java/com/squareup/kotlinpoet/BuilderLess.kt new file mode 100644 index 0000000000..fecc2e4abf --- /dev/null +++ b/src/main/java/com/squareup/kotlinpoet/BuilderLess.kt @@ -0,0 +1,155 @@ +package com.squareup.kotlinpoet + +import kotlin.reflect.KClass + +fun main(args: Array) { + + val greetFunSpec = FunSpec.builder("greet") + .addStatement("println(%S)", "Hello, \$name") + .build() + + val greeterTypeSpec = TypeSpec.of( + className = "Greeter", + constructorProperties = listOf(PropertySpec.property("name", typeName())), + functions = listOf(greetFunSpec) + ) + + val mainFunSpec = FunSpec.builder("main") + .addParameter("args", String::class, KModifier.VARARG) + .addStatement("%T(args[0]).greet()", ClassName("", "Greeter")) + .build() + + val file = FileSpec.of( + packageName = "", + fileName = "HelloWorld", + types = listOf(greeterTypeSpec), + functions = listOf(mainFunSpec) + ) + file.writeTo(System.out) +} + +inline fun typeName(clazz: Class = T::class.java): ClassName + = T::class.asTypeName() + + +fun PropertySpec.Companion.property( + name: String, + type: ClassName, + modifiers: List = emptyList(), + initializer: CodeBlock? = null, + configuration: PropertySpec.Builder.() -> Unit = {} +): PropertySpec = PropertySpec.builder(name, type, *modifiers.toTypedArray()) + .apply { + if (initializer != null) initializer(initializer) + configuration() + } + .build() + +fun PropertySpec.Companion.varProperty( + name: String, + type: ClassName, + modifiers: List = emptyList(), + initializer: CodeBlock? = null, + configuration: PropertySpec.Builder.() -> Unit = {} +) : PropertySpec = PropertySpec.varBuilder(name, type, *modifiers.toTypedArray()) + .apply { + if (initializer != null) initializer(initializer) + configuration() + }.build() + +fun PropertySpec.Companion.nullableProperty( + name: String, + type: ClassName, + modifiers: List = emptyList(), + initializer: CodeBlock? = null, + configuration: PropertySpec.Builder.() -> Unit = {} +) : PropertySpec = PropertySpec.property(name, type.asNullable(), modifiers, initializer) + +fun PropertySpec.Companion.nullableVarProperty( + name: String, + type: ClassName, + modifiers: List = emptyList(), + initializer: CodeBlock? = null, + configuration: PropertySpec.Builder.() -> Unit = {} +): PropertySpec = PropertySpec.nullableProperty(name, type.asNullable(), modifiers, initializer, configuration) + + +fun TypeSpec.Companion.of( + className: String, + constructorProperties: List = emptyList(), + modifiers: List = emptyList(), + types: Iterable = emptyList(), + primaryConstructor: FunSpec? = null, + functions: Iterable = emptyList(), + annotations: Annotations = Annotations(), + properties: List = emptyList(), + configuration: TypeSpec.Builder.() -> Unit = {} +): TypeSpec { + return TypeSpec.classBuilder(className) + .apply { + require(constructorProperties.isEmpty() || primaryConstructor == null) + if (primaryConstructor != null) { + primaryConstructor(primaryConstructor) + } + if (constructorProperties.isNotEmpty()) { + val parameterSpecs = mutableListOf() + for (cp in constructorProperties) { + parameterSpecs += ParameterSpec.builder(cp.name, cp.type, *cp.modifiers.toTypedArray()) + .apply { + if (cp.initializer != null) defaultValue(cp.initializer) + } + .build() + addProperty(cp.toBuilder().initializer(cp.name).build()) + } + + primaryConstructor( + FunSpec.constructorBuilder() + .addParameters(parameterSpecs) + .build()) + } + for (t in types) addType(t) + for (f in functions) addFunction(f) + for (a in annotations.specs) addAnnotation(a) + for (a in annotations.classes) addAnnotation(a) + for (a in annotations.classNames) addAnnotation(a) + for (a in annotations.kclasses) addAnnotation(a) + addModifiers(*modifiers.toTypedArray()) + addProperties(properties) + + configuration() + } + .build() +} + +fun FileSpec.Companion.of( + packageName: String, + fileName: String, + types: Iterable = emptyList(), + functions: Iterable = emptyList(), + annotations: Annotations = Annotations(), + properties: List = emptyList(), + comment: String = "", + typeAliases: Iterable = emptyList(), + configuration: FileSpec.Builder.() -> Unit = {} +): FileSpec = FileSpec.builder(packageName, fileName) + .apply { + for (t in types) addType(t) + for (f in functions) addFunction(f) + for (a in annotations.specs) addAnnotation(a) + for (a in annotations.classes) addAnnotation(a) + for (a in annotations.classNames) addAnnotation(a) + for (a in annotations.kclasses) addAnnotation(a) + for (p in properties) addProperty(p) + if (comment.isNotEmpty()) addComment(comment) + for (ty in typeAliases) addTypeAlias(ty) + configuration() + } + .build() + + +data class Annotations( + val specs: Iterable = emptyList(), + val classes: Iterable> = emptyList(), + val kclasses: Iterable> = emptyList(), + val classNames: Iterable = emptyList() +)