Documentation
¶
Overview ¶
Package storage provides a generic layered YAML store engine.
Both internal/config and internal/project compose a Store[T] with their own schema types. The store handles file discovery (static paths or walk-up), per-file loading with migrations, N-way merge with provenance tracking, and scoped writes with atomic I/O.
Index ¶
- Variables
- func ResolveProjectRoot() (string, error)
- func ValidateDirectories() error
- type LayerInfo
- type Migration
- type Option
- func WithCacheDir() Option
- func WithConfigDir() Option
- func WithDataDir() Option
- func WithDefaults(yaml string) Option
- func WithDirs(dirs ...string) Option
- func WithFilenames(names ...string) Option
- func WithLock() Option
- func WithMigrations(fns ...Migration) Option
- func WithPaths(dirs ...string) Option
- func WithStateDir() Option
- func WithWalkUp() Option
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrNotInProject = errors.New("storage: CWD is not within a registered project")
ErrNotInProject is returned when CWD is not within a registered project's directory tree. Walk-up discovery falls back to home-level configs only.
var ErrRegistryNotFound = errors.New("storage: project registry not found")
ErrRegistryNotFound is returned when the project registry file cannot be located during walk-up discovery. Discovery continues with explicit paths.
Functions ¶
func ResolveProjectRoot ¶
ResolveProjectRoot resolves the project root for the current working directory by reading the project registry and finding the deepest registered root that contains CWD. Returns ErrRegistryNotFound if the registry does not exist, and ErrNotInProject if CWD is not within any registered project.
func ValidateDirectories ¶ added in v0.3.2
func ValidateDirectories() error
ValidateDirectories resolves all four XDG-style directories and returns an error if any two resolve to the same path. This catches misconfiguration (e.g. CLAWKER_DATA_DIR accidentally pointing at the config directory).
Types ¶
type LayerInfo ¶
type LayerInfo struct {
Filename string // which filename matched (e.g., "clawker.yaml")
Path string // resolved absolute path
}
LayerInfo describes a discovered configuration layer.
type Migration ¶
Migration is a caller-provided function that inspects a raw YAML map and optionally transforms it. Returns true if the map was modified (triggers an atomic re-save of the source file).
type Option ¶
type Option func(*options)
Option configures store construction via NewStore.
func WithCacheDir ¶
func WithCacheDir() Option
WithCacheDir adds the resolved cache directory to the explicit path list. Resolution: CLAWKER_CACHE_DIR > XDG_CACHE_HOME > ~/.cache/clawker
func WithConfigDir ¶
func WithConfigDir() Option
WithConfigDir adds the resolved config directory to the explicit path list. Resolution: CLAWKER_CONFIG_DIR > XDG_CONFIG_HOME > ~/.config/clawker
func WithDataDir ¶
func WithDataDir() Option
WithDataDir adds the resolved data directory to the explicit path list. Resolution: CLAWKER_DATA_DIR > XDG_DATA_HOME > ~/.local/share/clawker
func WithDefaults ¶
WithDefaults provides a YAML string as the lowest-priority base layer. The string is parsed and merged before any discovered files. The same constant can be used for scaffolding (clawker init) and defaults.
func WithDirs ¶ added in v0.2.4
WithDirs adds directories to be probed with dual placement discovery. Each directory uses the same dual-placement logic as walk-up: if a .clawker/ subdirectory exists, it probes .clawker/{filename} (dir form); otherwise it probes .{filename} (flat dotfile form). Both .yaml and .yml extensions are accepted. No registry required. Directories are probed in the order given (first = highest priority). Priority: walk-up > dirs > explicit paths (WithPaths/WithConfigDir/etc.).
func WithFilenames ¶
WithFilenames sets the ordered list of filenames to discover. All filenames must share the same schema type T. At each walk-up level the first filename in the list takes merge precedence when discovered at the same depth.
func WithLock ¶
func WithLock() Option
WithLock enables flock-based advisory locking for Write operations. Use for stores that need cross-process mutual exclusion (e.g. registry).
func WithMigrations ¶
WithMigrations registers precondition-based migration functions. Each migration runs independently on every discovered file's raw map. Migrations that return true trigger an atomic re-save of that file.
func WithPaths ¶
WithPaths adds explicit directories to the discovery path list. Files are probed as {dir}/{filename} for each configured filename.
func WithStateDir ¶
func WithStateDir() Option
WithStateDir adds the resolved state directory to the explicit path list. Resolution: CLAWKER_STATE_DIR > XDG_STATE_HOME > ~/.local/state/clawker
func WithWalkUp ¶
func WithWalkUp() Option
WithWalkUp enables bounded walk-up discovery from CWD to the registered project root. The store resolves both CWD and project root internally: CWD via os.Getwd(), project root by reading the registry at dataDir(). At each level the store checks for .clawker/{filename} (dir form) first, then .{filename} (flat dotfile form). Walk-up never proceeds past the project root. If CWD is not within a registered project, walk-up is skipped and discovery falls back to explicit paths only.
type Store ¶
type Store[T any] struct { // contains filtered or unexported fields }
Store is a generic layered YAML store engine. Both internal/config and internal/project compose a Store[T] with their own schema types. The store handles file discovery, per-file loading with migrations, N-way merge with provenance, and scoped writes.
Internally, the store maintains a node tree (map[string]any) as the merge engine and persistence layer. The typed struct T is deserialized from the merged tree and published as an immutable snapshot via atomic.Pointer. Readers get the current snapshot lock-free; writers deep-copy, mutate the copy, sync the tree, and atomically swap.
Load: file → node tree → merge → deserialize → immutable snapshot Set: deep copy → mutate copy → serialize into tree → atomic swap Write: node tree → file
func NewFromString ¶
NewFromString creates a store from a YAML string without any filesystem discovery, migration, or layering. The parsed string becomes the sole node tree, deserialized into a typed value. Useful for building test doubles. Write is a no-op (no paths configured).
func NewStore ¶
NewStore constructs a store by discovering files, loading each as a raw map, merging all maps into a single tree, and deserializing the merged tree into a typed value.
Discovery modes are additive: walk-up files (if enabled) come first (highest priority), followed by explicit path files (lowest priority). If walk-up fails (not in a project, registry missing), discovery falls back to explicit paths only — this is not an error.
The resulting store is immediately usable via Read/Set/Write.
func (*Store[T]) Layers ¶
Layers returns information about the discovered configuration layers. Layers are ordered from highest priority (index 0) to lowest. No lock needed — layers are immutable after construction.
func (*Store[T]) Read ¶ added in v0.3.2
func (s *Store[T]) Read() *T
Read returns the current immutable snapshot. The returned pointer is safe to hold, inspect, and pass around — it will never be mutated by the store. Set publishes new snapshots via atomic swap; existing readers are unaffected.
Lock-free: uses atomic.Pointer.Load.
func (*Store[T]) Set ¶
Set applies a mutation function to a deep copy of the current value, syncs the change into the node tree, and atomically publishes the new snapshot. Changes are not persisted until Write is called.
The copy-on-write approach means existing Read callers holding the old snapshot are unaffected — they see consistent (stale) data.
After fn runs, the mutated copy is serialized back into the tree using structToMap (which ignores omitempty tags). This ensures that explicit zero-value assignments (e.g. setting a bool to false) are captured in the tree for persistence.
func (*Store[T]) Write ¶
Write persists the current tree to disk.
Without arguments, each top-level field is routed to the layer it originated from (via provenance). Fields without provenance (e.g. from defaults or newly added by Set) route to the highest-priority layer.
With a filename argument, all fields are written to the first layer matching that filename. This supports explicit layer targeting for scenarios like a settings TUI where the user picks the save destination.
Write sequence per target: read existing file → merge fields → atomic write (temp+rename). If locking is enabled (WithLock), each file write is wrapped in a cross-process flock.
After a successful write, dirty tracking is cleared.