diff --git a/.gitignore b/.gitignore index 1a431c2..71f6a2d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,11 +11,8 @@ # Test binary, built with `go test -c` *.test -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - -# Dependency directories (remove the comment below to include it) -# vendor/ +# Dependency directories +vendor/ # Go workspace file go.work @@ -23,4 +20,6 @@ go.work.sum # env file .env -cover.* + +build/ +.task/ diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..025ff4b --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,86 @@ +version: '3' + +tasks: + default: + cmds: + - task: fmt + - task: test + - task: build + + fmt: + desc: Format go code + sources: + - '**/*.go' + cmds: + - go fmt ./... + - go mod tidy + + gen: + desc: Generate files + sources: + - '**/*.go' + cmds: + - go generate ./... + + vet: + desc: Vet go code + sources: + - '**/*.go' + cmds: + - go vet ./... + + critic: + desc: Critique go code + sources: + - '**/*.go' + cmds: + - gocritic check ./... + + staticcheck: + desc: Static check go code + sources: + - '**/*.go' + cmds: + - staticcheck ./... + + vuln: + desc: Check for vulnerabilities + sources: + - '**/*.go' + cmds: + - govulncheck ./... + + lint: + desc: Do static analysis + deps: + - vet + - critic + - staticcheck + - vuln + + test: + desc: Run unit tests + deps: [fmt, vet] + sources: + - '**/*.go' + generates: + - build/cover.out + cmds: + - go test -race -cover -coverprofile build/cover.out ./... + + coverage-report: + desc: Build coverage report + deps: [test] + sources: + - build/cover.out + generates: + - build/cover.html + cmds: + - go tool cover -html=build/cover.out -o build/cover.html + + serve-report: + desc: Serve the coverage report + deps: [coverage-report] + cmds: + - ip addr list | grep inet + - php -S 0.0.0.0:3265 -t build diff --git a/generators.go b/generators.go new file mode 100644 index 0000000..c409988 --- /dev/null +++ b/generators.go @@ -0,0 +1,99 @@ +package nomino + +import ( + "errors" + "time" + + "github.com/google/uuid" +) + +// Generator is a function that returns the "random" portion of the returned filename. +// Technically, it doesn't necessarily need to be random, and could be based on time, or a counter, +// for example. +type Generator func() (string, error) + +// WithGenerator sets the specified generator +func WithGenerator(g Generator) Option { + return func(c *config) { + c.generator = g + } +} + +// ErrMissingGenerators is returned by a multi-generator if no generators are supplied. +var ErrMissingGenerators = errors.New("no generators supplied") + +func missingGen() (string, error) { + return "", ErrMissingGenerators +} + +// MultiGeneratorInOrder allows the use of multiple generators. Each new invokation will use the next generator in turn. +// If none are passed, the generator will always return ErrMissingGenerators. +func MultiGeneratorInOrder(gens ...Generator) Generator { + if len(gens) == 0 { + return missingGen + } + + if len(gens) == 1 { + return gens[0] + } + + var idx int + return func() (string, error) { + st, err := gens[idx]() + idx = (idx + 1) % len(gens) + return st, err + } +} + +func uuidGen() (string, error) { + u, err := uuid.NewRandom() + if err != nil { + return "", err + } + return u.String(), nil +} + +// UUID generates a UUIDv4. +func UUID() Generator { + return uuidGen +} + +// FileTimestamp is the default format for WithTimestamp and WithTime +const FileTimestamp string = "2006-01-02_03-05-06-0700" + +// Timestamp generates a a date and time for the current time. +// It is formatted accourding to FileTimestamp +func Timestamp() Generator { + return TimestampWithFormat(FileTimestamp) +} + +// TimestampWithFormat generates a date and time for the current time with the supplied format. +func TimestampWithFormat(f string) Generator { + return FormattedTime(time.Now(), FileTimestamp) +} + +// Time generates a date and time for the supplied time. +// It is formatted accourding to FileTimestamp +func Time(t time.Time) Generator { + return FormattedTime(t, FileTimestamp) +} + +// FormattedTime generates a date and time for the supplied time with the supplied format. +func FormattedTime(t time.Time, f string) Generator { + return func() (string, error) { + return t.Format(f), nil + } +} + +// FileTimestamp is the default format for WithTimestampUTC and WithTimeUTC +const FileTimestampNoTZ string = "2006-01-02_03-05-06" + +// TimestampUTC generates a date and time for the current time in UTC without a timezone in the format. +func TimestampUTC() Generator { + return TimeUTC(time.Now()) +} + +// TimeUTC generates a date and time for the supplied time in UTC without a timezone in the format. +func TimeUTC(t time.Time) Generator { + return FormattedTime(t.UTC(), FileTimestampNoTZ) +} diff --git a/generators_test.go b/generators_test.go new file mode 100644 index 0000000..210181f --- /dev/null +++ b/generators_test.go @@ -0,0 +1,107 @@ +package nomino + +import ( + "errors" + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestWithGenerator(t *testing.T) { + g := func() (string, error) { return "abc", nil } + var c config + WithGenerator(g)(&c) + st, err := c.generator() + assert.NoError(t, err) + assert.Equal(t, "abc", st) +} + +func TestMultiGeneratorInOrder(t *testing.T) { + st1 := "abc" + st2 := "def" + er1 := errors.New("oops") + g1 := func() (string, error) { return st1, nil } + g2 := func() (string, error) { return st2, nil } + g3 := func() (string, error) { return "", er1 } + g := MultiGeneratorInOrder(g1, g2, g3) + st, err := g() + assert.NoError(t, err) + assert.Equal(t, st1, st) + st, err = g() + assert.NoError(t, err) + assert.Equal(t, st2, st) + st, err = g() + assert.Zero(t, st) + assert.ErrorIs(t, err, er1) + st, err = g() + assert.NoError(t, err) + assert.Equal(t, st1, st) +} + +func TestMultiGeneratorInOrderOne(t *testing.T) { + st1 := "abc" + g1 := func() (string, error) { return st1, nil } + g := MultiGeneratorInOrder(g1) + + st, err := g() + assert.NoError(t, err) + assert.Equal(t, st1, st) + st, err = g() + assert.NoError(t, err) + assert.Equal(t, st1, st) +} + +func TestMultiGeneratorInOrderMissing(t *testing.T) { + g := MultiGeneratorInOrder() + st, err := g() + assert.Zero(t, st) + assert.ErrorIs(t, err, ErrMissingGenerators) + st, err = g() + assert.Zero(t, st) + assert.ErrorIs(t, err, ErrMissingGenerators) +} + +func TestUUID(t *testing.T) { + st, err := UUID()() + assert.NoError(t, err) + _, parseErr := uuid.Parse(st) + assert.NoError(t, parseErr) +} + +type badRead struct{} + +func (badRead) Read([]byte) (int, error) { + return 0, errors.New("sorry") +} + +func TestUUIDFail(t *testing.T) { + uuid.SetRand(badRead{}) + defer uuid.SetRand(nil) + + _, err := UUID()() + assert.Equal(t, errors.New("sorry"), err) +} + +func TestTimestamp(t *testing.T) { + n := time.Now() + st, err := Timestamp()() + assert.NoError(t, err) + assert.Equal(t, n.Format(FileTimestamp), st) +} + +func TestTime(t *testing.T) { + d := time.Date(1986, time.March, 28, 12, 0, 0, 0, time.UTC) + + st, err := Time(d)() + assert.NoError(t, err) + assert.Equal(t, d.Format(FileTimestamp), st) +} + +func TestTimestampUTC(t *testing.T) { + n := time.Now() + st, err := TimestampUTC()() + assert.NoError(t, err) + assert.Equal(t, n.UTC().Format(FileTimestampNoTZ), st) +} diff --git a/go.mod b/go.mod index ff9df1b..de6af60 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,14 @@ module codeberg.org/danjones000/nomino go 1.23.6 + +require ( + github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..14c872b --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/options.go b/options.go new file mode 100644 index 0000000..7f93d57 --- /dev/null +++ b/options.go @@ -0,0 +1,57 @@ +package nomino + +import "strings" + +type config struct { + original string + prefix string + suffix string + extension string + generator Generator +} + +func defaultConf() config { + return config{ + extension: ".txt", + generator: uuidGen, + } +} + +// Option is an option for nomino +type Option func(c *config) + +// WithOriginal sets the original filename. +// This will be included in the generated name after the generated string and before the suffix. +func WithOriginal(o string) Option { + return func(c *config) { + c.original = "_" + o + } +} + +// WithPrefix sets a prefix for the generated name. +func WithPrefix(p string) Option { + return func(c *config) { + c.prefix = p + "_" + } +} + +// WithSuffix sets a suffix for the generated name. It will be included in the base name before the suffix. +func WithSuffix(s string) Option { + return func(c *config) { + c.suffix = "_" + s + } +} + +// WithoutExtension sets no extension for the generated filename. By default, it will be txt +func WithoutExtension() Option { + return func(c *config) { + c.extension = "" + } +} + +// WithExtension sets the extension for the generated filename. +func WithExtension(ext string) Option { + return func(c *config) { + c.extension = "." + strings.TrimPrefix(ext, ".") + } +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..ea9ed47 --- /dev/null +++ b/options_test.go @@ -0,0 +1,50 @@ +package nomino + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestDefaultConf(t *testing.T) { + c := defaultConf() + assert.Equal(t, ".txt", c.extension) + st, _ := c.generator() + _, parseErr := uuid.Parse(st) + assert.NoError(t, parseErr) +} + +func TestWithOriginal(t *testing.T) { + var c config + name := "foobar" + WithOriginal(name)(&c) + assert.Equal(t, "_"+name, c.original) +} + +func TestWithPrefix(t *testing.T) { + var c config + pref := "draft" + WithPrefix(pref)(&c) + assert.Equal(t, pref+"_", c.prefix) +} + +func TestWithSuffix(t *testing.T) { + var c config + suff := "out" + WithSuffix(suff)(&c) + assert.Equal(t, "_"+suff, c.suffix) +} + +func TestWithoutExtension(t *testing.T) { + c := config{extension: ".foobar"} + WithoutExtension()(&c) + assert.Equal(t, "", c.extension) +} + +func TestWithExtension(t *testing.T) { + var c config + ext := "yaml" + WithExtension(ext)(&c) + assert.Equal(t, "."+ext, c.extension) +}