# summer-compass Configuration management module for [SummerCMS](https://git.golem15.com/golem15/summercms). Handles config file discovery, environment layering, plugin namespacing, and runtime overrides — the Scala 3 equivalent of Laravel's `Illuminate\Config`. Compass is **not** a config parser. [Jig](https://github.com/lbialy/jig) handles HOCON parsing and type-safe codec derivation. Compass is the CMS orchestration layer on top: where do config files live, how do environments overlay, how do plugins register their config. ## Quick start ```scala import summer.compass.* import java.nio.file.Path val config = SummerCompass( baseConfigDir = Path.of("config"), env = "production", // or omit to auto-detect from SUMMER_ENV ) // String-path access (dot-notation) config.getString("app.name") // => "SummerCMS" config.getInt("database.port") // => 5432 config.getBoolean("app.debug") // => false (production override) config.getOption("app.url") // => Some("http://localhost") config.has("database.host") // => true // Register a plugin's config directory config.addNamespace("acme.blog", Path.of("plugins/acme/blog/config")) config.getInt("acme.blog.postsPerPage") // => 10 config.getBoolean("acme.blog.showAuthor") // => true // Runtime overrides (highest priority, in-memory) config.set("app.name", "MyApp") config.getString("app.name") // => "MyApp" // Persist overrides to disk config.persist() // writes to config/env/production/overrides.conf // Reload everything from disk (clears runtime overrides) config.reload() ``` ## Typed config access Compass integrates with Jig's `ConfigReader` derivation for type-safe section loading: ```scala import machinespir.it.jig.ConfigReader case class DatabaseConfig( host: String, port: Int, name: String, ) derives ConfigReader val db = config.load[DatabaseConfig]("database") // => Right(DatabaseConfig("db.example.com", 5432, "summer_prod")) ``` Returns `Either[String, T]` — no exceptions thrown. ## Config file layout Config files use [HOCON](https://github.com/lightbend/config/blob/main/HOCON.md) (`.conf`), organized by section with environment overlays: ``` config/ app.conf # base app config database.conf # base database config env/ dev/ database.conf # dev overrides for database production/ app.conf # production overrides for app overrides.conf # persisted runtime overrides ``` Each file becomes a config section namespaced by its filename. `database.conf` with `host = "localhost"` is accessed as `database.host`. ## Environment layering Given `SUMMER_ENV=dev`, Compass merges configs with this priority (highest wins): ``` 1. Runtime set() overrides (in-memory) 2. config/env/dev/overrides.conf (persisted overrides) 3. config/env/dev/database.conf (environment overlay) 4. config/database.conf (base config) ``` Example: ```hocon # config/database.conf (base) host = "localhost" port = 5432 # config/env/dev/database.conf (env overlay) host = "dev-db.local" ``` Result: ``` database.host = "dev-db.local" # overridden by env/dev database.port = 5432 # from base ``` Merging uses sconfig's `withFallback` — nested HOCON objects merge correctly, not just top-level keys. ## Environment detection Compass reads the `SUMMER_ENV` environment variable. Defaults to `"production"` if unset or empty. Can also be passed explicitly: ```scala SummerCompass(configDir, env = "staging") ``` ## Plugin namespaces Plugins register their config directory at boot time. All `.conf` files in that directory are loaded and namespaced under the plugin identifier: ```scala config.addNamespace("acme.blog", Path.of("plugins/acme/blog/config")) // plugins/acme/blog/config/config.conf contains: postsPerPage = 10 config.getInt("acme.blog.postsPerPage") // => 10 ``` Plugin configs are cached after first access. Calling `addNamespace` again for the same namespace invalidates the cache. ## Runtime overrides `set()` creates in-memory overrides with the highest priority — they win over any file-based config: ```scala config.set("database.host", "override.local") config.getString("database.host") // => "override.local" ``` `persist()` writes all current overrides to `config/env/{env}/overrides.conf`. These persist across restarts (loaded during boot as part of the merged config). `reload()` clears all runtime overrides and re-reads everything from disk. ## API reference ```scala trait Compass: // String-path access 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 def load[T: ConfigReader](section: String): Either[String, T] // Plugin registration def addNamespace(namespace: String, path: java.nio.file.Path): Unit // Runtime overrides def set(key: String, value: String): Unit def persist(): Either[String, Unit] // Environment def environment: String def configPath: java.nio.file.Path // Lifecycle def reload(): Unit ``` ## Module structure ``` src/main/scala/summer/compass/ Compass.scala Public API trait ConfigDiscovery.scala Stateless file scanning, env detection, HOCON merging SummerCompass.scala Thread-safe implementation (ConcurrentHashMap + @volatile) ``` ## Thread safety `SummerCompass` is thread-safe: - `ConcurrentHashMap` for plugin namespaces, plugin config cache, and runtime overrides - `@volatile` for the merged config reference (swapped atomically on `reload()`) - `computeIfAbsent` for lazy plugin config loading ## Dependencies | Dependency | Version | Purpose | |-----------|---------|---------| | `ma.chinespirit:jig` | 0.1.0 | HOCON parsing (via sconfig) + type-safe ConfigReader derivation | | `org.scalameta:munit` | 1.0.3 | Testing (test scope only) | No effect systems. No Cats, no ZIO. Pure direct-style Scala 3 on JDK 21. ## Building ```bash sbt compile # compile sbt test # run all 28 tests ``` Requires JDK 21+ and sbt 1.10+. ## Roadmap - **Phase 2** — Integration: wire into SummerCMS boot sequence, auto-discover plugin configs during plugin registration - **Phase 3** — Admin UI: config editor in admin backend, schema validation against typed config classes - **Phase 4** — `summer compass:cache` command to compile all config into a single cached file for production ## License Part of SummerCMS.