Skip to content

OXY-143: Add ConfigFileService for runtime config-file management - #295

Open
Kalin-Rudnicki wants to merge 2 commits into
mainfrom
OXY-143
Open

OXY-143: Add ConfigFileService for runtime config-file management#295
Kalin-Rudnicki wants to merge 2 commits into
mainfrom
OXY-143

Conversation

@Kalin-Rudnicki

Copy link
Copy Markdown
Owner

OXY-143 — Create a service for managing config files

Adds a reusable, injectable ZIO service that centralizes the config-file lifecycle, and unifies it with the existing @envConfig startup path.

What was done

  • ConfigFileService (oxygen.executable.config) — trait + live/default/test ZLayers + accessors + Live:
    • load[A] / loadJson, save[A] (atomic: serialize-by-extension → temp sibling → moveTo, auto-creates parent dirs)
    • mergeDirectory[A] / mergeDirectoryJson (reduceLeft(_ ++ _), sorted by path for deterministic precedence)
    • loadResolved[A] (file→load, dir→merge — the @envConfig semantics), list, exists
  • ConfigFileError typed ADT (wraps FileSystemError + JSON/YAML decode failures; mirrors TestContainerError/MigrationError).
  • JSON + YAML handling (.json/.yaml/.yml) via oxygen-json / YamlParser/YamlWriter; respects FileSystem.current.
  • Refactored ConfigLoader (@envConfig) to delegate to ConfigFileService.Live.loadResolved, so startup and runtime cannot diverge.
  • Docs: docs/docs/executable/config-files.md contrasting @envConfig (startup) vs the service (runtime); added to nav.

Placement decision

Lives in oxygen-executable (the ticket's documented alternative), not oxygen-zio: the service needs YamlParser, and oxygen-zio does not depend on oxygen-yaml. oxygen-executable already depends on both and owns ConfigLoader.

Verification

  • oxygen-executableJVM/test: 43 passed / 0 failed (14 new + existing @envConfig tests).
  • oxygen-executableJS/compile + oxygen-executableNative/compile: success.

Notes

  • Directory merge now sorts by path before folding (old code was nondeterministic) — deliberate, safer.
  • Full assumptions/decisions + confidence score (8.5/10) in report/OXY-143.md.

🤖 Generated with Claude Code

Introduce a reusable, injectable ZIO service (`ConfigFileService`) that centralizes
the config-file lifecycle: load / merge / list / exists / atomic save for JSON + YAML
files, with a typed `ConfigFileError` ADT.

Refactor the private `ConfigLoader` (backing `@envConfig`) to delegate to
`ConfigFileService.Live.loadResolved`, so the startup and runtime paths share a single
implementation (same file/dir resolution, JSON/YAML handling, and directory-merge order).

- New `oxygen.executable.config.{ConfigFileService, ConfigFileError}` with live/default/test layers.
- Atomic save via temp-sibling + move; parent dirs auto-created.
- Directory merge sorts by path then reduceLeft(_ ++ _) (deterministic precedence).
- 14 unit tests (round-trip, directory-merge, missing-path, errors); JVM/JS/Native compile.
- Docs: executable/config-files.md contrasting @envconfig (startup) vs the service (runtime).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Kalin-Rudnicki

Copy link
Copy Markdown
Owner Author

🤖 Automated multi-agent review (confirmed findings)

High

save() cannot overwrite an existing config file — moveTo has no REPLACE_EXISTING
modules/general/executable/src/main/scala/oxygen/executable/config/ConfigFileService.scala:156
save() writes to a temp sibling then calls tmp.moveTo(file), but JavaPath.moveTo invokes Files.move without StandardCopyOption.REPLACE_EXISTING, so a second save() to an already-existing path throws FileAlreadyExistsException (wrapped as ConfigFileError.FileSystem), breaking the service's core purpose of updating an existing config. Tests pass only because each saves to a fresh temp path.


These are AI-generated, adversarially-verified findings and may still contain false positives.

save() moved a temp sibling into place via JavaPath.moveTo, which called
Files.move without REPLACE_EXISTING and therefore threw
FileAlreadyExistsException when the destination already existed (the
service's core update-an-existing-config case). Add an overwrite-capable
moveTo(dest, replaceExisting) overload (default false preserves existing
callers) and use replaceExisting = true in save(). Add a unit test proving
save() overwrites an existing file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011YxWKdsz97QT9BD7AdpSq6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant