├── .gitignore ├── LICENSE.md ├── README.md ├── annotation-processor ├── build.gradle └── src │ └── main │ ├── java │ └── com │ │ └── masabi │ │ └── kotlinbuilder │ │ └── JvmBuilderAnnotationProcessor.kt │ └── resources │ └── META-INF │ └── services │ └── javax.annotation.processing.Processor ├── annotations ├── README.md ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── masabi │ └── kotlinbuilder │ └── annotations │ └── JvmBuilder.kt ├── build.gradle ├── example ├── build.gradle └── src │ ├── main │ └── java │ │ ├── Example.kt │ │ └── com │ │ └── masabi │ │ └── kotlin │ │ └── TestClasses.kt │ └── test │ └── groovy │ └── BuildingSpec.groovy ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── package.gradle ├── settings.gradle ├── src ├── main │ └── java │ │ └── Library.java └── test │ └── java │ └── LibraryTest.java └── version.properties /.gitignore: -------------------------------------------------------------------------------- 1 | out/ 2 | 3 | # Log Files 4 | *.log 5 | 6 | # IntelliJ IDEA modules 7 | *.iml 8 | 9 | # IntelliJ IDEA folder 10 | .idea 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | 16 | # Gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Misc 21 | .DS_Store 22 | Thumbs.db 23 | *.swp 24 | *.bak 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2018] [Masabi Ltd] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Build Status](https://codebuild.eu-west-1.amazonaws.com/badges?uuid=eyJlbmNyeXB0ZWREYXRhIjoic29QQnlzQktXdE9ORHArNEd6TlNHNXNNaFh4eGU4VzRmRU5INU9BWWFZNmFHZWZURGdmZDJ3MVVEVXlHeC9heDlmdml3RjJsTVozV25uNnZLVms4VUc4PSIsIml2UGFyYW1ldGVyU3BlYyI6InFDTG5iWjhUa21VMEF2OEkiLCJtYXRlcmlhbFNldFNlcmlhbCI6MX0%3D&branch=master) 2 | 3 | # Kotlin Builder 4 | JVM Builder for Kotlin Data Classes 5 | 6 | ## Introduction 7 | This is an annotation processor for Kotlin data classes to generate a builder for use from other JVM languages. This provides an alternative to an all-args constructor or generating telescopic constructors. 8 | 9 | Usage is as simple as adding the `@JvmBuilder` annotation to the class of your choice, for example: 10 | ``` 11 | @JvmBuilder 12 | data class Person(val name: String, val age: Int) 13 | ``` 14 | 15 | After applying kapt, a `Person_Builder` class will be generated allowing you to construct an object from other language, e.g. in Java this would be: 16 | ``` 17 | new Person_Builder() 18 | .name("Henry") 19 | .age(15) 20 | .build() 21 | ``` 22 | 23 | ### Nullability 24 | The annotation processor will generate a builder that accepts nullable fields. As you're calling this from another JVM language, this is ok as they typically don't have the notion of nullability. During the build process, nullability will be checked and an `IllegalStateException` will be raised informing you of the mistake as illustrated n the following, taken from the examples: 25 | 26 | Kotlin: 27 | ``` 28 | @JvmBuilder 29 | data class Param1Int(val param1: Int) 30 | ``` 31 | 32 | Groovy Test: 33 | ``` 34 | def "will error if trying to build and a value isn't provided for a non-null field"() { 35 | when: 36 | new Param1Int_Builder().param1(null).build() 37 | 38 | then: 39 | thrown(IllegalStateException) 40 | } 41 | ``` 42 | 43 | ### Default Values 44 | If defaults have been provided in the data class, these can be omitted from the builder chain and the original value will be provided, as shown in this example: 45 | 46 | Kotlin: 47 | ``` 48 | @JvmBuilder 49 | data class Param1Default(val defaultString: String = "The D. Fault") 50 | ``` 51 | 52 | Groovy Test: 53 | ``` 54 | def "uses defaults when provided"() { 55 | expect: 56 | new Param1Default_Builder().build().defaultString == "The D. Fault" 57 | } 58 | ``` 59 | 60 | If a parameter does not have a default value and is not nullable, an `IllegalStateException` will be raised on build. 61 | 62 | ### Static builder() method 63 | It is normal for the class the builder is for to generate a static `builder()` method to provide easy access to creating a builder instance. As there is no officially supported way during annotation processing to modify existing classes this is not provided, but can be achieved with the following: 64 | ``` 65 | @JvmBuilder 66 | data class BuilderMethodProvided(val param1: Int = 1, val param2: String = "Default") { 67 | companion object { 68 | @JvmStatic fun builder() = BuilderMethodProvided_Builder() 69 | } 70 | } 71 | ``` 72 | Obviously this won't compile until you have run kapt to generate the builder. 73 | 74 | ### Custom setter prefix 75 | As of version `1.1.0` it is possible to provide a custom prefix for the generated setter methods. 76 | 77 | ``` 78 | @JvmBuilder(setterPrefix = "with") 79 | data class CustomSetterSpec(val param1: String) 80 | ``` 81 | 82 | ``` 83 | new CustomSetterSpec_Builder() 84 | .withParam1("ignored") 85 | .build() 86 | ``` 87 | Please note that prior to this version the default prefix was `with`. This has been removed so that no default is provided. 88 | 89 | ## Getting Started 90 | Kotlin Builder is available from maven central and must be added as a `kapt` and `compile` dependency to your project. The `example/` directory has a fully working example to get you started. 91 | 92 | The builder uses runtime reflection for nullability and default parameters, so this must be provided on your runtime classpath, shown below: 93 | ``` 94 | apply plugin: 'kotlin' 95 | apply plugin: 'kotlin-kapt' 96 | 97 | def kotlinbuilderVersion = "1.1.0" 98 | 99 | dependencies { 100 | compileOnly "com.masabi.kotlinbuilder:masabi-kotlinbuilder:$kotlinbuilderVersion" 101 | 102 | implementation ( 103 | "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version", 104 | "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 105 | ) 106 | 107 | kapt( 108 | "com.masabi.kotlinbuilder:masabi-kotlinbuilder-processor:$kotlinbuilderVersion" 109 | ) 110 | } 111 | ``` 112 | 113 | # Change Log 114 | **Version 1.1.0 (03-09-2018)** 115 | 116 | * Split the annotation out from the processor to avoid classpath leaking ([jffiorillo](https://github.com/jffiorillo)) 117 | * Removed "with" as the default setter prefix ([jffiorillo](https://github.com/jffiorillo)) 118 | * Provide customer setter prefix 119 | 120 | **Version 1.0.1 (23-07-2018)** 121 | 122 | * Fixed issue where nullable values weren't being overriden 123 | 124 | **Version 1.0.0 (20-07-2018)** 125 | 126 | * Initial release 127 | -------------------------------------------------------------------------------- /annotation-processor/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply from: '../package.gradle' 3 | 4 | jar { 5 | baseName = "masabi-kotlinbuilder-processor" 6 | } 7 | 8 | dependencies { 9 | compileOnly( 10 | project(':annotations'), 11 | "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 12 | ) 13 | 14 | implementation 'com.squareup:kotlinpoet:0.7.0' 15 | } -------------------------------------------------------------------------------- /annotation-processor/src/main/java/com/masabi/kotlinbuilder/JvmBuilderAnnotationProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.masabi.kotlinbuilder 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.FunSpec 5 | import com.squareup.kotlinpoet.TypeName 6 | 7 | import com.masabi.kotlinbuilder.JvmBuilderAnnotationProcessor.BuilderField 8 | import com.masabi.kotlinbuilder.annotations.JvmBuilder 9 | import com.squareup.kotlinpoet.* 10 | import java.io.File 11 | import javax.annotation.processing.* 12 | import javax.lang.model.SourceVersion 13 | import javax.lang.model.element.Element 14 | import javax.lang.model.element.TypeElement 15 | import javax.lang.model.element.VariableElement 16 | import javax.lang.model.type.TypeMirror 17 | import javax.lang.model.util.ElementFilter 18 | import javax.tools.Diagnostic.Kind.ERROR 19 | import javax.tools.Diagnostic.Kind.WARNING 20 | import kotlin.reflect.KParameter 21 | 22 | private const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated" 23 | 24 | @SupportedSourceVersion(SourceVersion.RELEASE_8) 25 | @SupportedAnnotationTypes("com.masabi.kotlinbuilder.annotations.JvmBuilder") 26 | @SupportedOptions(KAPT_KOTLIN_GENERATED_OPTION_NAME) 27 | class JvmBuilderAnnotationProcessor : AbstractProcessor() { 28 | 29 | override fun process(annotations: MutableSet?, roundEnv: RoundEnvironment): Boolean { 30 | val annotatedElements = roundEnv.getElementsAnnotatedWith(JvmBuilder::class.java) 31 | if (annotatedElements.isEmpty()) return false 32 | 33 | val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] 34 | ?: run { 35 | processingEnv.messager.printMessage(ERROR, "Can't find the target directory for generated Kotlin files.") 36 | return false 37 | } 38 | 39 | annotatedElements.forEach { 40 | generateBuilder(kaptKotlinGeneratedDir, it) 41 | } 42 | 43 | return true 44 | } 45 | 46 | private fun generateBuilder(generationDir: String, annotatedElement: Element) { 47 | val className = annotatedElement.simpleName.toString() 48 | val pack = processingEnv.elementUtils.getPackageOf(annotatedElement).toString() 49 | val builderClassName = "${className}_Builder" 50 | 51 | FileSpec.builder(pack, builderClassName) 52 | .addType(builderSpec(builderClassName, annotatedElement)) 53 | .build() 54 | .writeTo(File(generationDir, "$builderClassName.kt")) 55 | } 56 | 57 | private fun builderSpec(builderClassName: String, targetClass: Element): TypeSpec { 58 | val builderClass = BuilderClass( 59 | builderClass = builderClassName, 60 | targetClass = targetClass.asType(), 61 | properties = propertiesFrom(targetClass) 62 | ) 63 | 64 | return TypeSpec.classBuilder(builderClassName) 65 | .addProperties(builderClass.propertySpecs()) 66 | .addFunctions(builderClass.funSpecs()) 67 | .build() 68 | } 69 | 70 | private fun propertiesFrom(targetClass: Element): List { 71 | val constructors = ElementFilter.constructorsIn(targetClass.enclosedElements) 72 | 73 | if (constructors.size == 0) { 74 | processingEnv.messager.printMessage(ERROR, "No primary constructor found") 75 | } 76 | 77 | val setterPrefix = targetClass.setterPrefix 78 | val primaryConstructor = constructors.sortedBy { it.parameters.size }.last() 79 | val constructorParams = primaryConstructor.parameters 80 | return constructorParams.map { it.asBuilderField(setterPrefix) } 81 | } 82 | 83 | private fun log(any: Any) { 84 | processingEnv.messager.printMessage(WARNING, any.toString()) 85 | } 86 | 87 | data class BuilderClass( 88 | val properties: List, 89 | val builderClass: String, 90 | val targetClass: TypeMirror 91 | ) { 92 | fun propertySpecs(): Iterable { 93 | return listOf(mapBasedProperties()) 94 | } 95 | 96 | private fun mapBasedProperties(): PropertySpec { 97 | val name = ParameterizedTypeName.get(ClassName("kotlin.collections", "MutableMap"), String::class.asTypeName(), Any::class.asTypeName().asNullable()) 98 | return PropertySpec.builder("values", name) 99 | .addModifiers(KModifier.PRIVATE) 100 | .initializer("mutableMapOf()") 101 | .build() 102 | } 103 | 104 | fun funSpecs(): Iterable { 105 | return properties.map { it.asFunSpec(builderClass) } + builderSpecs() 106 | } 107 | 108 | private fun builderSpecs(): Iterable { 109 | return listOf( 110 | builderSpec(), 111 | nonNullArgCheckerSpec(), 112 | mandatoryArgCheckerSpec(), 113 | fillInMissingNullables() 114 | ) 115 | } 116 | 117 | private fun nonNullArgCheckerSpec(): FunSpec { 118 | return FunSpec.builder("verifyNonNullArgumentsArePresent") 119 | .addModifiers(KModifier.PRIVATE) 120 | .addParameter(ParameterSpec.builder("parametersByName", ParameterizedTypeName.get(Map::class.asClassName(), String::class.asTypeName().asNullable(), KParameter::class.asTypeName())).build()) 121 | .addStatement(""" 122 | parametersByName 123 | .filter { !it.value.type.isMarkedNullable } 124 | .filter { !it.value.isOptional } 125 | .forEach { if (values.get(it.key) == null) throw IllegalStateException("'${'$'}{it.key}' cannot be null") } 126 | """.trimIndent()) 127 | .build() 128 | } 129 | 130 | private fun fillInMissingNullables(): FunSpec { 131 | return FunSpec.builder("fillInMissingNullables") 132 | .addModifiers(KModifier.PRIVATE) 133 | .addParameter(ParameterSpec.builder("parametersByName", ParameterizedTypeName.get(Map::class.asClassName(), String::class.asTypeName().asNullable(), KParameter::class.asTypeName())).build()) 134 | .addStatement(""" 135 | parametersByName 136 | .filter { !values.containsKey(it.value.name) } 137 | .filter { it.value.type.isMarkedNullable } 138 | .filter { !it.value.isOptional } 139 | .forEach { values[it.key!!] = null } 140 | """.trimIndent()) 141 | .build() 142 | } 143 | 144 | private fun mandatoryArgCheckerSpec(): FunSpec { 145 | return FunSpec.builder("verifyMandatoryArgumentsArePresent") 146 | .addModifiers(KModifier.PRIVATE) 147 | .addParameter(ParameterSpec.builder("parametersByName", ParameterizedTypeName.get(Map::class.asClassName(), String::class.asTypeName().asNullable(), KParameter::class.asTypeName())).build()) 148 | .addStatement(""" 149 | parametersByName 150 | .filter { !it.value.isOptional } 151 | .forEach { if (!values.containsKey(it.key)) throw IllegalStateException("'${'$'}{it.key}' has no default value, you must set one") } 152 | """.trimIndent()) 153 | .build() 154 | } 155 | 156 | 157 | private fun builderSpec(): FunSpec { 158 | return FunSpec.builder("build") 159 | .returns(targetClass.asTypeName()) 160 | .addStatement("val constructor = ::%T", targetClass) 161 | .addStatement("val parametersByName = constructor.parameters.groupBy { it.name }.mapValues { it.value.first() }") 162 | .addStatement("fillInMissingNullables(parametersByName)") 163 | .addStatement("verifyNonNullArgumentsArePresent(parametersByName)") 164 | .addStatement("verifyMandatoryArgumentsArePresent(parametersByName)") 165 | .addStatement("return constructor.callBy(mapOf(*values.map { parametersByName.getValue(it.key) to values.get(it.key) }.toTypedArray()))") 166 | .build() 167 | } 168 | } 169 | 170 | data class BuilderField(val name: String, val type: TypeName, val setterPrefix: String) { 171 | fun asFunSpec(builderClass: String): FunSpec { 172 | return FunSpec.builder(generateSetterName()) 173 | .returns(ClassName.bestGuess(builderClass)) 174 | .addParameter(name, type.asNullable()) 175 | .addStatement("""this.values["$name"] = $name""") 176 | .addStatement("return this") 177 | .build() 178 | } 179 | 180 | fun generateSetterName(): String { 181 | return if (setterPrefix.isNotEmpty()) { 182 | "$setterPrefix${name.capitalize()}" 183 | } else { 184 | name 185 | } 186 | } 187 | } 188 | } 189 | 190 | private fun VariableElement.asBuilderField(setterPrefix: String): BuilderField = 191 | BuilderField( 192 | name = simpleName.toString(), 193 | type = asType().asTypeName().corrected(), 194 | setterPrefix = setterPrefix 195 | ) 196 | 197 | private val Element.setterPrefix: String 198 | get() = getAnnotation(JvmBuilder::class.java).setterPrefix 199 | 200 | private fun TypeName.corrected(): TypeName { 201 | return if (this.toString() == "java.lang.String") ClassName("kotlin", "String") else this 202 | } 203 | -------------------------------------------------------------------------------- /annotation-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor: -------------------------------------------------------------------------------- 1 | com.masabi.kotlinbuilder.JvmBuilderAnnotationProcessor -------------------------------------------------------------------------------- /annotations/README.md: -------------------------------------------------------------------------------- 1 | ## Kotlin Builder Annotations 2 | 3 | This module contains the annotations require for this framework. 4 | 5 | 6 | ``` 7 | 8 | def kotlinbuilderVersion = "1.0.1" 9 | 10 | dependencies { 11 | compileOnly "com.masabi.kotlinbuilder:kotlinbuilder-annotations:$kotlinbuilderVersion" 12 | } 13 | ``` -------------------------------------------------------------------------------- /annotations/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | 3 | apply from: '../package.gradle' 4 | 5 | jar { 6 | baseName = "masabi-kotlinbuilder" 7 | } 8 | 9 | dependencies { 10 | compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 11 | } -------------------------------------------------------------------------------- /annotations/src/main/kotlin/com/masabi/kotlinbuilder/annotations/JvmBuilder.kt: -------------------------------------------------------------------------------- 1 | package com.masabi.kotlinbuilder.annotations 2 | 3 | @Target(AnnotationTarget.CLASS) 4 | @Retention(AnnotationRetention.SOURCE) 5 | annotation class JvmBuilder( 6 | val setterPrefix: String = "" 7 | ) -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.2.31' 3 | 4 | repositories { 5 | jcenter() 6 | mavenLocal() 7 | } 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | defaultTasks 'test' 14 | 15 | allprojects { 16 | apply plugin: 'idea' 17 | repositories { 18 | jcenter() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'kotlin' 2 | apply plugin: 'kotlin-kapt' 3 | 4 | dependencies { 5 | compileOnly project(':annotations') 6 | compile( 7 | "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version", 8 | "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" 9 | ) 10 | 11 | kapt( 12 | project(":annotation-processor") 13 | ) 14 | 15 | testCompile( 16 | "org.spockframework:spock-core:1.1-groovy-2.4" 17 | ) 18 | } 19 | 20 | apply plugin: 'idea' 21 | apply plugin: 'groovy' 22 | 23 | idea { 24 | module { 25 | sourceDirs += files('build/generated/source/kapt/main', 'build/generated/source/kaptKotlin/main') 26 | generatedSourceDirs += files('build/generated/source/kapt/main', 'build/generated/source/kaptKotlin/main') 27 | } 28 | } -------------------------------------------------------------------------------- /example/src/main/java/Example.kt: -------------------------------------------------------------------------------- 1 | package org.kotlin.test 2 | 3 | //import org.kotlin.annotationProcessor.TestAnnotation 4 | 5 | //@TestAnnotation 6 | class SimpleClass -------------------------------------------------------------------------------- /example/src/main/java/com/masabi/kotlin/TestClasses.kt: -------------------------------------------------------------------------------- 1 | package com.masabi.kotlin 2 | 3 | import com.masabi.kotlinbuilder.annotations.JvmBuilder 4 | 5 | 6 | @JvmBuilder 7 | data class Param1Int(val param1: Int) 8 | 9 | @JvmBuilder 10 | data class Param2Int(val param1: Int, val param2: Int) 11 | 12 | @JvmBuilder 13 | data class Param9Int(val param1: Int, val param2: Int, val param3: Int, val param4: Int, val param5: Int, val param6: Int, val param7: Int, val param8: Int, val param9: Int) 14 | 15 | @JvmBuilder 16 | data class Param1KotlinString(val param1: String) 17 | 18 | @JvmBuilder 19 | data class Param1NullableString(val nullableString: String?) 20 | 21 | @JvmBuilder 22 | data class NullableAndMandatoryMixed(val nullableString: String?, val nonNullableString: String) 23 | 24 | @JvmBuilder 25 | data class Param1Default(val defaultString: String = "The D. Fault") 26 | 27 | @JvmBuilder 28 | data class BuilderMethodProvided(val param1: Int = 1, val param2: String = "Default") { 29 | companion object { 30 | @JvmStatic fun builder() = BuilderMethodProvided_Builder() 31 | } 32 | } 33 | 34 | @JvmBuilder(setterPrefix = "with") 35 | data class CustomSetterSpec(val param1: String) -------------------------------------------------------------------------------- /example/src/test/groovy/BuildingSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.masabi.kotlin.* 2 | import spock.lang.Specification 3 | 4 | class BuildingSpec extends Specification { 5 | def "can build an object with 1 int argument"() { 6 | given: 7 | def direct = new Param1Int(99) 8 | def built = new Param1Int_Builder().param1(99).build() 9 | 10 | expect: 11 | direct == built 12 | } 13 | 14 | def "can build an object with 2 int arguments"() { 15 | given: 16 | def direct = new Param2Int(69, 42) 17 | def built = new Param2Int_Builder().param1(69).param2(42).build() 18 | 19 | expect: 20 | direct == built 21 | } 22 | 23 | def "can build an object with more int arguments"() { 24 | given: 25 | def direct = new Param9Int(1, 2, 3, 4, 5, 6, 7, 8, 9) 26 | def built = new Param9Int_Builder() 27 | .param1(1) 28 | .param2(2) 29 | .param3(3) 30 | .param4(4) 31 | .param5(5) 32 | .param6(6) 33 | .param7(7) 34 | .param8(8) 35 | .param9(9) 36 | .build() 37 | 38 | expect: 39 | direct == built 40 | } 41 | 42 | def "can build an object with a kotlin string"() { 43 | given: 44 | def direct = new Param1KotlinString("Hello Java World") 45 | def built = new Param1KotlinString_Builder() 46 | .param1("Hello Java World") 47 | .build() 48 | 49 | expect: 50 | direct == built 51 | } 52 | 53 | def "will error if trying to build and a value isn't provided for a non-null field"() { 54 | when: 55 | new Param1Int_Builder().param1(null).build() 56 | 57 | then: 58 | thrown(IllegalStateException) 59 | } 60 | 61 | def "will error if trying to build and single mandatory parameter hasn't been provided"() { 62 | when: 63 | new Param1Int_Builder().build() 64 | 65 | then: 66 | thrown(IllegalStateException) 67 | } 68 | 69 | def "will not error if nullable parameters aren't provided"() { 70 | expect: 71 | new NullableAndMandatoryMixed_Builder().nonNullableString("I'm here").build().nullableString == null 72 | } 73 | 74 | def "can mix nullable and non-nullable"() { 75 | expect: 76 | new NullableAndMandatoryMixed_Builder().nonNullableString("I'm here").build().nonNullableString == "I'm here" 77 | } 78 | 79 | def "uses defaults when provided"() { 80 | expect: 81 | new Param1Default_Builder().build().defaultString == "The D. Fault" 82 | } 83 | 84 | def "uses provided value instead of null when provided"() { 85 | expect: 86 | new Param1NullableString_Builder().nullableString("provided").build().nullableString == "provided" 87 | } 88 | 89 | def "can use a static builder on the data class"() { 90 | when: 91 | BuilderMethodProvided.builder().build() 92 | 93 | then: 94 | notThrown() 95 | } 96 | 97 | def "can use a custom method for field spec"() { 98 | expect: 99 | new CustomSetterSpec_Builder() 100 | .withParam1("ignored") 101 | .build() 102 | .param1 == "ignored" 103 | 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kapt.verbose=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Masabi/kotlinbuilder/7c1dfd7f06117cc56902d707a4124ae33f1965b1/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 02 12:58:01 CEST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8.1-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /package.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'maven' 2 | apply plugin: 'signing' 3 | apply from: "${project.rootDir}/version.properties" 4 | 5 | group = "com.masabi.kotlinbuilder" 6 | 7 | task javadocJar(type: Jar, dependsOn: javadoc) { 8 | classifier = 'javadoc' 9 | from 'build/docs/javadoc' 10 | } 11 | 12 | task sourcesJar(type: Jar) { 13 | from sourceSets.main.allSource 14 | classifier = 'sources' 15 | } 16 | 17 | artifacts { 18 | archives jar 19 | archives javadocJar 20 | archives sourcesJar 21 | } 22 | 23 | signing { 24 | sign configurations.archives 25 | } 26 | 27 | def sonatypeCreds = [ 28 | userName: project.properties['ossrhUsername'] ?: "", 29 | password: project.properties['ossrhPassword'] ?: "" 30 | ] 31 | def mavenDeployRepo = [ 32 | releases: project.properties['mavenReleaseRepo'] ?: "https://oss.sonatype.org/service/local/staging/deploy/maven2/", 33 | snapshot: project.properties['mavenSnapshotRepo'] ?: "https://oss.sonatype.org/content/repositories/snapshots/" 34 | ] 35 | 36 | uploadArchives { 37 | repositories { 38 | mavenDeployer { 39 | beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } 40 | 41 | repository(url: mavenDeployRepo.releases) { 42 | authentication(sonatypeCreds) 43 | } 44 | 45 | snapshotRepository(url: mavenDeployRepo.snapshot) { 46 | authentication(sonatypeCreds) 47 | } 48 | 49 | pom.project { 50 | name 'Kotlin Builder' 51 | packaging 'jar' 52 | description 'Annotation processor for generating a JVM style builder for Kotlin data classes' 53 | url 'https://github.com/Masabi/kotlinbuilder' 54 | 55 | scm { 56 | url 'https://github.com/Masabi/kotlinbuilder' 57 | connection 'scm:git:git://github.com/Masabi/kotlinbuilder.git' 58 | developerConnection 'scm:git:gitgithub.com/Masabi/kotlinbuilder.git' 59 | } 60 | 61 | licenses { 62 | license { 63 | name 'The Apache Software License, Version 2.0' 64 | url 'http://www.apache.org/licenses/LICENSE-2.0.txt' 65 | distribution 'repo' 66 | } 67 | } 68 | 69 | developers { 70 | developer { 71 | id 'tddmonkey' 72 | name 'Colin Vipurs' 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kotlinbuilder' 2 | 3 | include ':annotation-processor', ':example', ':annotations' -------------------------------------------------------------------------------- /src/main/java/Library.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | public class Library { 5 | public boolean someLibraryMethod() { 6 | return true; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/test/java/LibraryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Java source file was generated by the Gradle 'init' task. 3 | */ 4 | import org.junit.Test; 5 | import static org.junit.Assert.*; 6 | 7 | public class LibraryTest { 8 | @Test public void testSomeLibraryMethod() { 9 | Library classUnderTest = new Library(); 10 | assertTrue("someLibraryMethod should return 'true'", classUnderTest.someLibraryMethod()); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /version.properties: -------------------------------------------------------------------------------- 1 | version="1.1.0" 2 | --------------------------------------------------------------------------------