Initial commit
This commit is contained in:
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# sbt build artifacts
|
||||
target/
|
||||
project/target/
|
||||
project/project/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.bsp/
|
||||
.metals/
|
||||
.vscode/
|
||||
*.iml
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
20
build.sbt
Normal file
20
build.sbt
Normal file
@@ -0,0 +1,20 @@
|
||||
lazy val root = (project in file("."))
|
||||
.settings(
|
||||
name := "summercms-compass",
|
||||
organization := "com.golem15.summer",
|
||||
version := "0.1.0-SNAPSHOT",
|
||||
scalaVersion := "3.3.4",
|
||||
|
||||
libraryDependencies ++= Seq(
|
||||
"ma.chinespirit" %% "jig" % "0.1.0",
|
||||
"org.scalameta" %% "munit" % "1.0.3" % Test,
|
||||
),
|
||||
|
||||
scalacOptions ++= Seq(
|
||||
"-Wunused:all",
|
||||
"-deprecation",
|
||||
"-feature",
|
||||
),
|
||||
|
||||
testFrameworks += new TestFramework("munit.Framework"),
|
||||
)
|
||||
1
project/build.properties
Normal file
1
project/build.properties
Normal file
@@ -0,0 +1 @@
|
||||
sbt.version=1.10.7
|
||||
49
src/main/scala/summer/compass/Compass.scala
Normal file
49
src/main/scala/summer/compass/Compass.scala
Normal file
@@ -0,0 +1,49 @@
|
||||
package summer.compass
|
||||
|
||||
import machinespir.it.jig.ConfigReader
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Public API for configuration management.
|
||||
*
|
||||
* Compass is the CMS orchestration layer on top of Jig (HOCON parsing).
|
||||
* It handles file discovery, environment layering, plugin namespacing,
|
||||
* and runtime overrides — similar to Laravel's Illuminate\Config.
|
||||
*/
|
||||
trait Compass:
|
||||
|
||||
// --- String-path access (dot-notation, dynamic) ---
|
||||
|
||||
def getString(key: String, default: String = ""): String
|
||||
def getInt(key: String, default: Int = 0): Int
|
||||
def getBoolean(key: String, default: Boolean = false): Boolean
|
||||
def getOption(key: String): Option[String]
|
||||
def has(key: String): Boolean
|
||||
|
||||
// --- Typed access via Jig ConfigReader ---
|
||||
|
||||
/** Load a config section into a typed case class. */
|
||||
def load[T: ConfigReader](section: String): Either[String, T]
|
||||
|
||||
// --- Plugin config registration ---
|
||||
|
||||
/** Register a plugin namespace with its config directory path. */
|
||||
def addNamespace(namespace: String, path: Path): Unit
|
||||
|
||||
// --- Runtime overrides ---
|
||||
|
||||
/** Set a runtime config override (highest priority, in-memory). */
|
||||
def set(key: String, value: String): Unit
|
||||
|
||||
/** Persist runtime overrides to config/env/{env}/overrides.conf. */
|
||||
def persist(): Either[String, Unit]
|
||||
|
||||
// --- Environment ---
|
||||
|
||||
def environment: String
|
||||
def configPath: Path
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
/** Reload all config from disk, clearing caches and runtime overrides. */
|
||||
def reload(): Unit
|
||||
82
src/main/scala/summer/compass/ConfigDiscovery.scala
Normal file
82
src/main/scala/summer/compass/ConfigDiscovery.scala
Normal file
@@ -0,0 +1,82 @@
|
||||
package summer.compass
|
||||
|
||||
import machinespir.it.jig.{Config, ConfigFactory}
|
||||
import java.nio.file.{Files, Path}
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
/**
|
||||
* Stateless utility for config file discovery, environment detection,
|
||||
* and HOCON merging with environment overlays.
|
||||
*/
|
||||
object ConfigDiscovery:
|
||||
|
||||
/** Detect environment from SUMMER_ENV env var, defaulting to "production". */
|
||||
def detectEnvironment(): String =
|
||||
Option(System.getenv("SUMMER_ENV")).filter(_.nonEmpty).getOrElse("production")
|
||||
|
||||
/**
|
||||
* Scan a config directory for *.conf files.
|
||||
* Returns a map of section name (filename without .conf) to file path.
|
||||
*/
|
||||
def discoverSections(configDir: Path): Map[String, Path] =
|
||||
if !Files.isDirectory(configDir) then Map.empty
|
||||
else
|
||||
val builder = Map.newBuilder[String, Path]
|
||||
val stream = Files.list(configDir)
|
||||
try
|
||||
stream.iterator.asScala
|
||||
.filter(p => Files.isRegularFile(p) && p.toString.endsWith(".conf"))
|
||||
.foreach { p =>
|
||||
val filename = p.getFileName.toString
|
||||
val section = filename.stripSuffix(".conf")
|
||||
builder += section -> p
|
||||
}
|
||||
finally stream.close()
|
||||
builder.result()
|
||||
|
||||
/**
|
||||
* Load a single section with environment overlay.
|
||||
* The env file's values override the base file's values via withFallback.
|
||||
*/
|
||||
def loadSection(configDir: Path, section: String, env: String): Config =
|
||||
val basePath = configDir.resolve(s"$section.conf")
|
||||
val envPath = configDir.resolve(s"env/$env/$section.conf")
|
||||
val baseConfig = loadFile(basePath)
|
||||
val envConfig = loadFile(envPath)
|
||||
// env overrides base: envConfig takes priority, base is fallback
|
||||
envConfig.withFallback(baseConfig)
|
||||
|
||||
/**
|
||||
* Load all discovered sections into a single merged Config.
|
||||
* Each section is namespaced under its filename:
|
||||
* database.conf -> database.host, database.port, etc.
|
||||
*/
|
||||
def loadAll(configDir: Path, env: String): Config =
|
||||
val sections = discoverSections(configDir)
|
||||
sections.foldLeft(ConfigFactory.empty()) { case (acc, (section, _)) =>
|
||||
val sectionConfig = loadSection(configDir, section, env)
|
||||
// Wrap section config under its name as namespace
|
||||
val wrapped = sectionConfig.atKey(section)
|
||||
acc.withFallback(wrapped)
|
||||
}
|
||||
|
||||
/** Load all .conf files from a plugin config directory (flat, no env overlay). */
|
||||
def loadPluginConfig(pluginConfigDir: Path): Config =
|
||||
if !Files.isDirectory(pluginConfigDir) then ConfigFactory.empty()
|
||||
else
|
||||
val stream = Files.list(pluginConfigDir)
|
||||
try
|
||||
stream.iterator.asScala
|
||||
.filter(p => Files.isRegularFile(p) && p.toString.endsWith(".conf"))
|
||||
.foldLeft(ConfigFactory.empty()) { (acc, path) =>
|
||||
val config = loadFile(path)
|
||||
acc.withFallback(config)
|
||||
}
|
||||
finally stream.close()
|
||||
|
||||
/** Load a single HOCON file, returning empty config if it doesn't exist. */
|
||||
private def loadFile(path: Path): Config =
|
||||
if Files.exists(path) then
|
||||
ConfigFactory.parseFile(path.toFile)
|
||||
else
|
||||
ConfigFactory.empty()
|
||||
161
src/main/scala/summer/compass/SummerCompass.scala
Normal file
161
src/main/scala/summer/compass/SummerCompass.scala
Normal file
@@ -0,0 +1,161 @@
|
||||
package summer.compass
|
||||
|
||||
import machinespir.it.jig.{Config, ConfigFactory, ConfigReader, read}
|
||||
import java.nio.file.{Files, Path}
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import scala.util.Try
|
||||
|
||||
// Thread-safe implementation of [[Compass]].
|
||||
//
|
||||
// Override priority (highest wins):
|
||||
// 1. Runtime set() overrides (in-memory ConcurrentHashMap)
|
||||
// 2. overrides.conf from env dir (loaded at boot into mergedConfig)
|
||||
// 3. Environment overlay (config/env/{env}/*.conf)
|
||||
// 4. Base config (config/*.conf)
|
||||
final class SummerCompass(
|
||||
baseConfigDir: Path,
|
||||
env: String = ConfigDiscovery.detectEnvironment(),
|
||||
) extends Compass:
|
||||
|
||||
// --- Mutable state (thread-safe) ---
|
||||
|
||||
/** Unified config from all discovered files + env overlay + persisted overrides. */
|
||||
@volatile private var mergedConfig: Config = loadMergedConfig()
|
||||
|
||||
/** Plugin namespace -> config directory path. */
|
||||
private val namespaces = new ConcurrentHashMap[String, Path]()
|
||||
|
||||
/** Cached plugin configs: namespace -> Config. */
|
||||
private val pluginConfigs = new ConcurrentHashMap[String, Config]()
|
||||
|
||||
/** Runtime overrides (highest priority). */
|
||||
private val overrides = new ConcurrentHashMap[String, String]()
|
||||
|
||||
// --- String-path access ---
|
||||
|
||||
override def getString(key: String, default: String): String =
|
||||
val ov = overrides.get(key)
|
||||
if ov != null then ov
|
||||
else
|
||||
val cfg = effectiveConfig
|
||||
if cfg.hasPath(key) then cfg.getString(key)
|
||||
else default
|
||||
|
||||
override def getInt(key: String, default: Int): Int =
|
||||
val ov = overrides.get(key)
|
||||
if ov != null then
|
||||
Try(ov.toInt).getOrElse(default)
|
||||
else
|
||||
val cfg = effectiveConfig
|
||||
if cfg.hasPath(key) then cfg.getInt(key)
|
||||
else default
|
||||
|
||||
override def getBoolean(key: String, default: Boolean): Boolean =
|
||||
val ov = overrides.get(key)
|
||||
if ov != null then
|
||||
ov.toLowerCase match
|
||||
case "true" | "yes" | "on" | "1" => true
|
||||
case "false" | "no" | "off" | "0" => false
|
||||
case _ => default
|
||||
else
|
||||
val cfg = effectiveConfig
|
||||
if cfg.hasPath(key) then cfg.getBoolean(key)
|
||||
else default
|
||||
|
||||
override def getOption(key: String): Option[String] =
|
||||
val ov = overrides.get(key)
|
||||
if ov != null then Some(ov)
|
||||
else
|
||||
val cfg = effectiveConfig
|
||||
if cfg.hasPath(key) then Some(cfg.getString(key))
|
||||
else None
|
||||
|
||||
override def has(key: String): Boolean =
|
||||
overrides.containsKey(key) || effectiveConfig.hasPath(key)
|
||||
|
||||
// --- Typed access ---
|
||||
|
||||
override def load[T: ConfigReader](section: String): Either[String, T] =
|
||||
val cfg = effectiveConfig
|
||||
if !cfg.hasPath(section) then
|
||||
Left(s"Config section '$section' not found")
|
||||
else
|
||||
val subConfig = cfg.getConfig(section)
|
||||
read.config[T](subConfig).left.map(_.msg)
|
||||
|
||||
// --- Plugin namespaces ---
|
||||
|
||||
override def addNamespace(namespace: String, path: Path): Unit =
|
||||
namespaces.put(namespace, path)
|
||||
// Invalidate cached plugin config for this namespace
|
||||
pluginConfigs.remove(namespace)
|
||||
|
||||
// --- Runtime overrides ---
|
||||
|
||||
override def set(key: String, value: String): Unit =
|
||||
overrides.put(key, value)
|
||||
|
||||
override def persist(): Either[String, Unit] =
|
||||
Try {
|
||||
val envDir = baseConfigDir.resolve(s"env/$env")
|
||||
Files.createDirectories(envDir)
|
||||
val overridesPath = envDir.resolve("overrides.conf")
|
||||
val sb = new StringBuilder()
|
||||
import scala.jdk.CollectionConverters.*
|
||||
for entry <- overrides.entrySet().asScala do
|
||||
val key = entry.getKey.nn
|
||||
val value = entry.getValue.nn
|
||||
// Write each override as a HOCON key-value pair
|
||||
sb.append(s"""$key = "${escapeHocon(value)}"\n""")
|
||||
Files.writeString(overridesPath, sb.toString)
|
||||
()
|
||||
}.toEither.left.map(e => s"Failed to persist overrides: ${e.getMessage}")
|
||||
|
||||
// --- Environment ---
|
||||
|
||||
override def environment: String = env
|
||||
override def configPath: Path = baseConfigDir
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
override def reload(): Unit =
|
||||
overrides.clear()
|
||||
pluginConfigs.clear()
|
||||
mergedConfig = loadMergedConfig()
|
||||
|
||||
// --- Internal ---
|
||||
|
||||
/**
|
||||
* Build the effective config: plugin configs merged on top of base merged config.
|
||||
* Plugin registration happens at boot time, so this is not a hot path concern.
|
||||
*/
|
||||
private def effectiveConfig: Config =
|
||||
if namespaces.isEmpty then mergedConfig
|
||||
else
|
||||
import scala.jdk.CollectionConverters.*
|
||||
namespaces.entrySet().asScala.foldLeft(mergedConfig) { (acc, entry) =>
|
||||
val ns = entry.getKey.nn
|
||||
val path = entry.getValue.nn
|
||||
val pluginCfg = pluginConfigs.computeIfAbsent(ns, _ => {
|
||||
ConfigDiscovery.loadPluginConfig(path).atPath(ns)
|
||||
})
|
||||
pluginCfg.withFallback(acc)
|
||||
}
|
||||
|
||||
/** Load all base config + env overlay + persisted overrides into one Config. */
|
||||
private def loadMergedConfig(): Config =
|
||||
val base = ConfigDiscovery.loadAll(baseConfigDir, env)
|
||||
// Also load persisted overrides.conf if it exists
|
||||
val overridesPath = baseConfigDir.resolve(s"env/$env/overrides.conf")
|
||||
val persistedOverrides =
|
||||
if Files.exists(overridesPath) then ConfigFactory.parseFile(overridesPath.toFile)
|
||||
else ConfigFactory.empty()
|
||||
// Persisted overrides take priority over base+env
|
||||
persistedOverrides.withFallback(base)
|
||||
|
||||
/** Escape a string value for HOCON output. */
|
||||
private def escapeHocon(value: String): String =
|
||||
value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
3
src/test/resources/config/app.conf
Normal file
3
src/test/resources/config/app.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
name = "SummerCMS"
|
||||
debug = true
|
||||
url = "http://localhost"
|
||||
3
src/test/resources/config/database.conf
Normal file
3
src/test/resources/config/database.conf
Normal file
@@ -0,0 +1,3 @@
|
||||
host = "localhost"
|
||||
port = 5432
|
||||
name = "summer_dev"
|
||||
1
src/test/resources/config/env/dev/database.conf
vendored
Normal file
1
src/test/resources/config/env/dev/database.conf
vendored
Normal file
@@ -0,0 +1 @@
|
||||
host = "dev-db.local"
|
||||
1
src/test/resources/config/env/production/app.conf
vendored
Normal file
1
src/test/resources/config/env/production/app.conf
vendored
Normal file
@@ -0,0 +1 @@
|
||||
debug = false
|
||||
2
src/test/resources/plugins/acme.blog/config.conf
Normal file
2
src/test/resources/plugins/acme.blog/config.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
postsPerPage = 10
|
||||
showAuthor = true
|
||||
229
src/test/scala/summer/compass/CompassSpec.scala
Normal file
229
src/test/scala/summer/compass/CompassSpec.scala
Normal file
@@ -0,0 +1,229 @@
|
||||
package summer.compass
|
||||
|
||||
import machinespir.it.jig.ConfigReader
|
||||
import java.nio.file.{Files, Path}
|
||||
|
||||
class CompassSpec extends munit.FunSuite:
|
||||
|
||||
/** Resolve test resources/config directory from classpath. */
|
||||
val testConfigDir: Path =
|
||||
Path.of(getClass.getClassLoader.getResource("config/app.conf").toURI).getParent
|
||||
|
||||
val testPluginsDir: Path =
|
||||
Path.of(getClass.getClassLoader.getResource("plugins/acme.blog/config.conf").toURI).getParent
|
||||
|
||||
// --- Basic string-path access ---
|
||||
|
||||
test("getString returns value from base config") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getString("app.name"), "SummerCMS")
|
||||
}
|
||||
|
||||
test("getString returns default when key missing") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getString("app.nonexistent", "fallback"), "fallback")
|
||||
}
|
||||
|
||||
test("getInt returns integer value") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getInt("database.port"), 5432)
|
||||
}
|
||||
|
||||
test("getInt returns default when key missing") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getInt("database.missing", 3306), 3306)
|
||||
}
|
||||
|
||||
test("getBoolean returns boolean value") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getBoolean("app.debug"), true)
|
||||
}
|
||||
|
||||
test("getBoolean returns default when key missing") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getBoolean("app.missing", false), false)
|
||||
}
|
||||
|
||||
test("getOption returns Some for existing key") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getOption("app.url"), Some("http://localhost"))
|
||||
}
|
||||
|
||||
test("getOption returns None for missing key") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getOption("app.nonexistent"), None)
|
||||
}
|
||||
|
||||
test("has returns true for existing key") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assert(compass.has("app.name"))
|
||||
}
|
||||
|
||||
test("has returns false for missing key") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assert(!compass.has("app.nonexistent"))
|
||||
}
|
||||
|
||||
// --- Environment layering ---
|
||||
|
||||
test("dev environment overrides database host") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getString("database.host"), "dev-db.local")
|
||||
// Non-overridden values remain from base
|
||||
assertEquals(compass.getInt("database.port"), 5432)
|
||||
}
|
||||
|
||||
test("production environment overrides app debug") {
|
||||
val compass = SummerCompass(testConfigDir, env = "production")
|
||||
assertEquals(compass.getBoolean("app.debug"), false)
|
||||
// Non-overridden values remain from base
|
||||
assertEquals(compass.getString("app.name"), "SummerCMS")
|
||||
}
|
||||
|
||||
test("production database stays at base values") {
|
||||
val compass = SummerCompass(testConfigDir, env = "production")
|
||||
assertEquals(compass.getString("database.host"), "localhost")
|
||||
}
|
||||
|
||||
test("environment accessor returns configured env") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.environment, "dev")
|
||||
}
|
||||
|
||||
test("configPath accessor returns base config dir") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.configPath, testConfigDir)
|
||||
}
|
||||
|
||||
// --- Plugin namespaces ---
|
||||
|
||||
test("addNamespace makes plugin config accessible via dot-path") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.addNamespace("acme.blog", testPluginsDir)
|
||||
assertEquals(compass.getInt("acme.blog.postsPerPage"), 10)
|
||||
assertEquals(compass.getBoolean("acme.blog.showAuthor"), true)
|
||||
}
|
||||
|
||||
test("plugin namespace does not interfere with base config") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.addNamespace("acme.blog", testPluginsDir)
|
||||
// Base config still works
|
||||
assertEquals(compass.getString("app.name"), "SummerCMS")
|
||||
}
|
||||
|
||||
// --- Runtime overrides ---
|
||||
|
||||
test("set overrides file config") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assertEquals(compass.getString("app.name"), "SummerCMS")
|
||||
compass.set("app.name", "OverriddenCMS")
|
||||
assertEquals(compass.getString("app.name"), "OverriddenCMS")
|
||||
}
|
||||
|
||||
test("set override shows in has") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
assert(!compass.has("custom.new.key"))
|
||||
compass.set("custom.new.key", "value")
|
||||
assert(compass.has("custom.new.key"))
|
||||
}
|
||||
|
||||
test("set override works with getOption") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.set("app.name", "Overridden")
|
||||
assertEquals(compass.getOption("app.name"), Some("Overridden"))
|
||||
}
|
||||
|
||||
test("set override for int values") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.set("database.port", "9999")
|
||||
assertEquals(compass.getInt("database.port"), 9999)
|
||||
}
|
||||
|
||||
test("set override for boolean values") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.set("app.debug", "false")
|
||||
assertEquals(compass.getBoolean("app.debug"), false)
|
||||
}
|
||||
|
||||
// --- Typed access ---
|
||||
|
||||
case class DatabaseConfig(host: String, port: Int, name: String) derives ConfigReader
|
||||
|
||||
test("load typed config from section") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
val result = compass.load[DatabaseConfig]("database")
|
||||
assert(result.isRight, s"Expected Right, got $result")
|
||||
val db = result.toOption.get
|
||||
assertEquals(db.host, "dev-db.local")
|
||||
assertEquals(db.port, 5432)
|
||||
assertEquals(db.name, "summer_dev")
|
||||
}
|
||||
|
||||
test("load returns Left for missing section") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
val result = compass.load[DatabaseConfig]("nonexistent")
|
||||
assert(result.isLeft)
|
||||
}
|
||||
|
||||
// --- Persist and reload ---
|
||||
|
||||
test("persist writes overrides to disk and reload reads them back") {
|
||||
// Use a temp directory to avoid polluting test resources
|
||||
val tempDir = Files.createTempDirectory("compass-test")
|
||||
val configDir = tempDir.resolve("config")
|
||||
Files.createDirectories(configDir)
|
||||
Files.writeString(configDir.resolve("app.conf"), """name = "TestApp"""")
|
||||
|
||||
val compass = SummerCompass(configDir, env = "test")
|
||||
assertEquals(compass.getString("app.name"), "TestApp")
|
||||
|
||||
// Set an override and persist
|
||||
compass.set("app.name", "PersistApp")
|
||||
assertEquals(compass.getString("app.name"), "PersistApp")
|
||||
val persistResult = compass.persist()
|
||||
assert(persistResult.isRight, s"Persist failed: $persistResult")
|
||||
|
||||
// Verify the overrides file was written
|
||||
val overridesPath = configDir.resolve("env/test/overrides.conf")
|
||||
assert(Files.exists(overridesPath))
|
||||
|
||||
// Reload clears in-memory overrides but persisted file is loaded
|
||||
compass.reload()
|
||||
assertEquals(compass.getString("app.name"), "PersistApp")
|
||||
|
||||
// Cleanup
|
||||
deleteRecursive(tempDir)
|
||||
}
|
||||
|
||||
test("reload clears runtime overrides") {
|
||||
val compass = SummerCompass(testConfigDir, env = "dev")
|
||||
compass.set("app.name", "Temporary")
|
||||
assertEquals(compass.getString("app.name"), "Temporary")
|
||||
compass.reload()
|
||||
assertEquals(compass.getString("app.name"), "SummerCMS")
|
||||
}
|
||||
|
||||
// --- ConfigDiscovery ---
|
||||
|
||||
test("ConfigDiscovery.detectEnvironment defaults to production") {
|
||||
// Only reliable if SUMMER_ENV is not set in CI
|
||||
// This test just verifies the method doesn't throw
|
||||
val env = ConfigDiscovery.detectEnvironment()
|
||||
assert(env.nonEmpty)
|
||||
}
|
||||
|
||||
test("ConfigDiscovery.discoverSections finds conf files") {
|
||||
val sections = ConfigDiscovery.discoverSections(testConfigDir)
|
||||
assert(sections.contains("app"))
|
||||
assert(sections.contains("database"))
|
||||
assert(!sections.contains("env"))
|
||||
}
|
||||
|
||||
// --- Helper ---
|
||||
|
||||
private def deleteRecursive(path: Path): Unit =
|
||||
if Files.isDirectory(path) then
|
||||
val stream = Files.list(path)
|
||||
try stream.forEach(p => deleteRecursive(p))
|
||||
finally stream.close()
|
||||
Files.deleteIfExists(path)
|
||||
Reference in New Issue
Block a user