Skip to content

An application property generator framework that validates your property values on runtime.

License

Notifications You must be signed in to change notification settings

propactive/propactive

Repository files navigation

Propactive

CICD Code Climate Test Coverage Latest Version GitHub License

An application property generator framework that validates and generates yourapplication.propertiesfile on runtime.

Table of Contents

Setup

Using theplugins&dependenciesblocks, you can set up Propactive as follows:

Kotlin DSL:

plugins {
id("io.github.propactive") version"2.1.0"
}

dependencies {
implementation("io.github.propactive:propactive-jvm:2.1.0")
}

Minimal example:

/** Placed at the root of the main source directory. (i.e. src/main/kotlin/ApplicationProperties.kt)*/
@Environment
objectApplicationProperties {
@Property(["HelloWorld"])
constvalproperty="propactive.property.key"
}

Running the Propactive task./gradlew generateApplicationPropertieswill generate the following properties file:

propactive.property.key=HelloWorld

The file will be namedapplication.propertiesand it will be located within thedistdirectory of your set build destination. If you want to learnhow to configure the location of your properties object, how to set a custom application properties filename,orhow to work with multiple environments when using Propactive, then have a look at the rest this guide.

Gradle Plugin configurations:

Proactive provides a plugin extension that allows you to specify the destination of the created application properties file, set the location of the implementation class, and/or specify which environments you want to generate application properties files for by default.

Here is an example that generates the files to a directory calledpropertieswithin your build folder, locates the implementation class of the application properties object atio.github.propactive.demo.Properties,and will only generate theprodenvironment application properties within a file namedapplication.propertieswhen the taskgenerateApplicationPropertiesis executed:

propactive {
environments="prod"
implementationClass="io.github.propactive.demo.Properties"
destination=layout.buildDirectory.dir("properties").get().asFile.absolutePath
filenameOverride="application.properties"
}

The default values for thepropactiveextension are as follows:

propactive {
environments="*"
implementationClass="ApplicationProperties"
destination=layout.buildDirectory.dir("resources/main").get().asFile.absolutePath
autoGenerateApplicationProperties=true
filenameOverride=null
classCompileDependency=null
}

Optional: Enable Class Compile Optimisation For Custom Compilation Tasks

Since Propactive is a runtime property generator that relies on loading a properties class at runtime, we need to ensure that the properties class is compiled before thegenerateApplicationPropertiestask is executed. By default, the plugin will set theclassCompileDependencyoption tocompileJavaor/andcompileKotlinif you are using Kotlin.

However, you can set theclassCompileDependencyoption to something else if you want to optimise your build time, or if you are compiling your classes from a different source set. For example, if you areusing Scala, you can set theclassCompileDependencyoption tocompileScala:

propactive {
classCompileDependency="compileScala"
}

Optional: Disable Auto-Generated Properties File

By default, the plugin will generate the properties file when theclassestask is executed. If you want to disable this behaviour, you can set theautoGenerateApplicationPropertiesoption tofalse:

propactive {
autoGenerateApplicationProperties=false
}

Plugin Tasks

Propactive provides 2 tasks that you can use to generate and validate your application properties files:

Propactive tasks
----------------
generateApplicationProperties - Generates application properties file for each given environment.

Optional configurations:
-Penvironments
Description: Comma separated list of environments to generate the properties for.
Example: "test,stage,prod"
Default: "*" (All provided environments)
-PimplementationClass
Description: Sets the location of your properties object.
Example: "com.package.path.to.your.ApplicationProperties"
Default: "ApplicationProperties" (At the root of your project, without a package path.)
-Pdestination
Description: Sets the location of your generated properties file within the build directory.
Example: layout.buildDirectory.dir( "properties" ).get().asFile.absolutePath
Default: layout.buildDirectory.dir( "resources/main" ).get().asFile.absolutePath (In the main resources directory)
-PfilenameOverride
Description: Allows overriding given filename for when you're generating properties for a single environment.
Example: "dev-application.properties"
Note: This can only be used when generating application properties for a singular environment.

validateApplicationProperties - Validates the application properties without generating any files.

Optional configurations:
-PimplementationClass
Description: Sets the location of your properties object.
Example: "com.package.path.to.your.ApplicationProperties"
Default: "ApplicationProperties" (At the root of your project, without a package path.)

Runtime Property Validation

One of the key features Propactive has is the ability to validate given property values on runtime in a modular manner. Let's consider the following scenario, You have an environment dependant URL values for a property calledapp.web.server.url:

  • prod:https:// prodland
  • test:http:// nonprodland
  • dev:http://127.0.0.1/

Therefore, you will end up creating 3application.propertiesfiles:

#prod-application.properties
app.web.server.url=https:// prodland
#test-application.properties
app.web.server.url=http:// nonprodland
#dev-application.properties
app.web.server.url=http://127.0.0.1/

Usually, this is fine, but as you scale, you have many environments, and dozens of application properties that have different values for each environment. Therefore, this becomes a mundane process and error-prone. Not only you will need to define a constant for app.web.server.urlto test your property values, and perhaps another constant to reference it on your application side, you will also need to parse each file if you want to test that the URL value is of valid format, if such precision is required.

With Propactive, this could simply be written like so:

@Environment(["prod/test/dev: *-application.properties"])
objectProperties {
@Property(
value=[
"prod: https:// prodland",
"test: http:// nonprodland",
"dev: http://127.0.0.1/",
],
type=URL::class
)
constvalappWebServerUrlPropertyKey="app.web.server.url"
}

