From b39168cbd70e1ef04cfd00f00a3c55980f8b24fc Mon Sep 17 00:00:00 2001 From: uzzu Date: Mon, 27 Nov 2023 01:45:01 +0900 Subject: [PATCH] Fix behavior of project to by default ignore the filename option specified for this plugin in the parent project's gradle properties; This changes can be disabled by gradle.properties in the root project --- CHANGELOG.md | 4 + .../co/uzzu/dotenv/gradle/Configuration.kt | 165 ++++++++++++++ .../co/uzzu/dotenv/gradle/DotEnvResolver.kt | 42 +--- .../dotenv/gradle/ChangeDotEnvFileTest.kt | 204 ++++++++++++++++++ .../gradle/ChangeDotEnvTemplateFileTest.kt | 111 ++++++++++ 5 files changed, 495 insertions(+), 31 deletions(-) create mode 100644 plugin/src/main/kotlin/co/uzzu/dotenv/gradle/Configuration.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d37eb1..15e2862 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Changed +- The behavior of project to ignore the filename option specified for this plugin in the parent project's gradle properties by default. + - For example, if `dotenv.filename=.env.staging` is set in the root project, this setting will automatically apply to sub-projects as well. While this follows the correct resolution order of Gradle Properties, it has been a source of confusion for users working with dotenv. + - To disable this default behavior, add `dotenv.filename.ignore.parent=false` to the gradle.properties in the root project. + - A same update has been applied to the specification of template file names. To disable this default behavior, add `dotenv.template.filename.ignore.parent=false` to the gradle.properties in the root project. ### Deprecated diff --git a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/Configuration.kt b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/Configuration.kt new file mode 100644 index 0000000..22ec10d --- /dev/null +++ b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/Configuration.kt @@ -0,0 +1,165 @@ +package co.uzzu.dotenv.gradle + +import org.gradle.api.Project +import org.slf4j.LoggerFactory +import java.util.Properties + +internal interface Configuration { + val filename: String + val templateFilename: String +} + +internal interface RootConfiguration : Configuration { + val ignoreParentFilename: Boolean + val ignoreParentTemplateFilename: Boolean +} + +@Suppress("ConstPropertyName") +object ConfigurationKey { + const val Filename: String = RootConfigurationKey.Filename + const val TemplateFilename: String = RootConfigurationKey.TemplateFilename +} + +@Suppress("ConstPropertyName") +object RootConfigurationKey { + const val IgnoreParentFilename: String = "dotenv.filename.ignore.parent" + const val IgnoreParentTemplateFilename: String = "dotenv.template.filename.ignore.parent" + + const val Filename: String = "dotenv.filename" + const val TemplateFilename: String = "dotenv.template.filename" +} + +internal object DefaultConfiguration : Configuration { + override val filename: String = DefaultRootConfiguration.filename + override val templateFilename: String = DefaultRootConfiguration.templateFilename +} + +internal object DefaultRootConfiguration : RootConfiguration { + override val ignoreParentFilename: Boolean = true + override val ignoreParentTemplateFilename: Boolean = true + + override val filename: String = ".env" + override val templateFilename: String = ".env.template" +} + +internal class ConfigurationResolver( + private val project: Project, +) { + private val logger = LoggerFactory.getLogger(this::class.java.name) + + private val rootConfiguration: RootConfiguration by lazy { createRootConfiguration() } + + fun resolve(): Configuration { + return if (project == project.rootProject) { + rootConfiguration + } else { + val gradlePropertiesFromFile = project.gradlePropertiesFromFile() + ConfigurationImpl( + filename = resolveStringFor( + project, + gradlePropertiesFromFile, + ConfigurationKey.Filename, + DefaultRootConfiguration.filename, + rootConfiguration.ignoreParentFilename, + ), + templateFilename = resolveStringFor( + project, + gradlePropertiesFromFile, + ConfigurationKey.TemplateFilename, + DefaultRootConfiguration.templateFilename, + rootConfiguration.ignoreParentTemplateFilename, + ), + ) + } + } + + private fun resolveStringFor( + project: Project, + gradlePropertiesFromFile: Properties, + key: String, + defaultValue: String, + ignoreParent: Boolean + ): String = if (ignoreParent) { + gradlePropertiesFromFile.getProperty(key, defaultValue) + } else { + project.stringProperty(key, defaultValue) + } + + private fun createRootConfiguration(): RootConfiguration = + project.rootProject.let { + RootConfigurationImpl( + ignoreParentFilename = it.boolProperty( + RootConfigurationKey.IgnoreParentFilename, + DefaultRootConfiguration.ignoreParentFilename, + ), + ignoreParentTemplateFilename = it.boolProperty( + RootConfigurationKey.IgnoreParentTemplateFilename, + DefaultRootConfiguration.ignoreParentTemplateFilename, + ), + filename = it.stringProperty( + RootConfigurationKey.Filename, + DefaultRootConfiguration.filename, + ), + templateFilename = it.stringProperty( + RootConfigurationKey.TemplateFilename, + DefaultRootConfiguration.templateFilename, + ), + ) + } + + private fun Project.gradlePropertiesFromFile(): Properties { + val result = Properties() + val gradlePropertiesFile = file(Project.GRADLE_PROPERTIES) + if (gradlePropertiesFile.exists()) { + gradlePropertiesFile.inputStream().use { result.load(it) } + } + return result + } + + private fun Project.stringProperty(key: String, defaultValue: String): String = + if (properties.containsKey(key)) { + properties[key] as String + } else { + defaultValue + } + + private fun Project.boolProperty(key: String, defaultValue: Boolean): Boolean = + if (properties.containsKey(key)) { + @Suppress("MoveVariableDeclarationIntoWhen", "RedundantSuppression") + val value = properties[key] as String + when (value) { + "true" -> { + true + } + + "false" -> { + false + } + + else -> { + this@ConfigurationResolver.logger.warn( + buildString { + append("Could not resolve Boolean properties for key $key.") + append(""" Expect should be set "true" or "false", but was "$value". """) + append(" The plugin uses default value $defaultValue.") + } + ) + defaultValue + } + } + } else { + defaultValue + } +} + +private data class ConfigurationImpl( + override val filename: String, + override val templateFilename: String, +) : Configuration + +private data class RootConfigurationImpl( + override val ignoreParentFilename: Boolean, + override val ignoreParentTemplateFilename: Boolean, + override val filename: String, + override val templateFilename: String, +) : RootConfiguration diff --git a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt index 6b6011d..810f625 100644 --- a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt +++ b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt @@ -36,9 +36,10 @@ internal class DotEnvResolver(project: Project) { private fun Project.dotenv(): Map { require(this@dotenv.rootProject == this@DotEnvResolver.rootProject) + val config = ConfigurationResolver(this).resolve() if (dotenvCache[this] == null) { - val dotenvTemplate = dotenvTemplate() - val dotenvSource = dotenvSource() + val dotenvTemplate = dotenvTemplate(config) + val dotenvSource = dotenvSource(config) val variables = dotenvTemplate.keys .union(dotenvSource.keys) .associateWith { dotenvSource[it] } @@ -47,16 +48,16 @@ internal class DotEnvResolver(project: Project) { return checkNotNull(dotenvCache[project]) } - private fun Project.dotenvTemplate(): Map { - val filename = dotenvTemplateFilename() + private fun Project.dotenvTemplate(config: Configuration): Map { + val filename = config.templateFilename .let { - if (it != DEFAULT_TEMPLATE_FILENAME) { + if (it != DefaultConfiguration.templateFilename) { val templateFile = file(it) if (!templateFile.exists() || !templateFile.canRead()) { throw IOException( buildString { append("Could not read the dotenv template file specified in the gradle.properties.") - append(" $PROPERTY_TEMPLATE_FILENAME: $it,") + append(" ${ConfigurationKey.TemplateFilename}: $it,") append(" path: ${templateFile.absolutePath}") } ) @@ -67,16 +68,16 @@ internal class DotEnvResolver(project: Project) { return readText(filename).let(DotEnvParser::parse) } - private fun Project.dotenvSource(): Map { - val envFilename = dotenvFilename() + private fun Project.dotenvSource(config: Configuration): Map { + val envFilename = config.filename .let { - if (it != DEFAULT_FILENAME) { + if (it != DefaultConfiguration.filename) { val envFile = file(it) if (!envFile.exists() || !envFile.canRead()) { throw IOException( buildString { append("Could not read the dotenv file specified in the gradle.properties.") - append(" $PROPERTY_FILENAME: $it,") + append(" ${ConfigurationKey.Filename}: $it,") append(" path: ${envFile.absolutePath}") } ) @@ -87,20 +88,6 @@ internal class DotEnvResolver(project: Project) { return readText(envFilename).let(DotEnvParser::parse) } - private fun Project.dotenvFilename(): String = - if (properties.containsKey(PROPERTY_FILENAME)) { - properties[PROPERTY_FILENAME] as String - } else { - DEFAULT_FILENAME - } - - private fun Project.dotenvTemplateFilename(): String = - if (properties.containsKey(PROPERTY_TEMPLATE_FILENAME)) { - properties[PROPERTY_TEMPLATE_FILENAME] as String - } else { - DEFAULT_TEMPLATE_FILENAME - } - private fun Project.readText(filename: String): String { val file = file(filename) return if (file.exists()) { @@ -109,11 +96,4 @@ internal class DotEnvResolver(project: Project) { "" } } - - companion object { - private const val DEFAULT_FILENAME = ".env" - private const val DEFAULT_TEMPLATE_FILENAME = ".env.template" - private const val PROPERTY_FILENAME = "dotenv.filename" - private const val PROPERTY_TEMPLATE_FILENAME = "dotenv.template.filename" - } } diff --git a/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvFileTest.kt b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvFileTest.kt index 482ca2b..ccbcf8f 100644 --- a/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvFileTest.kt +++ b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvFileTest.kt @@ -192,4 +192,208 @@ class ChangeDotEnvFileTest { assertThat(result.output).contains("[sub] BAR: 2000") } } + + @Test + fun changeFileWithIgnoringParent() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] FOO: ${'$'}{env.FOO.value}") + """.trimIndent() + ) + file( + ".env.template", + """ + FOO= + """.trimIndent() + ) + file( + "env/.env.staging", + """ + FOO=1000 + """.trimIndent() + ) + file( + "gradle.properties", + """ + dotenv.filename=./env/.env.staging + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] BAR: ${'$'}{env.BAR.value}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + BAR= + """.trimIndent() + ) + file( + "sub/.env", + """ + BAR=200 + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] FOO: 1000") + assertThat(result.output).contains("[sub] BAR: 200") + } + } + + @Test + fun changeFileWithIgnoringParentByUsingCliOption() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] FOO: ${'$'}{env.FOO.value}") + """.trimIndent() + ) + file( + ".env.template", + """ + FOO= + """.trimIndent() + ) + file( + ".env", + """ + FOO=100 + """.trimIndent() + ) + file( + ".env.local", + """ + FOO=1000 + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] BAR: ${'$'}{env.BAR.value}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + BAR= + """.trimIndent() + ) + file( + "sub/.env", + """ + BAR=200 + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .withArguments("-Pdotenv.filename=.env.local") + .build() + + assertAll { + assertThat(result.output).contains("[root] FOO: 1000") + assertThat(result.output).contains("[sub] BAR: 200") + } + } + + @Test + fun changeFileWithoutIgnoringParent() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] FOO: ${'$'}{env.FOO.value}") + """.trimIndent() + ) + file( + ".env.template", + """ + FOO= + """.trimIndent() + ) + file( + "env/.env.staging", + """ + FOO=1000 + """.trimIndent() + ) + file( + "gradle.properties", + """ + dotenv.filename.ignore.parent=false + dotenv.filename=./env/.env.staging + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] BAR: ${'$'}{env.BAR.value}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + BAR= + """.trimIndent() + ) + directory("sub/env") + file( + "sub/env/.env.staging", + """ + BAR=2000 + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] FOO: 1000") + assertThat(result.output).contains("[sub] BAR: 2000") + } + } } diff --git a/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvTemplateFileTest.kt b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvTemplateFileTest.kt index bf180f6..c82d77a 100644 --- a/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvTemplateFileTest.kt +++ b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/ChangeDotEnvTemplateFileTest.kt @@ -166,4 +166,115 @@ class ChangeDotEnvTemplateFileTest { assertThat(result.output).contains("[sub] BAZ: 200") } } + + @Test + fun changeFileWithIgnoreParent() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] FOO: ${'$'}{env.FOO.orNull()}") + """.trimIndent() + ) + file( + ".env.example", + """ + FOO= + """.trimIndent() + ) + file( + "gradle.properties", + """ + dotenv.template.filename=.env.example + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] BAR: ${'$'}{env.BAR.orNull()}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + BAR= + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] FOO: null") + assertThat(result.output).contains("[sub] BAR: null") + } + } + + @Test + fun changeFileWithoutIgnoringParent() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] FOO: ${'$'}{env.FOO.orNull()}") + """.trimIndent() + ) + file( + ".env.example", + """ + FOO= + """.trimIndent() + ) + file( + "gradle.properties", + """ + dotenv.template.filename.ignore.parent=false + dotenv.template.filename=.env.example + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] BAR: ${'$'}{env.BAR.orNull()}") + """.trimIndent() + ) + file( + "sub/.env.example", + """ + BAR= + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] FOO: null") + assertThat(result.output).contains("[sub] BAR: null") + } + } }