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
Merged

#5 Adding DSL to define properties #9

merged 12 commits into from
Jan 14, 2019

Conversation

mierzwid
Copy link
Collaborator

@mierzwid mierzwid commented Jan 2, 2019

  • user is able to specify
    • default value
    • if field is required
    • validator
  • validation messages displayed in the props dialog

I propose the following DSL:

fork {
    config {
        cloneFiles()
        moveFiles(mapOf(
                "/com/company/aem/example" to "/{{projectGroup|substitute(\".\", \"/\")}}/{{projectName}}",
                "/Example" to "/{{projectLabel}}",
                "/example" to "/{{projectName}}"
        ))
        replaceContents(mapOf(
                "com.company.aem.example" to "{{projectGroup}}.{{projectName}}",
                "com.company.aem" to "{{projectGroup}}",
                "Example" to "{{projectLabel}}",
                "example" to "{{projectName}}"
        ))
    }
    defineProperties(mapOf(
            "aemSmbDomain" to {
                required()
                defaultValue = "Damian is cool"
                validator = {
                }
            },
            "aemInstanceLocalJarUrl" to {
                required()
                validator = {
                    if (!it.contains("@")) error("Oh, this is not an email!")
                }
            }
    ))
}

- user is able to specify
-- default value
-- if field is required
-- validator
- validation messages displayed in the props dialog
@pun-ky
Copy link
Contributor

pun-ky commented Jan 2, 2019

In validator dsl please change receiver instead of using 'it'

@pun-ky
Copy link
Contributor

pun-ky commented Jan 2, 2019

How about introducing new type of field, checkbox / boolean?

@pun-ky
Copy link
Contributor

pun-ky commented Jan 2, 2019

Maybe we should inverse logic because most of fields will be 'required', so that maybe you need to make it required by default an introduce instead 'optional' method

@@ -39,21 +41,33 @@ open class ForkExtension(val project: Project) {
}

fun inPlaceConfig(name: String, configurer: Config.() -> Unit) {
config(InPlaceConfig(project, name), configurer)
if (configExists(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 dislike extracting such trivial methods

@mierzwid
Copy link
Collaborator Author

mierzwid commented Jan 4, 2019

PR updated, I will update README in next PR.
New DSL looks like this:

fork {
    properties(mapOf(
            "enableSmbClient" to {
                type = CHECKBOX
                defaultValue = "true"
            },
            "aemInstanceAuthorHttpUrl" to {
                defaultValue = "http://localhost:4502"
                validator = {
                    if (!value.startsWith("http", ignoreCase = true)) error("This is not an HTTP URL!")
                }
            },
            "aemInstanceAuthorJvmOpts" to {
                optional()
                defaultValue = "-server -Xmx1024m -XX:MaxPermSize=256M -Djava.awt.headless=true"
                validator = {
                    if (!value.startsWith("-")) error("This is not a JVM option!")
                }
            },
            "aemSmbUsername" to {
                defaultValue = System.getProperty("user.name")
            },
            "aemSftpPassword" to {
                type = PASSWORD
            },
            "projectGroup" to {
                defaultValue = "org.neva"
            }
    ))
    config {
        cloneFiles()
        moveFiles(mapOf(
                "/com/company/aem/example" to "/{{projectGroup|substitute(\".\", \"/\")}}/{{projectName}}",
                "/Example" to "/{{projectLabel}}",
                "/example" to "/{{projectName}}"
        ))
        replaceContents(mapOf(
                "com.company.aem.example" to "{{projectGroup}}.{{projectName}}",
                "com.company.aem" to "{{projectGroup}}",
                "Example" to "{{projectLabel}}",
                "example" to "{{projectName}}"
        ))
    }
}

Copy link
Contributor

@pun-ky pun-ky left a comment

Choose a reason for hiding this comment

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

very nice one!!! ;)

src/main/kotlin/com/neva/gradle/fork/config/Config.kt Outdated Show resolved Hide resolved
@@ -136,7 +142,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 { prop -> prop.validate().hasErrors() }.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

if (name.isBlank()) throw PropertyException("Name of property definition cannot be blank!")
}

val PASSWORD = PropertyType.PASSWORD
Copy link
Contributor

Choose a reason for hiding this comment

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

what is the purpose of these vals?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Those values are used in DSL to simplify property type specification:
type = CHECKBOX
instead of
type = com.neva.gradle.fork.config.properties.PropertyType.CHECKBOX

Copy link
Contributor

@pun-ky pun-ky Jan 7, 2019

Choose a reason for hiding this comment

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

ok, but even Gradle is not doing this that way (see Test task and setTestLogging accepting enum), importing enum will be not so bad and I am opting for this approach instead

errors.add(message)
}

fun hasErrors() = errors.isNotEmpty()
Copy link
Contributor

Choose a reason for hiding this comment

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

hasErrors -> passing

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean:
val hasErrors = errors::isNotEmpty
?

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, name of method , nvm

Copy link
Contributor

@pun-ky pun-ky left a comment

Choose a reason for hiding this comment

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

how about introducing built-int / predefined validators for properties ending with Path or Url ? ;) which of course could be overidden / relaxed

}

