Compare commits
1 Commits
067b8fd30e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
112fb7f1d4 |
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