README.md
This commit is contained in:
215
README.md
Normal file
215
README.md
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
# 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.
|
||||||
Reference in New Issue
Block a user