diff options
| author | Paul Buetow <paul@buetow.org> | 2026-07-05 10:28:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-07-05 10:28:26 +0300 |
| commit | 7fdcb5ed7639508e05c9a3851704cf1de9f4476a (patch) | |
| tree | 816f26ea806f5656469216e3c592d059cb2f7027 /internal | |
| parent | fc0ce6f1918c4603254091d9fe77785996b56411 (diff) | |
make options pattern more generic with dot imports a bit less boilerplate for the user
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/resource/dir/dir.go | 66 | ||||
| -rw-r--r-- | internal/resource/dir/dir_test.go | 3 | ||||
| -rw-r--r-- | internal/resource/dir/source.go | 11 | ||||
| -rw-r--r-- | internal/resource/file/file.go | 65 | ||||
| -rw-r--r-- | internal/resource/file/file_test.go | 2 | ||||
| -rw-r--r-- | internal/resource/link/link.go | 42 | ||||
| -rw-r--r-- | internal/resource/link/link_test.go | 14 | ||||
| -rw-r--r-- | internal/resource/opt/opt.go | 156 |
8 files changed, 240 insertions, 119 deletions
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go index 9c34486..8be4395 100644 --- a/internal/resource/dir/dir.go +++ b/internal/resource/dir/dir.go @@ -8,6 +8,7 @@ import ( "strconv" "codeberg.org/snonux/gonf/internal/resource" + "codeberg.org/snonux/gonf/internal/resource/opt" ) type Dir struct { @@ -21,51 +22,28 @@ type Dir struct { absent bool } -type Option func(*Dir) +// SetSource implements opt.Sourced. +func (d *Dir) SetSource(source string) { d.source = source } -func WithSource(source string) Option { - return func(d *Dir) { - d.source = source - } -} +// SetOwner implements opt.Owner. +func (d *Dir) SetOwner(user string) { d.user = user } -func WithUser(user string) Option { - return func(d *Dir) { - d.user = user - } -} +// SetGroup implements opt.Grouped. +func (d *Dir) SetGroup(group string) { d.group = group } -func WithGroup(group string) Option { - return func(d *Dir) { - d.group = group - } -} +// SetMode implements opt.Moded (the directory's own mode). +func (d *Dir) SetMode(mode os.FileMode) { d.mode = mode } -func WithMode(mode os.FileMode) Option { - return func(d *Dir) { - d.mode = mode - } -} +// SetFileMode implements opt.FileModed (mode for files copied from source). +func (d *Dir) SetFileMode(mode os.FileMode) { d.fileMode = mode } -func WithFileMode(mode os.FileMode) Option { - return func(d *Dir) { - d.fileMode = mode - } -} +// SetPrune implements opt.Prunable. +func (d *Dir) SetPrune() { d.prune = true } -func WithPrune() Option { - return func(d *Dir) { - d.prune = true - } -} - -func IsAbsent() Option { - return func(d *Dir) { - d.absent = true - } -} +// SetAbsent implements opt.Absentable. +func (d *Dir) SetAbsent() { d.absent = true } -func build(path string, opts ...Option) (*Dir, error) { +func build(path string, opts ...opt.Option) (*Dir, error) { curr, err := user.Current() if err != nil { return nil, fmt.Errorf("failed to get current user for default: %w", err) @@ -79,8 +57,8 @@ func build(path string, opts ...Option) (*Dir, error) { group: curr.Gid, } - for _, opt := range opts { - opt(d) + for _, o := range opts { + o(d) } return d, nil @@ -200,7 +178,7 @@ func applyAttributesTo(path string, mode os.FileMode, usr, group string) error { // Ensure builds and applies the directory resource described by opts, // without registering it. -func Ensure(path string, opts ...Option) error { +func Ensure(path string, opts ...opt.Option) error { d, err := build(path, opts...) if err != nil { return err @@ -208,7 +186,7 @@ func Ensure(path string, opts ...Option) error { return d.apply() } -func Have(path string, opts ...Option) resource.Resource { +func Have(path string, opts ...opt.Option) resource.Resource { d, err := build(path, opts...) if err != nil { log.Fatalf("failed to apply directory resource %s: %v", path, err) @@ -223,7 +201,7 @@ func Have(path string, opts ...Option) resource.Resource { return res } -func Absent(path string, opts ...Option) resource.Resource { - opts = append(opts, IsAbsent()) +func Absent(path string, opts ...opt.Option) resource.Resource { + opts = append(opts, opt.IsAbsent()) return Have(path, opts...) } diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go index ca01535..5ffb5cf 100644 --- a/internal/resource/dir/dir_test.go +++ b/internal/resource/dir/dir_test.go @@ -6,6 +6,7 @@ import ( "testing" "codeberg.org/snonux/gonf/internal/resource/file" + . "codeberg.org/snonux/gonf/internal/resource/opt" ) func TestHaveDirectoryCreate(t *testing.T) { @@ -296,7 +297,7 @@ func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) { } singleTarget := filepath.Join(tmp, "single.conf") - if err := file.Ensure(singleTarget, file.WithSource(sourcePath)); err != nil { + if err := file.Ensure(singleTarget, WithSource(sourcePath)); err != nil { t.Fatal(err) } viaFile, err := os.ReadFile(singleTarget) diff --git a/internal/resource/dir/source.go b/internal/resource/dir/source.go index 8f9b73a..2e5ed98 100644 --- a/internal/resource/dir/source.go +++ b/internal/resource/dir/source.go @@ -9,6 +9,7 @@ import ( "codeberg.org/snonux/gonf/internal/resource/file" "codeberg.org/snonux/gonf/internal/resource/link" + "codeberg.org/snonux/gonf/internal/resource/opt" ) // copySourceTree mirrors d.source into d.path, dispatching each entry by @@ -63,7 +64,7 @@ func copySourceSymlink(sourcePath, target string) error { if err != nil { return fmt.Errorf("failed to read symlink %s: %w", sourcePath, err) } - return link.Ensure(target, link.IsSymlink(rawTarget)) + return link.Ensure(target, opt.WithSymlink(rawTarget)) } // copySourceFile delegates writing a single copied file to the file @@ -73,10 +74,10 @@ func copySourceSymlink(sourcePath, target string) error { // dir needs no special-casing of its own. func copySourceFile(d *Dir, sourcePath, target string) error { return file.Ensure(target, - file.WithSource(sourcePath), - file.WithMode(d.fileMode), - file.WithUser(d.user), - file.WithGroup(d.group), + opt.WithSource(sourcePath), + opt.WithMode(d.fileMode), + opt.WithOwner(d.user), + opt.WithGroup(d.group), ) } diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go index 257c407..6395403 100644 --- a/internal/resource/file/file.go +++ b/internal/resource/file/file.go @@ -11,6 +11,7 @@ import ( "text/template" "codeberg.org/snonux/gonf/internal/resource" + "codeberg.org/snonux/gonf/internal/resource/opt" ) type File struct { @@ -23,47 +24,33 @@ type File struct { absent bool } -type Option func(*File) - -func WithContent(content string) Option { - return func(f *File) { - f.content = content - f.source = "" - } +// SetContent implements opt.Contented. Setting literal content clears any +// previously configured source, as the two are mutually exclusive. +func (f *File) SetContent(content string) { + f.content = content + f.source = "" } -func WithSource(source string) Option { - return func(f *File) { - f.source = source - f.content = "" - } +// SetSource implements opt.Sourced. Setting a source clears any previously +// configured literal content, as the two are mutually exclusive. +func (f *File) SetSource(source string) { + f.source = source + f.content = "" } -func WithUser(user string) Option { - return func(f *File) { - f.user = user - } -} +// SetOwner implements opt.Owner. +func (f *File) SetOwner(user string) { f.user = user } -func WithGroup(group string) Option { - return func(f *File) { - f.group = group - } -} +// SetGroup implements opt.Grouped. +func (f *File) SetGroup(group string) { f.group = group } -func WithMode(mode os.FileMode) Option { - return func(f *File) { - f.mode = mode - } -} +// SetMode implements opt.Moded. +func (f *File) SetMode(mode os.FileMode) { f.mode = mode } -func IsAbsent() Option { - return func(f *File) { - f.absent = true - } -} +// SetAbsent implements opt.Absentable. +func (f *File) SetAbsent() { f.absent = true } -func build(path string, opts ...Option) (*File, error) { +func build(path string, opts ...opt.Option) (*File, error) { curr, err := user.Current() if err != nil { return nil, fmt.Errorf("failed to get current user for default: %w", err) @@ -76,8 +63,8 @@ func build(path string, opts ...Option) (*File, error) { group: curr.Gid, } - for _, opt := range opts { - opt(f) + for _, o := range opts { + o(f) } return f, nil @@ -222,7 +209,7 @@ func ensureAbsent(path string) error { // Ensure builds and applies the file resource described by opts, without // registering it. Used by other resource packages (e.g. dir) to write an // individual file without it becoming its own top-level resource. -func Ensure(path string, opts ...Option) error { +func Ensure(path string, opts ...opt.Option) error { f, err := build(path, opts...) if err != nil { return err @@ -230,7 +217,7 @@ func Ensure(path string, opts ...Option) error { return f.apply() } -func Have(path string, opts ...Option) resource.Resource { +func Have(path string, opts ...opt.Option) resource.Resource { f, err := build(path, opts...) if err != nil { log.Fatalf("failed to apply file resource %s: %v", path, err) @@ -245,7 +232,7 @@ func Have(path string, opts ...Option) resource.Resource { return res } -func Absent(path string, opts ...Option) resource.Resource { - opts = append(opts, IsAbsent()) +func Absent(path string, opts ...opt.Option) resource.Resource { + opts = append(opts, opt.IsAbsent()) return Have(path, opts...) } diff --git a/internal/resource/file/file_test.go b/internal/resource/file/file_test.go index b3b7711..49deb4c 100644 --- a/internal/resource/file/file_test.go +++ b/internal/resource/file/file_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "strings" "testing" + + . "codeberg.org/snonux/gonf/internal/resource/opt" ) func TestGetChecksum(t *testing.T) { diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go index 3133514..f9c6c8d 100644 --- a/internal/resource/link/link.go +++ b/internal/resource/link/link.go @@ -6,6 +6,7 @@ import ( "os" "codeberg.org/snonux/gonf/internal/resource" + "codeberg.org/snonux/gonf/internal/resource/opt" ) type kind int @@ -23,32 +24,25 @@ type Link struct { absent bool } -type Option func(*Link) - -func IsSymlink(target string) Option { - return func(l *Link) { - l.kind = symlinkKind - l.target = target - } +// SetSymlink implements opt.Linkable. +func (l *Link) SetSymlink(target string) { + l.kind = symlinkKind + l.target = target } -func IsHardlink(target string) Option { - return func(l *Link) { - l.kind = hardlinkKind - l.target = target - } +// SetHardlink implements opt.Linkable. +func (l *Link) SetHardlink(target string) { + l.kind = hardlinkKind + l.target = target } -func IsAbsent() Option { - return func(l *Link) { - l.absent = true - } -} +// SetAbsent implements opt.Absentable. +func (l *Link) SetAbsent() { l.absent = true } -func build(path string, opts ...Option) *Link { +func build(path string, opts ...opt.Option) *Link { l := &Link{path: path} - for _, opt := range opts { - opt(l) + for _, o := range opts { + o(l) } return l } @@ -87,11 +81,11 @@ func (l *Link) resourceType() string { // Ensure builds and applies the link resource described by opts, without // registering it. Used by other resource packages (e.g. dir) to recreate an // individual symlink without it becoming its own top-level resource. -func Ensure(path string, opts ...Option) error { +func Ensure(path string, opts ...opt.Option) error { return build(path, opts...).apply() } -func Have(path string, opts ...Option) resource.Resource { +func Have(path string, opts ...opt.Option) resource.Resource { l := build(path, opts...) res := resource.Register(l.resourceType(), l.path) @@ -102,8 +96,8 @@ func Have(path string, opts ...Option) resource.Resource { return res } -func Absent(path string, opts ...Option) resource.Resource { - opts = append(opts, IsAbsent()) +func Absent(path string, opts ...opt.Option) resource.Resource { + opts = append(opts, opt.IsAbsent()) return Have(path, opts...) } diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go index 6f1b1bc..a892527 100644 --- a/internal/resource/link/link_test.go +++ b/internal/resource/link/link_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + . "codeberg.org/snonux/gonf/internal/resource/opt" ) func TestHaveSymlinkCreateAndIdempotent(t *testing.T) { @@ -14,7 +16,7 @@ func TestHaveSymlinkCreateAndIdempotent(t *testing.T) { t.Fatal(err) } - Have(link, IsSymlink(target)) + Have(link, WithSymlink(target)) got, err := os.Readlink(link) if err != nil { t.Fatalf("readlink: %v", err) @@ -45,7 +47,7 @@ func TestHaveSymlinkRepoints(t *testing.T) { t.Fatal(err) } - Have(link, IsSymlink(newT)) + Have(link, WithSymlink(newT)) got, err := os.Readlink(link) if err != nil { t.Fatal(err) @@ -66,7 +68,7 @@ func TestHaveSymlinkMovesRealFileAside(t *testing.T) { t.Fatal(err) } - Have(link, IsSymlink(target)) + Have(link, WithSymlink(target)) got, err := os.Readlink(link) if err != nil { @@ -88,7 +90,7 @@ func TestHaveHardlinkCreateAndIdempotent(t *testing.T) { t.Fatal(err) } - Have(link, IsHardlink(target)) + Have(link, WithHardlink(target)) ti, err := os.Stat(target) if err != nil { @@ -121,7 +123,7 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) { t.Fatal(err) } - Have(link, IsHardlink(target)) + Have(link, WithHardlink(target)) ti, err := os.Stat(target) if err != nil { @@ -142,7 +144,7 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) { func TestHaveHardlinkMissingTarget(t *testing.T) { dir := t.TempDir() link := filepath.Join(dir, "link") - if err := Ensure(link, IsHardlink(filepath.Join(dir, "nope"))); err == nil { + if err := Ensure(link, WithHardlink(filepath.Join(dir, "nope"))); err == nil { t.Error("expected error when hardlink target does not exist") } } diff --git a/internal/resource/opt/opt.go b/internal/resource/opt/opt.go new file mode 100644 index 0000000..23f076d --- /dev/null +++ b/internal/resource/opt/opt.go @@ -0,0 +1,156 @@ +// Package opt provides interface-based, resource-agnostic configuration +// options shared by the file, dir, and link resource packages. +// +// It is designed to be dot-imported at call sites so options read as bare +// WithMode(...) / WithOwner(...) calls regardless of the resource type: +// +// import ( +// "codeberg.org/snonux/gonf/internal/resource/file" +// . "codeberg.org/snonux/gonf/internal/resource/opt" +// ) +// +// file.Have("file.txt", WithOwner("paul"), WithMode(0o755)) +// +// Each option targets a small capability interface (Owner, Moded, ...). A +// resource implements only the setters it supports. Applying an option to a +// resource that lacks the matching interface (e.g. WithPrune on a file) is a +// programming error in the resource declaration and aborts the program via +// log.Fatalf. +package opt + +import ( + "log" + "os" +) + +// Option configures a resource. It is applied to the concrete resource value +// (e.g. *file.File) during construction. +type Option func(any) + +// Capability interfaces. A resource implements only the setters it supports. +type ( + Owner interface{ SetOwner(string) } + Grouped interface{ SetGroup(string) } + Moded interface{ SetMode(os.FileMode) } + Sourced interface{ SetSource(string) } + Contented interface{ SetContent(string) } + FileModed interface{ SetFileMode(os.FileMode) } + Prunable interface{ SetPrune() } + Absentable interface{ SetAbsent() } + Linkable interface { + SetSymlink(target string) + SetHardlink(target string) + } +) + +// WithOwner sets the owning user of the resource. +func WithOwner(owner string) Option { + return func(t any) { + r, ok := t.(Owner) + if !ok { + log.Fatalf("%T does not support WithOwner", t) + } + r.SetOwner(owner) + } +} + +// WithGroup sets the owning group of the resource. +func WithGroup(group string) Option { + return func(t any) { + r, ok := t.(Grouped) + if !ok { + log.Fatalf("%T does not support WithGroup", t) + } + r.SetGroup(group) + } +} + +// WithMode sets the resource's own file mode. +func WithMode(mode os.FileMode) Option { + return func(t any) { + r, ok := t.(Moded) + if !ok { + log.Fatalf("%T does not support WithMode", t) + } + r.SetMode(mode) + } +} + +// WithSource sets the source path the resource is populated from. +func WithSource(source string) Option { + return func(t any) { + r, ok := t.(Sourced) + if !ok { + log.Fatalf("%T does not support WithSource", t) + } + r.SetSource(source) + } +} + +// WithContent sets literal content for the resource. +func WithContent(content string) Option { + return func(t any) { + r, ok := t.(Contented) + if !ok { + log.Fatalf("%T does not support WithContent", t) + } + r.SetContent(content) + } +} + +// WithFileMode sets the mode applied to regular files copied from a source +// tree (distinct from the resource's own mode). +func WithFileMode(mode os.FileMode) Option { + return func(t any) { + r, ok := t.(FileModed) + if !ok { + log.Fatalf("%T does not support WithFileMode", t) + } + r.SetFileMode(mode) + } +} + +// WithPrune enables reconciliation of extra destination entries during a +// source copy, and recursive removal during IsAbsent(). +func WithPrune() Option { + return func(t any) { + r, ok := t.(Prunable) + if !ok { + log.Fatalf("%T does not support WithPrune", t) + } + r.SetPrune() + } +} + +// IsAbsent marks the resource for removal. +func IsAbsent() Option { + return func(t any) { + r, ok := t.(Absentable) + if !ok { + log.Fatalf("%T does not support IsAbsent", t) + } + r.SetAbsent() + } +} + +// WithSymlink makes the resource a symbolic link pointing at target. +func WithSymlink(target string) Option { + return func(t any) { + r, ok := t.(Linkable) + if !ok { + log.Fatalf("%T does not support WithSymlink", t) + } + r.SetSymlink(target) + } +} + +// WithHardlink makes the resource a hard link pointing at target. +func WithHardlink(target string) Option { + return func(t any) { + r, ok := t.(Linkable) + if !ok { + log.Fatalf("%T does not support WithHardlink", t) + } + r.SetHardlink(target) + } +} |
