diff --git a/.github/workflows/ci-workflow.yml b/.github/workflows/ci-workflow.yml index 7ff347c..c14417d 100644 --- a/.github/workflows/ci-workflow.yml +++ b/.github/workflows/ci-workflow.yml @@ -43,7 +43,7 @@ jobs: run: go install github.com/mattn/goveralls@latest - name: Install ginkgo - run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.20.0 + run: go install github.com/onsi/ginkgo/v2/ginkgo@v2.20.2 - name: Checkout code uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 805ecb2..9dbe66e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ report.json coverage.html .task/ +test/json/marshal/ i18n/out/en-US/active.en-GB.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 0aee9a6..515e417 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,6 +25,7 @@ "dogsled", "dotenv", "dupl", + "ejson", "ents", "Ephidrina", "errcheck", @@ -59,7 +60,9 @@ "icase", "ineffassign", "Innerworld", + "jdef", "jibberjabber", + "jroot", "Kontroller", "leaktest", "linecomment", @@ -107,12 +110,15 @@ "thelper", "toplevel", "tparallel", + "tpers", + "tpref", "trimprefix", "tsys", "Turan", "typecheck", "unconvert", "unlambda", + "Unmarshall", "unparam", "usys", "vals", diff --git a/Taskfile.yml b/Taskfile.yml index cd1f17a..a053085 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -100,10 +100,14 @@ tasks: cmds: - go test ./collections - tp: + tpref: cmds: - go test ./pref + tpers: + cmds: + - go test ./internal/persist + tt: cmds: - go test diff --git a/director.go b/director.go index df836db..58f4001 100644 --- a/director.go +++ b/director.go @@ -99,7 +99,7 @@ func Prime(using *pref.Using, settings ...pref.Option) *Builders { ve := using.Validate() if using.O != nil { - return using.O, opts.GetWith(using.O), ve + return using.O, opts.Push(using.O), ve } o, binder, err := ext.options(settings...) diff --git a/internal/laboratory/traverse-fs.go b/internal/laboratory/traverse-fs.go index cf57007..2ad2817 100644 --- a/internal/laboratory/traverse-fs.go +++ b/internal/laboratory/traverse-fs.go @@ -1,24 +1,37 @@ package lab import ( + "io/fs" "os" + "strings" "testing/fstest" + + "github.com/snivilised/traverse/internal/third/lo" + "github.com/snivilised/traverse/locale" +) + +const ( + permFile = 0o666 ) +type testMapFile struct { + f fstest.MapFile +} + type TestTraverseFS struct { fstest.MapFS } -func (f *TestTraverseFS) FileExists(path string) bool { - if mapFile, found := f.MapFS[path]; found && !mapFile.Mode.IsDir() { +func (f *TestTraverseFS) FileExists(name string) bool { + if mapFile, found := f.MapFS[name]; found && !mapFile.Mode.IsDir() { return true } return false } -func (f *TestTraverseFS) DirectoryExists(path string) bool { - if mapFile, found := f.MapFS[path]; found && mapFile.Mode.IsDir() { +func (f *TestTraverseFS) DirectoryExists(name string) bool { + if mapFile, found := f.MapFS[name]; found && mapFile.Mode.IsDir() { return true } @@ -26,20 +39,49 @@ func (f *TestTraverseFS) DirectoryExists(path string) bool { } func (f *TestTraverseFS) Create(name string) (*os.File, error) { - _ = name - panic("NOT-IMPL: TestTraverseFS.Create") + if _, err := f.Stat(name); err == nil { + return nil, fs.ErrExist + } + + file := &fstest.MapFile{ + Mode: permFile, + } + + f.MapFS[name] = file + dummy := &os.File{} + return dummy, nil } -func (f *TestTraverseFS) MkDirAll(path string, perm os.FileMode) error { - _ = path - _ = perm - panic("NOT-IMPL: TestTraverseFS.MkDirAll") +func (f *TestTraverseFS) MkDirAll(name string, perm os.FileMode) error { + if !fs.ValidPath(name) { + return locale.NewInvalidPathError(name) + } + + segments := strings.Split(name, "/") + + _ = lo.Reduce(segments, + func(acc []string, s string, _ int) []string { + acc = append(acc, s) + path := strings.Join(acc, "/") + f.MapFS[path] = &fstest.MapFile{ + Mode: perm | os.ModeDir, + } + return acc + }, []string{}, + ) + + return nil } func (f *TestTraverseFS) WriteFile(name string, data []byte, perm os.FileMode) error { - _ = name - _ = data - _ = perm + if _, err := f.Stat(name); err == nil { + return fs.ErrExist + } + + f.MapFS[name] = &fstest.MapFile{ + Data: data, + Mode: perm, + } - panic("NOT-IMPL: TestTraverseFS.WriteFile") + return nil } diff --git a/internal/opts/get.go b/internal/opts/get-push.go similarity index 93% rename from internal/opts/get.go rename to internal/opts/get-push.go index e6b092b..2bf4ecf 100644 --- a/internal/opts/get.go +++ b/internal/opts/get-push.go @@ -29,7 +29,7 @@ func apply(o *pref.Options, settings ...pref.Option) (err error) { return err } -func GetWith(o *pref.Options) *Binder { +func Push(o *pref.Options) *Binder { binder := NewBinder() o.Events.Bind(&binder.Controls) diff --git a/internal/opts/json/concurrency-options.go b/internal/opts/json/concurrency-options.go new file mode 100644 index 0000000..f47715f --- /dev/null +++ b/internal/opts/json/concurrency-options.go @@ -0,0 +1,15 @@ +package json + +// TODO: can't have package name that is json as that clashes +// with the one in standard library at encoding/json, so need +// to rename; perhaps to js. + +type ( + // ConcurrencyOptions specifies options used for current traversal sessions + ConcurrencyOptions struct { + // NoW specifies the number of go-routines to use in the worker + // pool used for concurrent traversal sessions requested by using + // the Run function. + NoW uint `json:"no-of-workers"` + } +) diff --git a/internal/opts/json/filter-options.go b/internal/opts/json/filter-options.go new file mode 100644 index 0000000..2373eb2 --- /dev/null +++ b/internal/opts/json/filter-options.go @@ -0,0 +1,94 @@ +package json + +import ( + "github.com/snivilised/traverse/enums" +) + +type ( + PolyFilterDef struct { + File FilterDef + Folder FilterDef + } + + FilterDef struct { + // Type specifies the type of filter (mandatory) + Type enums.FilterType `json:"filter-type"` + + // Description describes filter (optional) + Description string `json:"filter-description"` + + // Pattern filter definition (mandatory) + Pattern string `json:"pattern"` + + // Scope which file system entries this filter applies to (defaults + // to ScopeAllEn) + Scope enums.FilterScope `json:"filter-scope"` + + // Negate, reverses the applicability of the filter (Defaults to false) + Negate bool `json:"negate"` + + // IfNotApplicable, when the filter does not apply to a directory entry, + // this value determines whether the callback is invoked for this entry + // or not (defaults to true). + IfNotApplicable enums.TriStateBool `json:"if-not-applicable"` + + // Poly allows for the definition of a PolyFilter which contains separate + // filters that target files and folders separately. If present, then + // all other fields are redundant, since the filter definitions inside + // Poly should be referred to instead. + Poly *PolyFilterDef + } + + ChildFilterDef struct { + // Type specifies the type of filter (mandatory) + Type enums.FilterType `json:"child-filter-type"` + + // Description describes filter (optional) + Description string `json:"child-filter-description"` + + // Pattern filter definition (mandatory) + Pattern string `json:"child-pattern"` + + // Negate, reverses the applicability of the filter (Defaults to false) + Negate bool `json:"negate"` + } + + SampleFilterDef struct { + // Type specifies the type of filter (mandatory) + Type enums.FilterType `json:"sample-filter-type"` + + // Description describes filter (optional) + Description string `json:"sample-description"` + + // Pattern filter definition (mandatory except if using Custom) + Pattern string `json:"sample-filter"` + + // Scope which file system entries this filter applies to; + // for sampling, only ScopeFile and ScopeFolder are valid. + Scope enums.FilterScope `json:"sample-filter-scope"` + + // Negate, reverses the applicability of the filter (Defaults to false) + Negate bool `json:"negate"` + + // Poly allows for the definition of a PolyFilter which contains separate + // filters that target files and folders separately. If present, then + // all other fields are redundant, since the filter definitions inside + // Poly should be referred to instead. + Poly *PolyFilterDef + } + + FilterOptions struct { + // Node filter definitions that applies to the current file system node + // + Node *FilterDef + + // Child denotes the Child filter that is applied to the files which + // are direct descendants of the current directory node being visited. + // + Child *ChildFilterDef + + // Sample is the filter used for sampling + // + Sample *SampleFilterDef + } +) diff --git a/internal/opts/json/hibernate-options.go b/internal/opts/json/hibernate-options.go new file mode 100644 index 0000000..f81797d --- /dev/null +++ b/internal/opts/json/hibernate-options.go @@ -0,0 +1,34 @@ +package json + +import "fmt" + +type ( + // HibernationBehaviour + HibernationBehaviour struct { + // InclusiveWake when wake occurs, permit client callback to + // be invoked for the current node. Inclusive, true by default + InclusiveWake bool `json:"hibernate-inclusive-wake"` + + // InclusiveSleep when sleep occurs, permit client callback to + // be invoked for the current node. Exclusive, false by default. + InclusiveSleep bool `json:"hibernate-inclusive-sleep"` + } + + // HibernateOptions + HibernateOptions struct { + // WakeAt defines a filter for hibernation wake condition + WakeAt *FilterDef + + // SleepAt defines a filter for hibernation sleep condition + SleepAt *FilterDef + + // Behaviour contains hibernation behavioural aspects + Behaviour HibernationBehaviour + } +) + +func (b *HibernationBehaviour) String() string { + return fmt.Sprintf("[HibernationBehaviour] inclusive wake: %v, inclusive sleep: %v", + b.InclusiveWake, b.InclusiveSleep, + ) +} diff --git a/internal/opts/json/navigation-behaviours.go b/internal/opts/json/navigation-behaviours.go new file mode 100644 index 0000000..11a9cd5 --- /dev/null +++ b/internal/opts/json/navigation-behaviours.go @@ -0,0 +1,47 @@ +package json + +type ( + SubPathBehaviour struct { + KeepTrailingSep bool + } + + SortBehaviour struct { + // case sensitive traversal order + // + IsCaseSensitive bool + + // SortFilesFirst defines whether a folder's files or directories + // should be navigated first. + // + SortFilesFirst bool + } + + CascadeBehaviour struct { + // Depth sets a maximum traversal depth + // + Depth uint + + // NoRecurse is an alternative to using Depth, but limits the traversal + // to just the path specified by the user. Since the raison d'etre + // of the navigator is to recursively process a directory tree, using + // NoRecurse would appear to be contrary to its natural behaviour. However + // there are clear usage scenarios where a client needs to process + // only the files in a specified directory. + // + NoRecurse bool + } + + NavigationBehaviours struct { + // SubPath, behaviours relating to handling of sub-path calculation + // + SubPath SubPathBehaviour + + // Sort, behaviours relating to sorting of a folder's directory entries. + // + Sort SortBehaviour + + // Cascade controls how deep to navigate + // + Cascade CascadeBehaviour + } +) diff --git a/internal/opts/json/options.go b/internal/opts/json/options.go new file mode 100644 index 0000000..079923d --- /dev/null +++ b/internal/opts/json/options.go @@ -0,0 +1,22 @@ +package json + +type ( + // Options defines the JSON persist format for options. + Options struct { + // Behaviours collection of behaviours that adjust the way navigation occurs, + // that can be tweaked by the client. + Behaviours NavigationBehaviours + + // Sampling options + Sampling SamplingOptions + + // Filter + Filter FilterOptions + + // Hibernation + Hibernate HibernateOptions + + // Concurrency contains options relating concurrency + Concurrency ConcurrencyOptions + } +) diff --git a/internal/opts/json/sampling-options.go b/internal/opts/json/sampling-options.go new file mode 100644 index 0000000..396669b --- /dev/null +++ b/internal/opts/json/sampling-options.go @@ -0,0 +1,28 @@ +package json + +import ( + "github.com/snivilised/traverse/enums" +) + +type ( + // EntryQuantities contains specification of no of files and folders + // used in various contexts, but primarily sampling. + EntryQuantities struct { + Files uint `json:"no-of-files"` + Folders uint `json:"no-of-folders"` + } + + // SamplingOptions + SamplingOptions struct { + // SampleType the type of sampling to use + SampleType enums.SampleType `json:"sample-type"` + + // SampleInReverse determines the direction of iteration for the sampling + // operation + SampleInReverse bool `json:"sample-in-reverse"` + + // NoOf specifies number of items required in each sample (only applies + // when not using Custom iterator options) + NoOf EntryQuantities + } +) diff --git a/internal/persist/convert-json_test.go b/internal/persist/convert-json_test.go new file mode 100644 index 0000000..ea394f7 --- /dev/null +++ b/internal/persist/convert-json_test.go @@ -0,0 +1,49 @@ +package persist_test + +import ( + "os" + "testing/fstest" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + "github.com/snivilised/li18ngo" + lab "github.com/snivilised/traverse/internal/laboratory" + "github.com/snivilised/traverse/internal/opts" + "github.com/snivilised/traverse/internal/persist" + "github.com/snivilised/traverse/lfs" + "github.com/snivilised/traverse/pref" +) + +var _ = Describe("Convert Options via JSON", Ordered, func() { + var ( + FS lfs.TraverseFS + ) + + BeforeAll(func() { + Expect(li18ngo.Use()).To(Succeed()) + }) + + BeforeEach(func() { + FS = &lab.TestTraverseFS{ + MapFS: fstest.MapFS{ + home: &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } + + _ = FS.MkDirAll(to, permDir|os.ModeDir) + }) + + Context("ToJSON", func() { + Context("given: source Options instance", func() { + It("should: convert to JSON", func() { + o, _, err := opts.Get( + pref.WithDepth(4), + ) + Expect(err).To(Succeed()) + Expect(persist.ToJSON(o)).To(HaveMarshaledEqual(o)) + }) + }) + }) +}) diff --git a/internal/persist/equals.go b/internal/persist/equals.go new file mode 100644 index 0000000..8e56629 --- /dev/null +++ b/internal/persist/equals.go @@ -0,0 +1,455 @@ +package persist + +import ( + "fmt" + + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + "github.com/snivilised/traverse/internal/opts/json" + "github.com/snivilised/traverse/locale" + "github.com/snivilised/traverse/pref" +) + +type UnequalValueError[T any] struct { + Field string + Value T + Other T +} + +func (e UnequalValueError[T]) Error() string { + s := fmt.Sprintf("unequal(%v) => value: %v, other: %v", e.Field, e.Value, e.Other) + + return s +} + +func (UnequalValueError[T]) Unwrap() error { + return locale.ErrUnEqualConversion +} + +type UnequalPtrError[T any, O any] struct { + Field string + Value *T + Other *O +} + +func (e UnequalPtrError[T, O]) Error() string { + s := fmt.Sprintf("unequal-ptr(%v) => value: %v, other: %v", e.Field, e.Value, e.Other) + + return s +} + +func (UnequalPtrError[T, O]) Unwrap() error { + return locale.ErrUnEqualConversion +} + +// Equals compare the pref.Options instance to the derived json instance json.Options. +// We can't use DeepEquals because, on the structs, because even though the structs +// may have te same members, DeepEqual will still fail because the host struct is +// different; eg: pref.NavigationBehaviours and json.NavigationBehaviours contain +// the same members, but they are different structs; which means comparison has to be +// done manually. +func Equals(o *pref.Options, jo *json.Options) (bool, error) { + if o == nil { + return false, fmt.Errorf("pref.Options %w", + UnequalPtrError[pref.Options, json.Options]{ + Field: "[nil pref.Options]", + Value: o, + Other: jo, + }, + ) + } + + if jo == nil { + return false, fmt.Errorf("json.Options %w", + UnequalPtrError[pref.Options, json.Options]{ + Field: "[nil json.Options]", + Value: o, + Other: jo, + }, + ) + } + + if equal, err := equalBehaviours(&o.Behaviours, &jo.Behaviours); !equal { + return false, err + } + + if equal, err := equalSampling(&o.Sampling, &jo.Sampling); !equal { + return false, err + } + + if equal, err := equalFilterOptions(&o.Filter, &jo.Filter); !equal { + return false, err + } + + if equal, err := equalFilterDef("wake-at", o.Hibernate.WakeAt, jo.Hibernate.WakeAt); !equal { + return equal, err + } + + if equal, err := equalFilterDef("sleep-at", o.Hibernate.SleepAt, jo.Hibernate.SleepAt); !equal { + return equal, err + } + + if o.Hibernate.Behaviour.InclusiveWake != jo.Hibernate.Behaviour.InclusiveWake { + return false, fmt.Errorf("hibernate-behaviour %w", UnequalValueError[bool]{ + Field: "InclusiveWake", + Value: o.Hibernate.Behaviour.InclusiveWake, + Other: jo.Hibernate.Behaviour.InclusiveWake, + }) + } + + if o.Hibernate.Behaviour.InclusiveSleep != jo.Hibernate.Behaviour.InclusiveSleep { + return false, fmt.Errorf("hibernate-behaviour %w", UnequalValueError[bool]{ + Field: "InclusiveSleep", + Value: o.Hibernate.Behaviour.InclusiveSleep, + Other: jo.Hibernate.Behaviour.InclusiveSleep, + }) + } + + if o.Concurrency.NoW != jo.Concurrency.NoW { + return false, fmt.Errorf("concurrency %w", UnequalValueError[uint]{ + Field: "NoW", + Value: o.Concurrency.NoW, + Other: jo.Concurrency.NoW, + }) + } + + return true, nil +} + +func equalBehaviours(o *pref.NavigationBehaviours, jo *json.NavigationBehaviours) (bool, error) { + if o.SubPath.KeepTrailingSep != jo.SubPath.KeepTrailingSep { + return false, fmt.Errorf("subPath %w", UnequalValueError[bool]{ + Field: "SubPath", + Value: o.SubPath.KeepTrailingSep, + Other: jo.SubPath.KeepTrailingSep, + }) + } + + if o.Sort.IsCaseSensitive != jo.Sort.IsCaseSensitive { + return false, fmt.Errorf("sort %w", UnequalValueError[bool]{ + Field: "IsCaseSensitive", + Value: o.Sort.IsCaseSensitive, + Other: jo.Sort.IsCaseSensitive, + }) + } + + if o.Cascade.Depth != jo.Cascade.Depth { + return false, fmt.Errorf("cascade %w", UnequalValueError[uint]{ + Field: "Depth", + Value: o.Cascade.Depth, + Other: jo.Cascade.Depth, + }) + } + + if o.Cascade.NoRecurse != jo.Cascade.NoRecurse { + return false, fmt.Errorf("cascade %w", UnequalValueError[bool]{ + Field: "NoRecurse", + Value: o.Cascade.NoRecurse, + Other: jo.Cascade.NoRecurse, + }) + } + + return true, nil +} + +func equalSampling(o *pref.SamplingOptions, jo *json.SamplingOptions) (bool, error) { + if o.SampleType != jo.SampleType { + return false, fmt.Errorf("sampling %w", UnequalValueError[enums.SampleType]{ + Field: "SampleType", + Value: o.SampleType, + Other: jo.SampleType, + }) + } + + if o.SampleInReverse != jo.SampleInReverse { + return false, fmt.Errorf("sampling %w", UnequalValueError[bool]{ + Field: "SampleInReverse", + Value: o.SampleInReverse, + Other: jo.SampleInReverse, + }) + } + + if o.NoOf.Files != jo.NoOf.Files { + return false, fmt.Errorf("sampling.noOf %w", UnequalValueError[uint]{ + Field: "Files", + Value: o.NoOf.Files, + Other: jo.NoOf.Files, + }) + } + + if o.NoOf.Folders != jo.NoOf.Folders { + return false, fmt.Errorf("sampling.noOf %w", UnequalValueError[uint]{ + Field: "Folders", + Value: o.NoOf.Folders, + Other: jo.NoOf.Folders, + }) + } + + return true, nil +} + +func equalFilterOptions(o *pref.FilterOptions, jo *json.FilterOptions) (bool, error) { + if equal, err := equalFilterDef("node", o.Node, jo.Node); !equal { + return equal, err + } + + if equal, err := equalChildFilterDef("child", o.Child, jo.Child); !equal { + return equal, err + } + + if equal, err := equalSampleFilterDef("sample-filter", o.Sample, jo.Sample); !equal { + return equal, err + } + + return true, nil +} + +func equalFilterDef(filterName string, + def *core.FilterDef, jdef *json.FilterDef, +) (bool, error) { + if def == nil && jdef == nil { + return true, nil + } + + if def == nil && jdef != nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.FilterDef, json.FilterDef]{ + Field: "[nil def]", + Value: def, + Other: jdef, + }, + ) + } + + if def != nil && jdef == nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.FilterDef, json.FilterDef]{ + Field: "[nil jdef]", + Value: def, + Other: jdef, + }, + ) + } + + if def.Type != jdef.Type { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[enums.FilterType]{ + Field: "Type", + Value: def.Type, + Other: jdef.Type, + }, + ) + } + + if def.Description != jdef.Description { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Description", + Value: def.Description, + Other: jdef.Description, + }, + ) + } + + if def.Pattern != jdef.Pattern { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Pattern", + Value: def.Pattern, + Other: jdef.Pattern, + }, + ) + } + + if def.Scope != jdef.Scope { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[enums.FilterScope]{ + Field: "Scope", + Value: def.Scope, + Other: jdef.Scope, + }, + ) + } + + if def.Negate != jdef.Negate { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[bool]{ + Field: "Negate", + Value: def.Negate, + Other: jdef.Negate, + }, + ) + } + + if def.IfNotApplicable != jdef.IfNotApplicable { + return false, fmt.Errorf("%q filter-def %w", filterName, + UnequalValueError[enums.TriStateBool]{ + Field: "IfNotApplicable", + Value: def.IfNotApplicable, + Other: jdef.IfNotApplicable, + }, + ) + } + + if def.Poly != nil { + if equal, err := equalFilterDef("poly", &def.Poly.File, &jdef.Poly.File); !equal { + return equal, err + } + + if equal, err := equalFilterDef("poly", &def.Poly.Folder, &jdef.Poly.Folder); !equal { + return equal, err + } + } + + return true, nil +} + +func equalChildFilterDef(filterName string, + def *core.ChildFilterDef, jdef *json.ChildFilterDef, +) (bool, error) { + if def == nil && jdef == nil { + return true, nil + } + + if def == nil && jdef != nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.ChildFilterDef, json.ChildFilterDef]{ + Field: "[nil def]", + Value: def, + Other: jdef, + }, + ) + } + + if def != nil && jdef == nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.ChildFilterDef, json.ChildFilterDef]{ + Field: "[nil jdef]", + Value: def, + Other: jdef, + }, + ) + } + + if def.Type != jdef.Type { + return false, fmt.Errorf("%q child-filter-def %w", filterName, + UnequalValueError[enums.FilterType]{ + Field: "Type", + Value: def.Type, + Other: jdef.Type, + }, + ) + } + + if def.Description != jdef.Description { + return false, fmt.Errorf("%q child-filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Description", + Value: def.Description, + Other: jdef.Description, + }, + ) + } + + if def.Pattern != jdef.Pattern { + return false, fmt.Errorf("%q child-filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Pattern", + Value: def.Pattern, + Other: jdef.Pattern, + }, + ) + } + + if def.Negate != jdef.Negate { + return false, fmt.Errorf("%q child-filter-def %w", filterName, + UnequalValueError[bool]{ + Field: "Negate", + Value: def.Negate, + Other: jdef.Negate, + }, + ) + } + + return true, nil +} + +func equalSampleFilterDef(filterName string, + def *core.SampleFilterDef, jdef *json.SampleFilterDef, +) (bool, error) { + if def == nil && jdef == nil { + return true, nil + } + + if def == nil && jdef != nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.SampleFilterDef, json.SampleFilterDef]{ + Field: "[nil def]", + Value: def, + Other: jdef, + }, + ) + } + + if def != nil && jdef == nil { + return false, fmt.Errorf("filter-def %w", + UnequalPtrError[core.SampleFilterDef, json.SampleFilterDef]{ + Field: "[nil jdef]", + Value: def, + Other: jdef, + }, + ) + } + + if def.Type != jdef.Type { + return false, fmt.Errorf("%q sample-filter-def %w", filterName, + UnequalValueError[enums.FilterType]{ + Field: "Type", + Value: def.Type, + Other: jdef.Type, + }, + ) + } + + if def.Description != jdef.Description { + return false, fmt.Errorf("%q sample-filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Description", + Value: def.Description, + Other: jdef.Description, + }, + ) + } + + if def.Pattern != jdef.Pattern { + return false, fmt.Errorf("%q sample-filter-def %w", filterName, + UnequalValueError[string]{ + Field: "Pattern", + Value: def.Pattern, + Other: jdef.Pattern, + }, + ) + } + + if def.Scope != jdef.Scope { + return false, fmt.Errorf("%q sample-filter-def %w", filterName, + UnequalValueError[enums.FilterScope]{ + Field: "Scope", + Value: def.Scope, + Other: jdef.Scope, + }, + ) + } + + if def.Negate != jdef.Negate { + return false, fmt.Errorf("%q sample-filter-def %w", filterName, + UnequalValueError[bool]{ + Field: "Negate", + Value: def.Negate, + Other: jdef.Negate, + }, + ) + } + + return true, nil +} diff --git a/internal/persist/json-matcher_test.go b/internal/persist/json-matcher_test.go new file mode 100644 index 0000000..bc3888c --- /dev/null +++ b/internal/persist/json-matcher_test.go @@ -0,0 +1,79 @@ +package persist_test + +import ( + "fmt" + + "github.com/onsi/gomega/types" + "github.com/snivilised/traverse/internal/opts/json" + "github.com/snivilised/traverse/internal/persist" + + "github.com/snivilised/traverse/pref" +) + +type MarshalJSONMatcher struct { + o *pref.Options + err error +} + +func HaveMarshaledEqual(o *pref.Options) types.GomegaMatcher { + return &MarshalJSONMatcher{ + o: o, + } +} + +func (m *MarshalJSONMatcher) Match(actual interface{}) (bool, error) { + jo, ok := actual.(*json.Options) + + if !ok { + return false, fmt.Errorf("โŒ matcher expected a *json.Options instance (%T)", jo) + } + + if equal, err := persist.Equals(m.o, jo); !equal { + m.err = err + return equal, err + } + + return true, nil +} + +func (m *MarshalJSONMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf("๐Ÿ”ฅ Expected\n\t%v\nJSON Marshal conversion result in equal result", m.err) +} + +func (m *MarshalJSONMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("๐Ÿ”ฅ Expected\n\t%v\nJSON Marshal conversion result in NON equal result", m.err) +} + +type UnMarshalJSONMatcher struct { + jo *json.Options + err error +} + +func HaveUnMarshaledEqual(jo *json.Options) types.GomegaMatcher { + return &UnMarshalJSONMatcher{ + jo: jo, + } +} + +func (m *UnMarshalJSONMatcher) Match(actual interface{}) (bool, error) { + o, ok := actual.(*pref.Options) + + if !ok { + return false, fmt.Errorf("โŒ matcher expected a *pref.Options instance (%T)", o) + } + + if equal, err := persist.Equals(o, m.jo); !equal { + m.err = err + return equal, err + } + + return true, nil +} + +func (m *UnMarshalJSONMatcher) FailureMessage(_ interface{}) string { + return fmt.Sprintf("๐Ÿ”ฅ Expected\n\t%v\nJSON UnMarshal conversion result in equal result", m.err) +} + +func (m *UnMarshalJSONMatcher) NegatedFailureMessage(_ interface{}) string { + return fmt.Sprintf("๐Ÿ”ฅ Expected\n\t%v\nJSON UnMarshal conversion result in NON equal result", m.err) +} diff --git a/internal/persist/json-options.go b/internal/persist/json-options.go new file mode 100644 index 0000000..11411eb --- /dev/null +++ b/internal/persist/json-options.go @@ -0,0 +1,182 @@ +package persist + +import ( + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/internal/opts/json" + "github.com/snivilised/traverse/internal/third/lo" + "github.com/snivilised/traverse/pref" +) + +// ๐Ÿ“ฆ pkg: persist - defines marshalling functionality. This package is +// required in order to avoid the circular dependency that would be created +// if these functions were defined in opts.json. + +const ( + JSONMarshalNoPrefix = "" + JSONMarshal2SpacesIndent = " " +) + +func ToJSON(o *pref.Options) *json.Options { + return &json.Options{ + Behaviours: json.NavigationBehaviours{ + SubPath: json.SubPathBehaviour{ + KeepTrailingSep: o.Behaviours.SubPath.KeepTrailingSep, + }, + Sort: json.SortBehaviour{ + IsCaseSensitive: o.Behaviours.Sort.IsCaseSensitive, + SortFilesFirst: o.Behaviours.Sort.SortFilesFirst, + }, + Cascade: json.CascadeBehaviour{ + Depth: o.Behaviours.Cascade.Depth, + NoRecurse: o.Behaviours.Cascade.NoRecurse, + }, + }, + Sampling: json.SamplingOptions{ + SampleType: o.Sampling.SampleType, + SampleInReverse: o.Sampling.SampleInReverse, + NoOf: json.EntryQuantities{ + Files: o.Sampling.NoOf.Files, + Folders: o.Sampling.NoOf.Folders, + }, + }, + Filter: json.FilterOptions{ + Node: NodeFilterDefToJSON(o.Filter.Node), + Child: lo.TernaryF(o.Filter.Child != nil, + func() *json.ChildFilterDef { + return &json.ChildFilterDef{ + Type: o.Filter.Child.Type, + Description: o.Filter.Child.Description, + Pattern: o.Filter.Child.Pattern, + Negate: o.Filter.Child.Negate, + } + }, + func() *json.ChildFilterDef { + return nil + }, + ), + Sample: lo.TernaryF(o.Filter.Sample != nil, + func() *json.SampleFilterDef { + return &json.SampleFilterDef{ + Type: o.Filter.Sample.Type, + Description: o.Filter.Sample.Description, + Pattern: o.Filter.Sample.Pattern, + Scope: o.Filter.Sample.Scope, + Negate: o.Filter.Sample.Negate, + } + }, + func() *json.SampleFilterDef { + return nil + }, + ), + }, + Hibernate: json.HibernateOptions{ + WakeAt: NodeFilterDefToJSON(o.Hibernate.WakeAt), + SleepAt: NodeFilterDefToJSON(o.Hibernate.SleepAt), + Behaviour: json.HibernationBehaviour{ + InclusiveWake: o.Hibernate.Behaviour.InclusiveWake, + InclusiveSleep: o.Hibernate.Behaviour.InclusiveSleep, + }, + }, + Concurrency: json.ConcurrencyOptions{ + NoW: o.Concurrency.NoW, + }, + } +} + +func NodeFilterDefToJSON(def *core.FilterDef) *json.FilterDef { + return lo.TernaryF(def != nil, + func() *json.FilterDef { + return &json.FilterDef{ + Type: def.Type, + Description: def.Description, + Pattern: def.Pattern, + Negate: def.Negate, + Scope: def.Scope, + IfNotApplicable: def.IfNotApplicable, + } + }, + func() *json.FilterDef { return nil }, + ) +} + +func FromJSON(o *json.Options) *pref.Options { + return &pref.Options{ + Behaviours: pref.NavigationBehaviours{ + SubPath: pref.SubPathBehaviour{ + KeepTrailingSep: o.Behaviours.SubPath.KeepTrailingSep, + }, + Sort: pref.SortBehaviour{ + IsCaseSensitive: o.Behaviours.Sort.IsCaseSensitive, + SortFilesFirst: o.Behaviours.Sort.SortFilesFirst, + }, + Cascade: pref.CascadeBehaviour{ + Depth: o.Behaviours.Cascade.Depth, + NoRecurse: o.Behaviours.Cascade.NoRecurse, + }, + }, + Sampling: pref.SamplingOptions{ + SampleType: o.Sampling.SampleType, + SampleInReverse: o.Sampling.SampleInReverse, + NoOf: pref.EntryQuantities{ + Files: o.Sampling.NoOf.Files, + Folders: o.Sampling.NoOf.Folders, + }, + }, + Filter: pref.FilterOptions{ + Node: NodeFilterDefFromJSON(o.Filter.Node), + Child: lo.TernaryF(o.Filter.Child != nil, + func() *core.ChildFilterDef { + return &core.ChildFilterDef{ + Type: o.Filter.Child.Type, + Description: o.Filter.Child.Description, + Pattern: o.Filter.Child.Pattern, + Negate: o.Filter.Child.Negate, + } + }, + func() *core.ChildFilterDef { + return nil + }, + ), + Sample: lo.TernaryF(o.Filter.Sample != nil, + func() *core.SampleFilterDef { + return &core.SampleFilterDef{ + Type: o.Filter.Sample.Type, + Description: o.Filter.Sample.Description, + Pattern: o.Filter.Sample.Pattern, + Scope: o.Filter.Node.Scope, + } + }, + func() *core.SampleFilterDef { + return nil + }, + ), + }, + Hibernate: core.HibernateOptions{ + WakeAt: NodeFilterDefFromJSON(o.Hibernate.WakeAt), + SleepAt: NodeFilterDefFromJSON(o.Hibernate.SleepAt), + Behaviour: core.HibernationBehaviour{ + InclusiveWake: o.Hibernate.Behaviour.InclusiveWake, + InclusiveSleep: o.Hibernate.Behaviour.InclusiveSleep, + }, + }, + Concurrency: pref.ConcurrencyOptions{ + NoW: o.Concurrency.NoW, + }, + } +} + +func NodeFilterDefFromJSON(def *json.FilterDef) *core.FilterDef { + return lo.TernaryF(def != nil, + func() *core.FilterDef { + return &core.FilterDef{ + Type: def.Type, + Description: def.Description, + Pattern: def.Pattern, + Negate: def.Negate, + } + }, + func() *core.FilterDef { + return nil + }, + ) +} diff --git a/internal/persist/marshaler-local-fs_test.go b/internal/persist/marshaler-local-fs_test.go new file mode 100644 index 0000000..eff33e2 --- /dev/null +++ b/internal/persist/marshaler-local-fs_test.go @@ -0,0 +1,96 @@ +package persist_test + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + "github.com/snivilised/li18ngo" + "github.com/snivilised/traverse/enums" + lab "github.com/snivilised/traverse/internal/laboratory" + "github.com/snivilised/traverse/internal/opts" + "github.com/snivilised/traverse/internal/persist" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/lfs" + "github.com/snivilised/traverse/pref" +) + +var _ = Describe("Marshaler", Ordered, func() { + var testPath string + + BeforeAll(func() { + Expect(li18ngo.Use()).To(Succeed()) + + testPath = lab.Repo("test") + testFile := filepath.Join(testPath, to, tempFile) + + if _, err := os.Stat(testFile); err == nil { + _ = os.Remove(testFile) + } + + toPath := filepath.Join(testPath, to) + if err := os.MkdirAll(toPath, permDir|os.ModeDir); err != nil { + Fail(err.Error()) + } + + fromPath := filepath.Join(testPath, from) + if err := os.MkdirAll(fromPath, permDir|os.ModeDir); err != nil { + Fail(err.Error()) + } + }) + + Context("local-fs", func() { + When("given pref.Options", func() { + Context("marshall", func() { + It("๐Ÿงช should: translate to json", func() { + o, _, err := opts.Get( + pref.WithDepth(4), + ) + Expect(err).To(Succeed()) + + writerFS := lfs.NewWriteFileFS(testPath, NoOverwrite) + writePath := to + "/" + tempFile + jo, err := persist.Marshal(&persist.MarshalState{ + O: o, + Active: &types.ActiveState{ + Root: to, + Hibernation: enums.HibernationPending, + NodePath: "/root/a/b/c", + Depth: 3, + }, + }, + writePath, permFile, writerFS, + ) + + Expect(err).To(Succeed()) + Expect(jo).NotTo(BeNil()) + }) + }) + }) + }) + + When("given json.Options", func() { + Context("unmarshal", func() { + XIt("๐Ÿงช should: translate from json", func() { + /* + o, _, _ := opts.Get() + marshaller = persist.NewReader(o, &types.ActiveState{ + Root: "some-root-path", + Hibernation: enums.HibernationPending, + NodePath: "/root/a/b/c", + Depth: 3, + }) + */ + readerFS := lfs.NewReadFileFS("/some-path") + state, err := persist.Unmarshal(&types.RestoreState{ + Path: "some-restore-path", + Resume: enums.ResumeStrategySpawn, + }, "/some-path", readerFS) + _ = state + + Expect(err).To(Succeed()) + }) + }) + }) +}) diff --git a/internal/persist/marshaler.go b/internal/persist/marshaler.go new file mode 100644 index 0000000..8b3bb83 --- /dev/null +++ b/internal/persist/marshaler.go @@ -0,0 +1,62 @@ +package persist + +import ( + ejson "encoding/json" + "io/fs" + + "github.com/snivilised/traverse/internal/opts/json" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/lfs" + "github.com/snivilised/traverse/pref" +) + +type ( + stateMarshaler interface { + Marshal(path string) error + Unmarshal(path string) error + } + + MarshalState struct { + O *pref.Options + Active *types.ActiveState + } + + jsonState struct { + JO *json.Options + Active *types.ActiveState + } +) + +func Marshal(ms *MarshalState, path string, perm fs.FileMode, + wfs lfs.WriteFileFS, +) (*json.Options, error) { + jo := ToJSON(ms.O) + state := &jsonState{ + JO: jo, + Active: ms.Active, + } + + data, err := ejson.MarshalIndent( + state, + JSONMarshalNoPrefix, JSONMarshal2SpacesIndent, + ) + + if err != nil { + return nil, err + } + + if equal, err := Equals(ms.O, jo); !equal { + return jo, err + } + + return jo, wfs.WriteFile(path, data, perm) +} + +func Unmarshal(_ *types.RestoreState, path string, + reader lfs.ReadFileFS, +) (*MarshalState, error) { + _ = path + _ = reader + + return &MarshalState{}, nil +} diff --git a/internal/persist/marshaler_test.go b/internal/persist/marshaler_test.go new file mode 100644 index 0000000..6667499 --- /dev/null +++ b/internal/persist/marshaler_test.go @@ -0,0 +1,267 @@ +package persist_test + +import ( + "errors" + "fmt" + "os" + "testing/fstest" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + + "github.com/snivilised/li18ngo" + "github.com/snivilised/traverse/core" + "github.com/snivilised/traverse/enums" + lab "github.com/snivilised/traverse/internal/laboratory" + "github.com/snivilised/traverse/internal/opts" + "github.com/snivilised/traverse/internal/opts/json" + "github.com/snivilised/traverse/internal/persist" + "github.com/snivilised/traverse/internal/types" + "github.com/snivilised/traverse/lfs" + "github.com/snivilised/traverse/locale" + "github.com/snivilised/traverse/pref" +) + +var _ = Describe("Marshaler", Ordered, func() { + var ( + FS lfs.TraverseFS + nodeFilterDef *core.FilterDef + childFilterDef *core.ChildFilterDef + ) + + BeforeAll(func() { + Expect(li18ngo.Use()).To(Succeed()) + nodeFilterDef = &core.FilterDef{ + Type: enums.FilterTypeGlob, + Description: "items without .flac suffix", + Pattern: "*.flac", + Scope: enums.ScopeAll, + Negate: true, + IfNotApplicable: enums.TriStateBoolTrue, + } + + childFilterDef = &core.ChildFilterDef{ + Type: enums.FilterTypeGlob, + Description: "items without .flac suffix", + Pattern: "*.flac", + Negate: true, + } + }) + + BeforeEach(func() { + FS = &lab.TestTraverseFS{ + MapFS: fstest.MapFS{ + home: &fstest.MapFile{ + Mode: os.ModeDir, + }, + }, + } + + _ = FS.MkDirAll(to, permDir|os.ModeDir) + }) + + Context("map-fs", func() { + Context("marshal", func() { + DescribeTable("success", + func(entry *errorTE) { + o, _, err := opts.Get( + entry.option(), + ) + Expect(err).To(Succeed()) + + writePath := to + "/" + tempFile + jo, err := persist.Marshal(&persist.MarshalState{ + O: o, + Active: &types.ActiveState{ + Root: to, + Hibernation: enums.HibernationPending, + NodePath: "/root/a/b/c", + Depth: 3, + }, + }, + writePath, permFile, FS, + ) + + Expect(err).To(Succeed()) + Expect(jo).NotTo(BeNil()) + + equals, err := persist.Equals(o, &json.Options{}) + Expect(equals).To(BeFalse(), "should not compare equal") + Expect(err).NotTo(Succeed()) + }, + func(entry *errorTE) string { + return fmt.Sprintf("given: %v, ๐Ÿงช should: marshal successfully", entry.given) + }, + + // NavigationBehaviours: + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "NavigationBehaviours.SubPathBehaviour", + option: func() pref.Option { + return pref.WithSubPathBehaviour(&pref.SubPathBehaviour{ + KeepTrailingSep: false, + }) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "NavigationBehaviours.WithSortBehaviour", + option: func() pref.Option { + return pref.WithSortBehaviour(&pref.SortBehaviour{ + IsCaseSensitive: true, + SortFilesFirst: true, + }) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "NavigationBehaviours.CascadeBehaviour.WithDepth", + option: func() pref.Option { + return pref.WithDepth(4) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "NavigationBehaviours.CascadeBehaviour.NoRecurse", + option: pref.WithNoRecurse, + }, + }), + + // SamplingOptions: + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "NavigationBehaviours.SamplingOptions", + option: func() pref.Option { + return pref.WithSamplingOptions(&pref.SamplingOptions{ + SampleType: enums.SampleTypeFilter, + SampleInReverse: true, + NoOf: pref.EntryQuantities{ + Files: 3, + Folders: 4, + }, + }) + }, + }, + }), + + // FilterOptions: + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "FilterOptions - Node", + option: func() pref.Option { + return pref.WithFilter(&pref.FilterOptions{ + Node: nodeFilterDef, + }) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "FilterOptions - Child", + option: func() pref.Option { + return pref.WithFilter(&pref.FilterOptions{ + Child: childFilterDef, + }) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "FilterOptions - Sample", + option: func() pref.Option { + return pref.WithFilter(&pref.FilterOptions{ + Sample: &core.SampleFilterDef{ + Type: enums.FilterTypeGlob, + Description: "items without .flac suffix", + Pattern: "*.flac", + Scope: enums.ScopeAll, + Negate: true, + Poly: &core.PolyFilterDef{ + File: *nodeFilterDef, + Folder: *nodeFilterDef, + }, + }, + }) + }, + }, + }), + + // HibernateOptions: + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "HibernateOptions.Behaviour.InclusiveWake", + option: func() pref.Option { + return pref.WithHibernationFilterWake(nodeFilterDef) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "HibernateOptions.Behaviour.InclusiveSleep", + option: func() pref.Option { + return pref.WithHibernationFilterSleep(nodeFilterDef) + }, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "HibernateOptions.Behaviour.InclusiveWake", + option: pref.WithHibernationBehaviourExclusiveWake, + }, + }), + + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "HibernateOptions.Behaviour.InclusiveSleep", + option: pref.WithHibernationBehaviourInclusiveSleep, + }, + }), + + // ConcurrencyOptions: + Entry(nil, &errorTE{ + marshalTE: marshalTE{ + given: "ConcurrencyOptions.NoW", + option: func() pref.Option { + return pref.WithNoW(5) + }, + }, + }), + ) + + Context("UnequalPtrError", func() { + When("pref.Options is nil", func() { + It("๐Ÿงช should: return UnequalPtrError", func() { + equals, err := persist.Equals(nil, &json.Options{}) + Expect(equals).To(BeFalse(), "should not compare equal") + Expect(err).NotTo(Succeed()) + Expect(errors.Is(err, locale.ErrUnEqualConversion)).To(BeTrue(), + "error should be a locale.ErrUnEqualConversion", + ) + }) + }) + + When("json FilterDef is nil", func() { + It("๐Ÿงช should: return UnequalPtrError", func() { + o, _, _ := opts.Get() + equals, err := persist.Equals(o, nil) + Expect(equals).To(BeFalse(), "should not compare equal") + Expect(err).NotTo(Succeed()) + Expect(errors.Is(err, locale.ErrUnEqualConversion)).To(BeTrue(), + "error should be a locale.ErrUnEqualConversion", + ) + }) + }) + }) + }) + }) +}) diff --git a/internal/persist/persist-suite_test.go b/internal/persist/persist-suite_test.go new file mode 100644 index 0000000..5d959e9 --- /dev/null +++ b/internal/persist/persist-suite_test.go @@ -0,0 +1,39 @@ +package persist_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" //nolint:revive // ok + . "github.com/onsi/gomega" //nolint:revive // ok + "github.com/snivilised/traverse/pref" +) + +func TestPersist(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Persist Suite") +} + +const ( + NoOverwrite = false + from = "json/unmarshal" + to = "json/marshal" + permDir = 0o777 + permFile = 0o666 + tempFile = "test-state-marshal.TEMP.json" + home = "/home" +) + +type ( + marshalTE struct { + given string + option func() pref.Option + } + + errorTE struct { + marshalTE + } + + conversionTE struct { + marshalTE + } +) diff --git a/internal/types/definitions.go b/internal/types/definitions.go index 8fe1ce6..b129a4d 100644 --- a/internal/types/definitions.go +++ b/internal/types/definitions.go @@ -113,6 +113,25 @@ type ( Pick(et enums.EntryType) AssignChildren(children []fs.DirEntry) } + + // ActiveState + ActiveState struct { + Root string + Hibernation enums.Hibernation + NodePath string + Depth int + // metrics + } + + SaveState struct { + Path string + } + + // RestoreState + RestoreState struct { + Path string + Resume enums.ResumeStrategy + } ) // KernelResult is the internal representation of core.TraverseResult diff --git a/lfs/ensure-path-at_test.go b/lfs/ensure-path-at_test.go index 876b422..3ccfcc0 100644 --- a/lfs/ensure-path-at_test.go +++ b/lfs/ensure-path-at_test.go @@ -60,7 +60,7 @@ var _ = Describe("EnsurePathAt", Ordered, func() { location += string(filepath.Separator) } - actual, err := lfs.EnsurePathAt(location, "default-test.log", perm, mfs) + actual, err := lfs.EnsurePathAt(location, "default-test.log", permFile, mfs) directory, _ := filepath.Split(actual) directory = filepath.Clean(directory) expected := lab.TrimRoot(lab.Path(home, entry.expected)) diff --git a/lfs/file-systems.go b/lfs/file-systems.go index 939b078..4de520e 100644 --- a/lfs/file-systems.go +++ b/lfs/file-systems.go @@ -3,6 +3,9 @@ package lfs import ( "io/fs" "os" + "path/filepath" + + "github.com/snivilised/traverse/locale" ) // ๐Ÿ”ฅ An important note about using standard golang file systems (io.fs/fs.FS) @@ -31,27 +34,32 @@ import ( // filepath.Separator with a virtual file system is not valid. // +// ๐Ÿงฉ ---> open + // ๐ŸŽฏ openFS type openFS struct { fsys fs.FS + root string } -func (f *openFS) Open(path string) (fs.File, error) { - return f.fsys.Open(path) +func (f *openFS) Open(name string) (fs.File, error) { + return f.fsys.Open(name) } +// ๐Ÿงฉ ---> stat + // ๐ŸŽฏ statFS type statFS struct { openFS } -func NewStatFS(path string) fs.StatFS { - ents := compose(path) +func NewStatFS(root string) fs.StatFS { + ents := compose(root) return &ents.stat } -func (f *statFS) Stat(path string) (fs.FileInfo, error) { - return fs.Stat(f.fsys, path) +func (f *statFS) Stat(name string) (fs.FileInfo, error) { + return fs.Stat(f.fsys, name) } // ๐Ÿงฉ ---> file system query @@ -62,12 +70,10 @@ type readDirFS struct { } // NewReadDirFS creates a native file system. -func NewReadDirFS(path string) fs.ReadDirFS { - ents := compose(path) +func NewReadDirFS(root string) fs.ReadDirFS { + ents := compose(root) - return &readDirFS{ - openFS: ents.open, - } + return &ents.exists } // Open opens the named file. @@ -79,8 +85,8 @@ func NewReadDirFS(path string) fs.ReadDirFS { // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. -func (n *readDirFS) Open(path string) (fs.File, error) { - return n.fsys.Open(path) +func (n *readDirFS) Open(name string) (fs.File, error) { + return n.fsys.Open(name) } // ReadDir reads the named directory @@ -99,12 +105,10 @@ type queryStatusFS struct { readDirFS } -func NewQueryStatusFS(path string) fs.StatFS { - ents := compose(path) +func NewQueryStatusFS(root string) fs.StatFS { + ents := compose(root) - return &queryStatusFS{ - readDirFS: ents.read, - } + return &ents.exists } // QueryStatusFromFS defines a file system that has a Stat @@ -135,15 +139,15 @@ type existsInFS struct { } // ExistsInFS -func NewExistsInFS(path string) ExistsInFS { - ents := compose(path) +func NewExistsInFS(root string) ExistsInFS { + ents := compose(root) return &ents.exists } // FileExists does file exist at the path specified -func (f *existsInFS) FileExists(path string) bool { - info, err := f.Stat(path) +func (f *existsInFS) FileExists(name string) bool { + info, err := f.Stat(name) if err != nil { return false } @@ -156,8 +160,8 @@ func (f *existsInFS) FileExists(path string) bool { } // DirectoryExists does directory exist at the path specified -func (f *existsInFS) DirectoryExists(path string) bool { - info, err := f.Stat(path) +func (f *existsInFS) DirectoryExists(name string) bool { + info, err := f.Stat(name) if err != nil { return false } @@ -174,12 +178,10 @@ type readFileFS struct { queryStatusFS } -func NewReadFileFS(path string) ReadFileFS { - ents := compose(path) +func NewReadFileFS(root string) ReadFileFS { + ents := compose(root) - return &readFileFS{ - queryStatusFS: ents.query, - } + return &ents.reader } // ReadFile reads the named file from the file system fs and returns its contents. @@ -190,8 +192,8 @@ func NewReadFileFS(path string) ReadFileFS { // If fs implements [ReadFileFS], ReadFile calls fs.ReadFile. // Otherwise ReadFile calls fs.Open and uses Read and Close // on the returned [File]. -func (f *readFileFS) ReadFile(path string) ([]byte, error) { - return fs.ReadFile(f.queryStatusFS.statFS.fsys, path) +func (f *readFileFS) ReadFile(name string) ([]byte, error) { + return fs.ReadFile(f.queryStatusFS.statFS.fsys, name) } // ๐Ÿงฉ ---> file system mutation @@ -209,7 +211,7 @@ type mkDirAllFS struct { // NewMkDirAllFS func NewMkDirAllFS() MkDirAllFS { - return &mkDirAllFS{} + panic("NOT-IMPL: NewMkDirAllFS") } // MkdirAll creates a directory named path, @@ -219,8 +221,10 @@ func NewMkDirAllFS() MkDirAllFS { // directories that MkdirAll creates. // If path is already a directory, MkdirAll does nothing // and returns nil. -func (*mkDirAllFS) MkDirAll(path string, perm os.FileMode) error { - return os.MkdirAll(path, perm) // !!! WRONG use the correct fs +func (f *mkDirAllFS) MkDirAll(name string, perm os.FileMode) error { + // TODO: check path is valid using fs.ValidPath + // + return os.MkdirAll(name, perm) } // ๐ŸŽฏ writeFileFS @@ -228,15 +232,10 @@ type writeFileFS struct { baseWriterFS } -func NewWriteFileFS(path string, overwrite bool) WriteFileFS { - ents := compose(path) +func NewWriteFileFS(root string, overwrite bool) WriteFileFS { + ents := compose(root).attach(overwrite) - return &writeFileFS{ - baseWriterFS: baseWriterFS{ - openFS: ents.open, - overwrite: overwrite, - }, - } + return &ents.writer } // Create creates or truncates the named file. If the file already exists, @@ -254,7 +253,12 @@ func NewWriteFileFS(path string, overwrite bool) WriteFileFS { // has to be made at the point of creating the file system. This is less // flexible and just results in friction, but this is out of our power. func (f *writeFileFS) Create(name string) (*os.File, error) { - return os.Create(name) + if !fs.ValidPath(name) { + return nil, locale.NewInvalidPathError(name) + } + + path := filepath.Join(f.root, name) + return os.Create(path) } // WriteFile writes data to the named file, creating it if necessary. @@ -263,7 +267,12 @@ func (f *writeFileFS) Create(name string) (*os.File, error) { // Since WriteFile requires multiple system calls to complete, a failure mid-operation // can leave the file in a partially written state. func (f *writeFileFS) WriteFile(name string, data []byte, perm os.FileMode) error { - return os.WriteFile(name, data, perm) + if !fs.ValidPath(name) { + return locale.NewInvalidPathError(name) + } + + path := filepath.Join(f.root, name) + return os.WriteFile(path, data, perm) } // ๐Ÿงฉ ---> file system aggregators @@ -277,17 +286,10 @@ type readerFS struct { } // NewReaderFS -func NewReaderFS(path string) ReaderFS { - ents := compose(path) +func NewReaderFS(root string) ReaderFS { + ents := compose(root) - return &readerFS{ - readDirFS: ents.read, - readFileFS: readFileFS{ - queryStatusFS: ents.query, - }, - existsInFS: ents.exists, - statFS: ents.stat, - } + return &ents.reader } // ๐ŸŽฏ writerFS @@ -296,17 +298,10 @@ type writerFS struct { writeFileFS } -func NewWriterFS(path string, overwrite bool) WriterFS { - ents := compose(path) +func NewWriterFS(root string, overwrite bool) WriterFS { + ents := compose(root).attach(overwrite) - return &writerFS{ - writeFileFS: writeFileFS{ - baseWriterFS: baseWriterFS{ - openFS: ents.open, - overwrite: overwrite, - }, - }, - } + return &ents.writer } // ๐ŸŽฏ traverseFS @@ -315,43 +310,49 @@ type traverseFS struct { writerFS } -func NewTraverseFS(path string, overwrite bool) TraverseFS { - ents := compose(path) +func NewTraverseFS(root string, overwrite bool) TraverseFS { + ents := compose(root).attach(overwrite) return &traverseFS{ - readerFS: readerFS{ - readDirFS: ents.read, - readFileFS: readFileFS{ - queryStatusFS: ents.query, - }, - existsInFS: ents.exists, - statFS: ents.stat, + readerFS: ents.reader, + writerFS: ents.writer, + } +} + +// ๐Ÿงฉ ---> construction + +type ( + entities struct { + open openFS + read readDirFS + stat statFS + query queryStatusFS + exists existsInFS + reader readerFS + writer writerFS + } +) + +func (e *entities) attach(overwrite bool) *entities { + e.writer = writerFS{ + mkDirAllFS: mkDirAllFS{ + existsInFS: e.exists, }, - writerFS: writerFS{ - mkDirAllFS: mkDirAllFS{ - existsInFS: ents.exists, - }, - writeFileFS: writeFileFS{ - baseWriterFS: baseWriterFS{ - openFS: ents.open, - overwrite: overwrite, - }, + writeFileFS: writeFileFS{ + baseWriterFS: baseWriterFS{ + openFS: e.open, + overwrite: overwrite, }, }, } -} -type entities struct { - open openFS - read readDirFS - stat statFS - query queryStatusFS - exists existsInFS + return e } func compose(root string) *entities { open := openFS{ fsys: os.DirFS(root), + root: root, } read := readDirFS{ openFS: open, @@ -369,11 +370,21 @@ func compose(root string) *entities { queryStatusFS: query, } + reader := readerFS{ + readDirFS: read, + readFileFS: readFileFS{ + queryStatusFS: query, + }, + existsInFS: exists, + statFS: stat, + } + return &entities{ open: open, read: read, stat: stat, query: query, exists: exists, + reader: reader, } } diff --git a/lfs/lfs-defs.go b/lfs/lfs-defs.go index 1cc2b19..9228f38 100644 --- a/lfs/lfs-defs.go +++ b/lfs/lfs-defs.go @@ -23,17 +23,17 @@ type ( // ExistsInFS contains methods that check the existence of file system items. ExistsInFS interface { // FileExists does file exist at the path specified - FileExists(path string) bool + FileExists(name string) bool // DirectoryExists does directory exist at the path specified - DirectoryExists(path string) bool + DirectoryExists(name string) bool } // ReadFileFS file system non streaming reader ReadFileFS interface { fs.FS // Read reads file at path, from file system specified - ReadFile(path string) ([]byte, error) + ReadFile(name string) ([]byte, error) } // ReaderFS @@ -47,7 +47,7 @@ type ( // MkDirAllFS is a file system with a MkDirAll method. MkDirAllFS interface { ExistsInFS - MkDirAll(path string, perm os.FileMode) error + MkDirAll(name string, perm os.FileMode) error } // CopyFS diff --git a/lfs/lfs-suite_test.go b/lfs/lfs-suite_test.go index da5fdd9..7982c8b 100644 --- a/lfs/lfs-suite_test.go +++ b/lfs/lfs-suite_test.go @@ -35,7 +35,7 @@ type ( ) const ( - perm = 0o766 + permFile = 0o666 ) var ( diff --git a/locale/messages-errors.go b/locale/messages-errors.go index 3254bb7..71ca56c 100644 --- a/locale/messages-errors.go +++ b/locale/messages-errors.go @@ -475,3 +475,75 @@ var ErrIDGeneratorFuncCantBeNil = IDGeneratorFuncCantBeNilError{ Data: IDGeneratorFuncCantBeNilErrorTemplData{}, }, } + +// โŒ UnEqualJSONConversion + +// UnEqualConversionTemplData +type UnEqualJSONConversionErrorTemplData struct { + traverseTemplData +} + +// Message +func (td UnEqualJSONConversionErrorTemplData) Message() *i18n.Message { + return &i18n.Message{ + ID: "un-equal-conversion.error", + Description: "JSON options conversion error", + Other: "unequal JSON conversion", + } +} + +type UnEqualConversionError struct { + li18ngo.LocalisableError +} + +var ErrUnEqualConversion = UnEqualConversionError{ + LocalisableError: li18ngo.LocalisableError{ + Data: UnEqualJSONConversionErrorTemplData{}, + }, +} + +// โŒ InvalidPath + +// InvalidPathErrorTemplData invalid file system path; path must be relative +// relative to the root already defined for this file system so should not +// start or end with a /. Also, only a / should be used to denote a separator +// which applies to all platforms. +type InvalidPathErrorTemplData struct { + traverseTemplData + Path string +} + +// IsInvalidExtGlobFilterMissingSeparatorError uses errors.Is to check +// if the err's error tree contains the core error: +// InvalidExtGlobFilterMissingSeparatorError +func IsInvalidPathError(err error) bool { + return errors.Is(err, errInvalidPath) +} + +func NewInvalidPathError(path string) error { + return errors.Wrap( + errInvalidPath, + li18ngo.Text(InvalidPathErrorTemplData{ + Path: path, + }), + ) +} + +// Message +func (td InvalidPathErrorTemplData) Message() *i18n.Message { + return &i18n.Message{ + ID: "invalid-path.error", + Description: "Invalid file system path", + Other: "invalid path {{.Path}}", + } +} + +type InvalidPathError struct { + li18ngo.LocalisableError +} + +var errInvalidPath = InvalidPathError{ + LocalisableError: li18ngo.LocalisableError{ + Data: InvalidPathErrorTemplData{}, + }, +} diff --git a/pref/json-options.go b/pref/json-options.go index 635ed98..8a20769 100644 --- a/pref/json-options.go +++ b/pref/json-options.go @@ -29,11 +29,3 @@ type JSONOptions struct { // Concurrency ConcurrencyOptions } - -func ToJSON(*Options) *JSONOptions { - return &JSONOptions{} -} - -func FromJSON(*JSONOptions) *Options { - return &Options{} -} diff --git a/pref/options-core.go b/pref/options-core.go deleted file mode 100644 index 16f4e3e..0000000 --- a/pref/options-core.go +++ /dev/null @@ -1,28 +0,0 @@ -package pref - -import ( - "github.com/snivilised/traverse/core" -) - -type CoreOptions struct { - // Behaviours collection of behaviours that adjust the way navigation occurs, - // that can be tweaked by the client. - // - Behaviours NavigationBehaviours - - // Sampling options - // - Sampling SamplingOptions - - // Filter - // - Filter FilterOptions - - // Hibernation - // - Hibernate core.HibernateOptions - - // Concurrency contains options relating concurrency - // - Concurrency ConcurrencyOptions -} diff --git a/scripts/coverage-exclusion-list.txt b/scripts/coverage-exclusion-list.txt index 914b755..ff91fa1 100644 --- a/scripts/coverage-exclusion-list.txt +++ b/scripts/coverage-exclusion-list.txt @@ -1,3 +1,4 @@ github.com/snivilised/traverse/enums github.com/snivilised/traverse/internal/third github.com/snivilised/traverse/internal/laboratory/ +github.com/snivilised/traverse/internal/persist/equals.go