feat: centralize Barkfile schema
This commit is contained in:
112
watch.go
Normal file
112
watch.go
Normal file
@@ -0,0 +1,112 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user