private fun setPropertyValue() {
when (propField) {
Copy link

Choose a reason for hiding this comment

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

can we use when as an expression here?

private fun setPropertyValue() = when (propField) {
    ....
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Only if I would add an empty else block. Since when is not returning any value I would leave it as it is.

Choose a reason for hiding this comment

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

Ok now i get it, we could also use sealed class and wrap all propField types in it but i think this is a bit different case. THX

@mierzwid
Copy link
Collaborator Author

mierzwid commented Jan 4, 2019

I added two new types: PATH & URL and default validation for them. By default properties whose names end with "path" or "url" get those types. This can be overridden.

@mierzwid
Copy link
Collaborator Author

mierzwid commented Jan 4, 2019

I already pushed updates to reflect new features in README.

README.md Outdated
@@ -129,6 +130,51 @@ Properties can be provided by (order makes precedence):

4. Mixed approach.

### Validating properties
Copy link
Contributor

Choose a reason for hiding this comment

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

defining section should be before validating ;)

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, I changed naming

Copy link
Contributor

@pun-ky pun-ky left a comment

Choose a reason for hiding this comment

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

approved but code style may be still improved

README.md Outdated
fork {
properties(mapOf(
"enableSmbClient" to {
type = CHECKBOX
Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer PropertyType.CHECKBOX with import or introduce a method checkbox() in a same manner as optional() that mutes internal state

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

optional()
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

import groovy.lang.Closure
import org.gradle.api.Project
import org.gradle.api.tasks.Input
import org.gradle.util.ConfigureUtil

open class ForkExtension(val project: Project) {

private val propertiesDefinitions = PropertyDefinitions()
Copy link
Contributor

Choose a reason for hiding this comment

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

only last part plural / propertyDefinitions

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

@@ -23,23 +27,27 @@ open class ForkExtension(val project: Project) {
}

fun config(configurer: Config.() -> Unit) {
config(SourceTargetConfig(project, Config.NAME_DEFAULT), configurer)
config(SourceTargetConfig(project, propertiesDefinitions, Config.NAME_DEFAULT), configurer)
Copy link
Contributor

Choose a reason for hiding this comment

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

how about changing to

SourceTargetConfig(this, Config.NAME_DEFAULT)

the ForkExtension is already holding project, property definitions could be then public or internal. don't be afraid od passing these heavylooking objects. this will simplify code much

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

@@ -14,13 +17,16 @@ import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.*

abstract class Config(val project: Project, val name: String) {
abstract class Config(val project: Project, private val propertyDefinitions: PropertyDefinitions, 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.

Config(ext: ForkExtension, val name: String)

class PropertyDefinitions {
private val definitions = mutableMapOf<String, PropertyDefinition>()

fun configure(propertiesConfiguration: Map<String, PropertyDefinition.() -> Unit>) {
Copy link
Contributor

Choose a reason for hiding this comment

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

you are in a scope of Property (class name is scoping Property keyword) so that why no just configuration as an arg name

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

definitions += propertiesConfiguration.mapValues { PropertyDefinition(it.key).apply(it.value) }
}

fun getProperty(prompt: PropertyPrompt): Property {
Copy link
Contributor

Choose a reason for hiding this comment

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

however, you are right, in that case getProperty is ok.. :)

required = false
}

private fun calculateDefaultType() = when {
Copy link
Contributor

Choose a reason for hiding this comment

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

calculate is rather math related. I prefer here determine / examine etc

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

import java.awt.Color
import javax.swing.*

class PropertyDialogField(private val property: Property, private val propField: JComponent, private val validationMessageLabel: JLabel, private val dialog: JDialog) {
Copy link
Contributor

Choose a reason for hiding this comment

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

please reformat that to
https://kotlinlang.org/docs/reference/coding-conventions.html#class-header-formatting

each field in constructor on separate line

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

get() = property.value

fun validateAndDisplayErrors(): Boolean {
setPropertyValue()
Copy link
Contributor

Choose a reason for hiding this comment

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

set indicates setter; please avoid setter like methods which are not setters actually

maybe better assign

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

@pun-ky pun-ky mentioned this pull request Jan 9, 2019
@mierzwid
Copy link
Collaborator Author

I pushed changes addressing CR remarks. This version doesn't support GroovyDSL.

I have another branch that supports Groovy. However, validators don't work in Groovy and the overall solution is ugly hacking.

@mierzwid
Copy link
Collaborator Author

I finally added support for Groovy DSL, now DSL is exactly the same in Groovy and Kotlin:

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!")
                }
            }
            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

#### Property definition
Property definition can consists of:
* 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

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?

}

fun property(name: String, action: Action<in PropertyDefinition>) {
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 :)

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

package com.neva.gradle.fork.config.properties

class PropertyDefinitions {
private val definitions = mutableMapOf<String, PropertyDefinition>()
Copy link
Contributor

Choose a reason for hiding this comment

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

mine and Cognifide's is matching each other, I have no problem with that :>

Intellij default + blank line around fields :D

open class PropertyDefinition @Inject constructor(val name: String) {

init {
if (name.isBlank()) throw PropertyException("Name of property definition cannot be blank!")
Copy link
Contributor

Choose a reason for hiding this comment

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

follow Kotlin code style / if need { }

}

fun validator(validateAction: Action<in Validator>) {
validator = validateAction
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 this.validator = validator :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I dislike repeating type in var/field name

this.defaultValue = defaultValue
}

fun url(defaultValue: String = "") {
Copy link
Contributor

Choose a reason for hiding this comment

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

consider implementing select option, it should not be hard?`

fun select(options: List<String>, defaultValue: String)` // when values <=> keys
fun select(options: Map<String, String>, defaultValue: String)

private val propField: JComponent,
private val validationMessageLabel: JLabel,
private val dialog: JDialog
) {
Copy link
Contributor

Choose a reason for hiding this comment

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

thx

@pun-ky pun-ky changed the base branch from master to develop January 14, 2019 13:10
@pun-ky pun-ky merged commit bc90d2e into neva-dev:develop Jan 14, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants