Compare commits

..

1 Commits

Author SHA1 Message Date
Jakub Zych
112fb7f1d4 README.md 2026-02-23 23:29:06 +01:00

215
README.md Normal file
View 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.