Skip to content

Commit

Permalink
Implement hierarchical dotenv definitions feature
Browse files Browse the repository at this point in the history
  • Loading branch information
uzzu committed Feb 28, 2022
1 parent 68cd70e commit e0159dc
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 48 deletions.
58 changes: 10 additions & 48 deletions plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,71 +1,33 @@
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<Project> {
override fun apply(target: Project) {
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<String, String?>) {
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<String, String> =
readText(filename).let(DotEnvParser::parse)

private fun Project.dotenvSource(filename: String = ".env"): Map<String, String> {
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"
}
}
84 changes: 84 additions & 0 deletions plugin/src/main/kotlin/co/uzzu/dotenv/gradle/DotEnvResolver.kt
Original file line number Diff line number Diff line change
@@ -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 cache: MutableMap<Project, Map<String, String?>> = mutableMapOf()

init {
rootProject = project.rootProject
}

fun resolve(project: Project): Map<String, String?> =
if (cache[project] != null) {
checkNotNull(cache[project])
} else {
cache[project] = createDotEnvMap(project)
checkNotNull(cache[project])
}

private fun createDotEnvMap(project: Project): Map<String, String?> {
require(project.rootProject == rootProject)
val variablesList = mutableListOf<Map<String, String?>>()

var current = project
while (true) {
val dotenvTemplate = current.dotenvTemplate()
val dotenvSource = current.dotenvSource()
val variables = dotenvTemplate.keys
.union(dotenvSource.keys)
.associateWith { dotenvSource[it] }
variablesList.add(0, variables)
if (current == rootProject) {
break
}
if (current.parent == null) {
break
}
current = checkNotNull(current.parent)
}
return variablesList
.fold(mutableMapOf<String, String?>()) { destination, variables -> destination.apply { putAll(variables) } }
.toMap()
}

private fun Project.dotenvTemplate(filename: String = ".env.template"): Map<String, String> =
readText(filename).let(DotEnvParser::parse)

private fun Project.dotenvSource(filename: String = ".env"): Map<String, String> {
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"
}
}
Original file line number Diff line number Diff line change
@@ -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")
}
}
}

0 comments on commit e0159dc

Please sign in to comment.