Files
barkfile-parser/watch.go
Shaun Campbell c7654ea72a
Some checks failed
Test and Release Module / test (push) Failing after 8s
Test and Release Module / release (push) Has been skipped
feat: centralize Barkfile schema
2026-09-15 19:44:54 -04:00

113 lines
2.9 KiB
Go

package barkfile
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"os"
"time"
)
const defaultPollInterval = 250 * time.Millisecond
// Change is one validated Barkfile revision or an error reading or validating it.
// Changes are delivered in source order and include the initial valid revision.
type Change struct {
Config Config
Err error
}
// WatchOptions configures file watching. A zero PollInterval uses 250ms.
type WatchOptions struct {
PollInterval time.Duration
}
// Load reads, parses, and validates a Barkfile.
func Load(path string) (Config, error) {
contents, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("read Barkfile: %w", err)
}
return parseAndValidate(contents)
}
// Watch emits the initial valid Barkfile and each subsequent file-content change.
// An invalid or unreadable revision is delivered as Change.Err; watching continues
// so a later valid revision is delivered. Cancel ctx to close the returned channel.
func Watch(ctx context.Context, path string, options WatchOptions) (<-chan Change, error) {
if ctx == nil {
return nil, errors.New("watch context is required")
}
interval := options.PollInterval
if interval == 0 {
interval = defaultPollInterval
}
if interval < 0 {
return nil, errors.New("watch poll interval cannot be negative")
}
initial, fingerprint, err := loadRevision(path)
if err != nil {
return nil, err
}
changes := make(chan Change)
go watchLoop(ctx, path, interval, initial, fingerprint, changes)
return changes, nil
}
func watchLoop(ctx context.Context, path string, interval time.Duration, initial Config, fingerprint [sha256.Size]byte, changes chan<- Change) {
defer close(changes)
if !sendChange(ctx, changes, Change{Config: initial}) {
return
}
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
config, nextFingerprint, err := loadRevision(path)
if nextFingerprint == fingerprint {
continue
}
fingerprint = nextFingerprint
if !sendChange(ctx, changes, Change{Config: config, Err: err}) {
return
}
}
}
}
func sendChange(ctx context.Context, changes chan<- Change, change Change) bool {
select {
case changes <- change:
return true
case <-ctx.Done():
return false
}
}
func loadRevision(path string) (Config, [sha256.Size]byte, error) {
contents, err := os.ReadFile(path)
if err != nil {
return Config{}, sha256.Sum256([]byte("read error:\x00" + err.Error())), fmt.Errorf("read Barkfile: %w", err)
}
fingerprint := sha256.Sum256(contents)
config, err := parseAndValidate(contents)
return config, fingerprint, err
}
func parseAndValidate(contents []byte) (Config, error) {
config, err := Parse(contents)
if err != nil {
return Config{}, err
}
if err := config.Validate(); err != nil {
return Config{}, fmt.Errorf("invalid Barkfile: %w", err)
}
return config, nil
}