Now locally, orwithin your CI/CD,you can generate the required application properties file by running the following command: (omit-Penvironmentsoption to generate the files for all environments)

#TIP: This can be added as part of your deployment or build process as required
./gradlew generateApplicationProperties -Penvironments=prod

This will generate a file namedprod-application.propertieswith the following entries:

app.web.server.url=https:// prodland

On top of that, it will validate the key value set by type (e.g.URL), if it's an invalid type, it will fail with a verbose error. For example, the error message below is produced by having a malformed protocol keyword: (e.g. "htps" instead of "https" )

Property named: "propactive.demo.url.key" within environment named: "prod" was expected to be of type: "URL", but value was: "htps:// prodland"

You can have a look below for thelist of natively supported property typesor learn how to write your custom property typesthat you can use for runtime validation.

Natively Supported Property Types

Propactive comes with a set of natively supported property types that you can use for validating your property values on runtime. Below is a reference for each type and the specification followed:

If you believe we missed a common property type, feel free to let us know by opening anissueor make a PR, and we will be happy to merge. Otherwise, please see the next section to learnhow to write your custom property types.

Writing Your Custom Property Types

Writing your custom property types is quite straightforward, you just need to implement thepropactive.type.Typeinterface, override thevalidatetype, returntrue(or the constantio.github.propactive.type.Type.VALID) when validation pass orfalse(or the constantio.github.propactive.type.Type.INVALID) when the validation fails, then you can use the type within your@Propertyannotation as usual.

Here is aPORT_NUMBERtype that you can use to validate if a port number is within a valid range: (i.e.0 till 65535)

importio.github.propactive.type.Type

objectPORT_NUMBER: Type {
overridefunvalidate(value:Any)=value
.runCatching { toString().toInt() }
.getOrDefault(-1)
.let{ number->numberin(1..65535) }
}
importio.github.propactive.environment.Environment
importio.github.propactive.property.Property

@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties",
])
objectApplicationProperties {
@Property(
value=[
"prod: 433",
"stage/test: 80",
"dev: 8080",
],
type=PORT_NUMBER::class
)
constvalappWebServerPortPropertyKey="app.web.server.port"
}

Running./gradlew generateApplicationPropertieswill generate the relevant application properties files, and the typed port number validation will occur at runtime. You cansee this code running within our demo project.

Working With Multiple Environments

Working with multiple environments' means you will need a way to distinguish between different environment filenames and different environment values. Proactive provides you the option to define multiple environments perApplicationProperties object and allows you to cascade multiple key entries against a single value.

Below is an example with 4 environments wherestageandtestshare the same values, butprodanddevhave separate entries. Note that the@Environmentannotation supports a special wildcard expansion key (*) that is evaluated to the environment name. (i.e. in the following examplestageandtestentries will generate 2 files named stage-application.propertiesandtest-application.properties)

@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties"
])
objectApplicationProperties {
@Property(
value=[
"prod: https:// prodland",
"stage/test: http:// nonprodland",
"dev: http://127.0.0.1/",
],
type=URL::class
)
constvalappWebServerUrlPropertyKey="app.web.server.url"
}

You can also map a property value with multiple environment keys. Above example shows that thestageandtestentries will share the same"app.web.server.url"value. (i.e.http:// nonprodland)

Blank values for properties (Rela xing mandatory values condition)

By default, properties key are expected to have a value assigned to it, and will error out if not. (i.e. an environment key cannot have a blank value) This condition can be relaxed by setting themandatoryoption to false:

@Environment
objectApplicationProperties {
@Property(mandatory=false)
constvalproperty="propactive.property.key"
}

This will generate a YAML file withpropactive.property.key=and assign no value (blank) to it.

Further Test Support With The Environment Factory

Sometimes you might want to do more granular testing on the application property keys and values. For that, we provide theEnvironmentFactoryobject for creating an Environment model that you can use for extracting any property name or value to uphold any assertions. Here is an example:

//The properties object
@Environment([
"prod: application.properties",
"stage/test: *-application.properties",
"dev: localhost-application.properties",
])
objectProperties {
@Property(
value=[
"prod: 3000",
"stage/test: 10000",
"dev: 30000",
],
type=INTEGER::class
)
constvaltimoutInMsPropertyKey="propactive.demo.timout-in-ms.key"
}

//The test class that's making use of the EnvironmentFactory object:
classPropertiesTest{
@Test
funshouldHaveTimeoutLargerThan250ms() {
findAllMatchingPropertiesFor(timoutInMsPropertyKey)
.forEach {
assertTrue(
it.value.toInt()>250,
"Expected:$timoutInMsPropertyKeyfor environment:${it.environment}to have a value larger than 250ms but was:${it.value}"
)
}
}

privatefunfindAllMatchingPropertiesFor(propertyKey:String):List<PropertyModel>=EnvironmentFactory
.create(Properties::class)
.mapNotNull { env->env.properties.firstOrNull { it.name==propertyKey } }
}

You cansee this code running within our demo project.

Demo Project (w/ a CICD Pipeline)

To make the usecase of the Proactive framework clear, we provide an example project that makes use of above-mentioned features and is integrated with its own CI/CD pipeline. You will see how the application properties are validated and generated per environment. To top it up, a docker image is created/ran for each environment on deployment with a job summary outputted for each environment properties.

The project can be found here:propactive/proactive-demo