Files
summercms/modules/compass/README.md
Jakub Zych 5fb22c28d0 fix(11-04): keep earlier overrides when compass Persist saves
Persist rewrote overrides.yaml with only this process's runtime values,
so saving one key (for example websockets:generate-vapid-keys --update)
dropped every key persisted earlier. It now starts from the saved file
and lets runtime values win.
2026-09-30 13:45:13 +02:00

113 lines
5.6 KiB
Markdown

# compass
Layered YAML configuration with per-environment directories, `SUMMER_` environment overrides, embedded plugin defaults and dot-path access.
`import "git.golem15.com/golem15/summercms/modules/compass"`
## Overview
`compass` is the SummerCMS counterpart of WinterCMS's `config/*.php` files, per-environment config directories, `.env` support and `Config::get('app.name')`. It merges several layers into one tree built on koanf, and every key is read with a dot path such as `app.name`. Plugin defaults live under the bare plugin ID (`acme.blog.posts_per_page`) instead of WinterCMS's `acme.blog::posts_per_page`. The generated `main` of an application calls `compass.Load("config")` and hands the result to [backpack](../backpack/README.md); [party](../party/README.md) merges plugin defaults into it during activation.
## Features
- A fixed layer order, lowest to highest precedence:
1. Plugin defaults added with `compass.Config.MergePlugin`: a plugin's `config.yaml` becomes `<plugin id>.<key>`, any other `<name>.yaml` becomes `<plugin id>.<name>.<key>`.
2. `<dir>/*.yaml` (and `*.yml`): each file is a section named after the file, so `config/app.yaml` provides `app.*`. Files load in sorted order.
3. `<dir>/env/<environment>/*.yaml`: per-environment sections with the same naming.
4. `SUMMER_` environment variables, including values from a `.env` file (see Configuration).
5. `<dir>/env/<environment>/overrides.yaml`, the file written by `compass.Config.Persist`.
6. In-memory values stored with `compass.Config.Set`.
- Typed getters with zero-value defaults: `compass.Config.String`, `compass.Config.Int`, `compass.Config.Bool`, plus `compass.Config.Lookup` and `compass.Config.Has` to tell a missing key from a zero value.
- `compass.Config.LoadSection` unmarshals a whole subtree into a struct using `koanf` struct tags.
- Runtime overrides: `compass.Config.Set` changes a value in memory, `compass.Config.Persist` saves all runtime values atomically to the environment's `overrides.yaml`, keeping the keys already saved there and replacing the saved value of any key set at runtime (directory mode 0700, file mode 0600, refusing any path outside the config directory), and `compass.Config.Reload` rereads every source and discards unsaved runtime values.
- `compass.Config.Environment` reports the active environment name, which must consist of letters, digits, `-` and `_`.
- Safe for concurrent reads and writes.
## Usage
```go
package blog
import (
"git.golem15.com/golem15/summercms/modules/compass"
)
type mailSettings struct {
Host string `koanf:"host"`
Port int `koanf:"port"`
}
func loadConfig() (*compass.Config, error) {
cfg, err := compass.Open(compass.Options{Dir: "config", Env: "development"})
if err != nil {
return nil, err
}
name := cfg.String("app.name")
perPage := cfg.Int("acme.blog.posts_per_page")
_, _ = name, perPage
var mail mailSettings
if err := cfg.LoadSection("mail", &mail); err != nil {
return nil, err
}
// Store a runtime override and write it to config/env/development/overrides.yaml.
if err := cfg.Set("acme.blog.posts_per_page", 25); err != nil {
return nil, err
}
return cfg, cfg.Persist()
}
```
A matching configuration directory:
```yaml
# config/app.yaml
name: Acme
url: http://localhost:8080
# config/env/development/app.yaml
debug: true
```
## API reference
| Identifier | Description |
|------------|-------------|
| `compass.Config` | The merged configuration tree with dot-path access. |
| `compass.Load` | Opens a config directory, taking the environment from `SUMMER_ENV`. |
| `compass.Open` | Opens configuration with explicit `compass.Options`. |
| `compass.Options` | Config directory, environment name and the environment variable list to read (defaults to the process environment). |
| `compass.Config.String` / `compass.Config.Int` / `compass.Config.Bool` | Typed getters that return the zero value for a missing key. |
| `compass.Config.Lookup` / `compass.Config.Has` | Raw value lookup and existence check. |
| `compass.Config.LoadSection` | Unmarshals a subtree into a struct with `koanf` tags. |
| `compass.Config.MergePlugin` | Adds a plugin's embedded default configuration under its plugin ID. |
| `compass.Config.Set` / `compass.Config.Persist` / `compass.Config.Reload` | Runtime overrides, saving them to disk, and rebuilding from disk. |
| `compass.Config.Environment` | Returns the active environment name. |
## Configuration
`compass` reads the process environment (or `compass.Options.Environ` when it is set):
| Variable | Default | Effect |
|----------|---------|--------|
| `SUMMER_ENV` | `production` | Selects the environment directory `config/env/<name>/`. An explicit `compass.Options.Env` wins over it. |
| `SUMMER_<SECTION>__<KEY>` | none | Overrides a config key: the prefix is removed, `__` separates path segments and the name is lower-cased, so `SUMMER_DATABASE__DSN` sets `database.dsn` and `SUMMER_ADMIN__JWT__SECRET` sets `admin.jwt.secret`. |
A `.env` file in the parent directory of the config directory (next to `config/`) supplies `KEY=VALUE` lines, with optional `export` prefixes and quotes, for variables that are not already set in the real environment. It never modifies the process environment.
## Dependencies
- SummerCMS modules: none.
- Third-party: `github.com/knadh/koanf/v2` with its `providers/file`, `providers/env/v2`, `providers/confmap` and `parsers/yaml` packages.
- Standard library: `fmt`, `io/fs`, `os`, `path/filepath`, `sort`, `strings`, `sync`, `unicode`.
## Testing
```sh
go test ./modules/compass/...
```
The tests use temporary directories and explicit environment lists and need no external services.