Jakub Zych 112fb7f1d4 README.md
2026-02-23 23:29:06 +01:00
2026-02-23 23:22:37 +01:00
2026-02-23 23:22:37 +01:00
2026-02-23 23:22:37 +01:00
2026-02-23 23:22:37 +01:00
2026-02-23 23:29:06 +01:00

summer-compass

Configuration management module for 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 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

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:

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 (.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:

# 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:

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:

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:

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

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

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 4summer compass:cache command to compile all config into a single cached file for production

License

Part of SummerCMS.

Description
No description provided
Readme 37 KiB
Languages
Scala 100%