From 1ab1f172949478dba28c750b6064d56dfd4c8099 Mon Sep 17 00:00:00 2001 From: uzzu Date: Tue, 1 Mar 2022 19:38:29 +0900 Subject: [PATCH] Implement hierarchical dotenv definitions feature --- .../co/uzzu/dotenv/gradle/DotEnvPlugin.kt | 58 ++------ .../co/uzzu/dotenv/gradle/DotEnvResolver.kt | 84 ++++++++++++ .../HierarchicalDotEnvDefinitionsTest.kt | 125 ++++++++++++++++++ 3 files changed, 219 insertions(+), 48 deletions(-) create mode 100644 plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt create mode 100644 plugin/src/test/kotlin/co/uzzu/dotenv/gradle/HierarchicalDotEnvDefinitionsTest.kt diff --git a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvPlugin.kt b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvPlugin.kt index cf999ec..b3c62c0 100644 --- a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvPlugin.kt +++ b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvPlugin.kt @@ -1,13 +1,10 @@ package co.uzzu.dotenv.gradle -import co.uzzu.dotenv.DotEnvParser import co.uzzu.dotenv.EnvProvider import co.uzzu.dotenv.SystemEnvProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ExtensionAware -import java.io.IOException -import java.nio.charset.Charset @Suppress("unused") class DotEnvPlugin : Plugin { @@ -15,57 +12,22 @@ class DotEnvPlugin : Plugin { check(target == target.rootProject) { "This plugin must be applied to root project." } val envProvider = SystemEnvProvider() - val dotenvTemplate = target.rootProject.dotenvTemplate() - val dotenvSource = target.rootProject.dotenvSource() - val dotenvMerged = dotenvTemplate.keys - .union(dotenvSource.keys) - .map { it to dotenvSource[it] } - .toMap() + val resolver = DotEnvResolver(target) + val rootVariables = resolver.resolve(target) - target.applyEnv(envProvider, dotenvMerged) - target.subprojects { it.applyEnv(envProvider, dotenvMerged) } + target.applyEnv(envProvider, rootVariables) + target.subprojects { it.applyEnv(envProvider, resolver.resolve(it)) } } private fun Project.applyEnv(envProvider: EnvProvider, dotenvProperties: Map) { - val env = - extensions.create("env", DotEnvRoot::class.java, envProvider, dotenvProperties) as ExtensionAware + val env = extensions.create( + "env", + DotEnvRoot::class.java, + envProvider, + dotenvProperties + ) as ExtensionAware dotenvProperties.forEach { (name, value) -> env.extensions.create(name, DotEnvProperty::class.java, envProvider, name, value) } } - - private fun Project.dotenvTemplate(filename: String = ".env.template"): Map = - readText(filename).let(DotEnvParser::parse) - - private fun Project.dotenvSource(filename: String = ".env"): Map { - val envFilename = System.getenv(Companion.KEY_ENV_FILE) - ?.takeIf { - val envFile = file(it) - if (!envFile.exists() || !envFile.canRead()) { - throw IOException( - buildString { - append("Could not read the dotenv file specified in the environment variables.") - append(" $KEY_ENV_FILE: $it,") - append(" path: ${envFile.absolutePath}") - } - ) - } - true - } - ?: filename - return readText(envFilename).let(DotEnvParser::parse) - } - - private fun Project.readText(filename: String): String { - val file = file(filename) - return if (file.exists()) { - file.readText(Charset.forName("UTF-8")) - } else { - "" - } - } - - companion object { - private const val KEY_ENV_FILE = "ENV_FILE" - } } diff --git a/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt new file mode 100644 index 0000000..6371a81 --- /dev/null +++ b/plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt @@ -0,0 +1,84 @@ +package co.uzzu.dotenv.gradle + +import co.uzzu.dotenv.DotEnvParser +import org.gradle.api.Project +import java.io.IOException +import java.nio.charset.Charset + +internal class DotEnvResolver(project: Project) { + + private val rootProject: Project + private val dotenvCache: MutableMap> = mutableMapOf() + + init { + rootProject = project.rootProject + } + + fun resolve(project: Project): Map { + val dotenvList = mutableListOf>() + + var current = project + while (true) { + val dotenv = current.dotenv() + dotenvList.add(0, dotenv) + if (current == rootProject) { + break + } + if (current.parent == null) { + break + } + current = checkNotNull(current.parent) + } + return dotenvList + .fold(mutableMapOf()) { destination, dotenv -> destination.apply { putAll(dotenv) } } + .toMap() + } + + private fun Project.dotenv(): Map { + require(this@dotenv.rootProject == this@DotEnvResolver.rootProject) + if (dotenvCache[this] == null) { + val dotenvTemplate = dotenvTemplate() + val dotenvSource = dotenvSource() + val variables = dotenvTemplate.keys + .union(dotenvSource.keys) + .associateWith { dotenvSource[it] } + dotenvCache[project] = variables + } + return checkNotNull(dotenvCache[project]) + } + + private fun Project.dotenvTemplate(filename: String = ".env.template"): Map = + readText(filename).let(DotEnvParser::parse) + + private fun Project.dotenvSource(filename: String = ".env"): Map { + val envFilename = System.getenv(KEY_ENV_FILE) + ?.takeIf { + val envFile = file(it) + if (!envFile.exists() || !envFile.canRead()) { + throw IOException( + buildString { + append("Could not read the dotenv file specified in the environment variables.") + append(" ${KEY_ENV_FILE}: $it,") + append(" path: ${envFile.absolutePath}") + } + ) + } + true + } + ?: filename + return readText(envFilename).let(DotEnvParser::parse) + } + + private fun Project.readText(filename: String): String { + val file = file(filename) + return if (file.exists()) { + file.readText(Charset.forName("UTF-8")) + } else { + "" + } + } + + companion object { + private const val KEY_ENV_FILE = "ENV_FILE" + } +} diff --git a/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/HierarchicalDotEnvDefinitionsTest.kt b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/HierarchicalDotEnvDefinitionsTest.kt new file mode 100644 index 0000000..0d33e4c --- /dev/null +++ b/plugin/src/test/kotlin/co/uzzu/dotenv/gradle/HierarchicalDotEnvDefinitionsTest.kt @@ -0,0 +1,125 @@ +package co.uzzu.dotenv.gradle + +import assertk.assertAll +import assertk.assertThat +import assertk.assertions.contains +import org.gradle.testkit.runner.GradleRunner +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +@Suppress("UnstableApiUsage") // GradleRunner#withPluginClasspath +class HierarchicalDotEnvDefinitionsTest { + + @TempDir + lateinit var projectDir: File + + @Test + fun subProjectOnlyVariables() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") } + println("[root] present FUGA: ${'$'}{env.isPresent("FUGA")}") + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] present FUGA: ${'$'}{env.isPresent("FUGA")}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + FUGA= + """.trimIndent() + ) + file( + "sub/.env", + """ + FUGA=200 + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] present FUGA: false") + assertThat(result.output).contains("[sub] present FUGA: true") + } + } + + @Test + fun useSubProjectVariablesIfDuplicatedWithRootProject() { + RootProject(projectDir) { + settingsGradle( + """ + include("sub") + """.trimIndent() + ) + buildGradle( + """ + plugins { + base + id("co.uzzu.dotenv.gradle") + } + println("[root] HOGE: ${'$'}{env.HOGE.value}") + """.trimIndent() + ) + file( + ".env.template", + """ + HOGE= + """.trimIndent() + ) + file( + ".env", + """ + HOGE=100 + """.trimIndent() + ) + directory("sub") + file( + "sub/build.gradle", + """ + println("[sub] HOGE: ${'$'}{env.HOGE.value}") + """.trimIndent() + ) + file( + "sub/.env.template", + """ + HOGE= + """.trimIndent() + ) + file( + "sub/.env", + """ + HOGE=200 + """.trimIndent() + ) + } + + val result = GradleRunner.create() + .withPluginClasspath() + .withProjectDir(projectDir) + .build() + + assertAll { + assertThat(result.output).contains("[root] HOGE: 100") + assertThat(result.output).contains("[sub] HOGE: 200") + } + } +}