Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

#5 Adding DSL to define properties #9

Merged
merged 12 commits into from
Jan 14, 2019
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Newcomers of Gradle Build System very often complain about that in Gradle there
* [Sample build script](#sample-build-script)
* [Defining and executing configurations](#defining-and-executing-configurations)
* [Providing properties](#providing-properties)
* [Defining project properties](#defining-project-properties)
* [Sample output](#sample-output)
* [License](#license)

Expand Down Expand Up @@ -129,6 +130,50 @@ Properties can be provided by (order makes precedence):

4. Mixed approach.

### Defining project properties

Configuring of project properties can be enhanced by providing properties definitions which can be used for property value validation, e.g.:
```kotlin
fork {
properties {
property("enableSomething") {
checkbox(defaultValue = true)
}
property("someJvaOpts") {
optional()
text(defaultValue = "-server -Xmx1024m -XX:MaxPermSize=256M -Djava.awt.headless=true")
validator {
if (!value.startsWith("-")) error("This is not a JVM option!")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if (value.split(" ").any{ !it.startsWith("-")) :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

}
}
property("someUserName") {
text(defaultValue = System.getProperty("user.name"))
}
property("projectGroup") {
text(defaultValue = "org.neva")
}
}
}
```

#### Property definition
Property definition can consists of:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line after header

* type specification: `type = TYPE_NAME`
* there are five types available: `TEXT` x(default one), `CHECKBOX` (representing boolean), `PASSWORD`, `PATH` & `URL`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are / not needed

* there is default convention of type inference using property name (case insensitive):
* ends with "password" -> `PASSWORD`
* starts with "enable", "disable" -> `CHECKBOX`
* ends with "enabled", "disabled" -> `CHECKBOX`
* ends with "url" -> `URL`
* ends with "path" -> `PATH`
* else -> `TEXT`
* default value specification: `defaultValue = System.getProperty("user.name")`
* if no value would be provided for property `defaultValue` is used
* declaring property as optional: `optional()`
* by default all properties are required
* specifying custom validator: `validator = {if (!value.startsWith("-")) error("This is not a JVM option!")}`
* by default `URL` & `PATH` properties gets basic validation which can be overridden or suppressed: `validator = {}`

### Sample output

After executing command `gradlew fork`, there will be a cloned project with correctly changed directory names, with replaced project name and label in text files (all stuff being previously performed manually).
Expand Down
29 changes: 22 additions & 7 deletions src/main/kotlin/com/neva/gradle/fork/ForkExtension.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package com.neva.gradle.fork
import com.neva.gradle.fork.config.Config
import com.neva.gradle.fork.config.InPlaceConfig
import com.neva.gradle.fork.config.SourceTargetConfig
import com.neva.gradle.fork.config.properties.PropertyDefinition
import com.neva.gradle.fork.config.properties.PropertyDefinitions
import groovy.lang.Closure
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.util.ConfigureUtil
Expand All @@ -13,6 +16,8 @@ open class ForkExtension(val project: Project) {
@Input
val configs = mutableListOf<Config>()

val propertyDefinitions = PropertyDefinitions()

fun config(name: String): Config {
return configs.find { it.name == name }
?: throw ForkException("Fork configuration '$name' is yet not defined.")
Expand All @@ -23,23 +28,33 @@ open class ForkExtension(val project: Project) {
}

fun config(configurer: Config.() -> Unit) {
config(SourceTargetConfig(project, Config.NAME_DEFAULT), configurer)
config(SourceTargetConfig(this, Config.NAME_DEFAULT), configurer)
}

fun config(name: String, configurer: Closure<*>) {
config(name) { ConfigureUtil.configure(configurer, this) }
}

fun config(name: String, configurer: Config.() -> Unit) {
config(SourceTargetConfig(project, name), configurer)
config(SourceTargetConfig(this, name), configurer)
}

fun inPlaceConfig(name: String, configurer: Closure<*>) {
inPlaceConfig(name) { ConfigureUtil.configure(configurer, this) }
}

fun inPlaceConfig(name: String, configurer: Config.() -> Unit) {
config(InPlaceConfig(project, name), configurer)
config(InPlaceConfig(this, name), configurer)
}

fun properties(action: Action<in ForkExtension>) {
action.execute(this)
}

fun property(name: String, action: Action<in PropertyDefinition>) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why in is here?

val definition = project.objects.newInstance(PropertyDefinition::class.java, name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you used new API introduced in 4.9 :) hope you know the purpose, teach me :)

action.execute(definition)
propertyDefinitions.add(definition)
}

private fun config(config: Config, configurer: Config.() -> Unit) {
Expand All @@ -49,11 +64,11 @@ open class ForkExtension(val project: Project) {

companion object {

const val NAME = "fork"
const val NAME = "fork"

fun of(project: Project): ForkExtension {
return project.extensions.getByType(ForkExtension::class.java)
}
fun of(project: Project): ForkExtension {
return project.extensions.getByType(ForkExtension::class.java)
}

}

Expand Down
30 changes: 19 additions & 11 deletions src/main/kotlin/com/neva/gradle/fork/config/Config.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.neva.gradle.fork.config

import com.neva.gradle.fork.ForkException
import com.neva.gradle.fork.ForkExtension
import com.neva.gradle.fork.config.properties.Property
import com.neva.gradle.fork.config.properties.PropertyPrompt
import com.neva.gradle.fork.config.rule.*
import com.neva.gradle.fork.gui.PropertyDialog
import com.neva.gradle.fork.template.TemplateEngine
import groovy.lang.Closure
import org.gradle.api.Project
import org.gradle.api.file.FileTree
import org.gradle.util.ConfigureUtil
import org.gradle.util.GFileUtils
Expand All @@ -14,13 +16,18 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.*

abstract class Config(val project: Project, val name: String) {
abstract class Config(private val forkExtension: ForkExtension, val name: String) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just extension or ext. you will not use any other extensions so that fully qualified forkExtension is over-expressive for me (whole plugin is fork scoped :))


val prompts = mutableMapOf<String, PropertyPrompt>()
private val prompts = mutableMapOf<String, PropertyPrompt>()

val props by lazy { promptFill() }
private val props by lazy { promptFill() }

val rules = mutableListOf<Rule>()
private val rules = mutableListOf<Rule>()

val project = forkExtension.project

val properties: List<Property>
get() = this.prompts.values.map { prompt -> forkExtension.propertyDefinitions.getProperty(prompt) }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this. not needed


abstract val sourcePath: String

Expand Down Expand Up @@ -61,19 +68,19 @@ abstract class Config(val project: Project, val name: String) {
return if (!value.isBlank()) value.toBoolean() else true
}

fun promptProp(prop: String, defaultProvider: () -> String?): () -> String {
fun promptProp(prop: String, defaultProvider: () -> String): () -> String {
prompts[prop] = PropertyPrompt(prop, defaultProvider)

return { props[prop] ?: throw ForkException("Fork prompt property '$prop' not bound.") }
}

fun promptProp(prop: String): () -> String {
return promptProp(prop) { null }
return promptProp(prop)
}

fun promptTemplate(template: String): () -> String {
templateEngine.parse(template).forEach { prop, defaultValue ->
prompts[prop] = PropertyPrompt(prop) { defaultValue }
templateEngine.parse(template).forEach { prop ->
prompts[prop] = PropertyPrompt(prop)
}

return { renderTemplate(template) }
Expand All @@ -83,7 +90,7 @@ abstract class Config(val project: Project, val name: String) {
return templateEngine.render(template, props)
}

private fun promptFill(): Map<String, String> {
private fun promptFill(): Map<String, String?> {
promptFillPropertiesFile(previousPropsFile)

try {
Expand Down Expand Up @@ -136,7 +143,7 @@ abstract class Config(val project: Project, val name: String) {
}

private fun promptValidate() {
val invalidProps = prompts.values.filter { !it.valid }.map { it.name }
val invalidProps = properties.filter(Property::isInvalid).map { it.name }
if (invalidProps.isNotEmpty()) {
throw ForkException("Fork cannot be performed, because of missing properties: $invalidProps."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing or invalid

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

val invalidProps = properties.filter(Property::isInvalid).map { it.name }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean, please correct message in exception

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing or invalid

I mean, we need to update message because now the cause of exception here is not filled property or property filled but not passing validation, build user should be informed about that`

+ " Specify them via properties file $propsFile or interactive mode.")
Expand Down Expand Up @@ -232,6 +239,7 @@ abstract class Config(val project: Project, val name: String) {

companion object {
const val NAME_DEFAULT = "default"
const val NAME_PROPERTIES = "properties"
}

}
4 changes: 2 additions & 2 deletions src/main/kotlin/com/neva/gradle/fork/config/InPlaceConfig.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.neva.gradle.fork.config

import org.gradle.api.Project
import com.neva.gradle.fork.ForkExtension

class InPlaceConfig(project: Project, name: String) : Config(project, name) {
class InPlaceConfig(forkExtension: ForkExtension, name: String) : Config(forkExtension, name) {

override val sourcePath: String by lazy { project.projectDir.absolutePath }

Expand Down
40 changes: 0 additions & 40 deletions src/main/kotlin/com/neva/gradle/fork/config/PropertyPrompt.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.neva.gradle.fork.config

import org.gradle.api.Project
import com.neva.gradle.fork.ForkExtension
import java.io.File

class SourceTargetConfig(project: Project, name: String) : Config(project, name) {
class SourceTargetConfig(forkExtension: ForkExtension, name: String) : Config(forkExtension, name) {

override val sourcePath: String by lazy(promptProp("sourcePath") {
project.projectDir.absolutePath
Expand Down
68 changes: 68 additions & 0 deletions src/main/kotlin/com/neva/gradle/fork/config/properties/Property.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.neva.gradle.fork.config.properties

import java.net.MalformedURLException
import java.net.URL
import java.nio.file.InvalidPathException
import java.nio.file.Paths

class Property(private val definition: PropertyDefinition, private val prompt: PropertyPrompt) {

val name: String
get() = prompt.name

var value: String
set(newValue) {
prompt.value = newValue
}
get() = prompt.valueOrDefault ?: definition.defaultValue

val label: String
get() = if (required) "${prompt.label}*" else prompt.label

val type: PropertyType = definition.type

private val required: Boolean
get() = definition.required

fun validate(): Validator {
val validator = Validator(value)
if (required && value.isBlank()) {
validator.error("This property is required.")
return validator
}
if (shouldBeValidated()) {
when (definition.validator) {
null -> applyDefaultValidation(validator)
else -> definition.validator?.execute(validator)
}
}
return validator
}

fun isInvalid() = validate().hasErrors()
pun-ky marked this conversation as resolved.
Show resolved Hide resolved

private fun applyDefaultValidation(validator: Validator) = when (type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool

PropertyType.PATH -> validatePath(validator)
PropertyType.URL -> validateUrl(validator)
else -> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this empty else branch looks strange, when is not exhaustive, maybe you could just remove it

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

then there is a warning, and I have to add curly braces, either way it is not perfect :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just else -> {} // with no new line

}
}

private fun validateUrl(validator: Validator) {
try {
URL(value)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about it. I am more sure about Apache Commons validator / https://github.com/Cognifide/gradle-aem-plugin/blob/master/src/main/kotlin/com/cognifide/gradle/aem/common/Formats.kt#L20

but who knows which is better, nvm

} catch (e: MalformedURLException) {
validator.error("This URL is invalid: \"${e.message}\"")
}
}

private fun validatePath(validator: Validator) {
try {
Paths.get(value)
} catch (e: InvalidPathException) {
validator.error("This path is invalid: \"${e.message}\"")
}
}

private fun shouldBeValidated() = required || value.isNotBlank()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool

}
Loading