Compare commits

...

8 commits

Author SHA1 Message Date
5c4e66d144 Add MultiGenerator 2025-03-10 14:25:00 -05:00
ce5f823d64 ♻️ Export Generator, and switch to WithGenerator Option 2025-03-10 13:48:11 -05:00
abe7acffd4 💡 Document code 2025-03-10 13:46:13 -05:00
1af608d7c9 Add UTC time-based generators 2025-03-10 11:52:55 -05:00
ee627547a8 Add time-based generators 2025-03-10 11:47:01 -05:00
0fc4369679 Add UUID generator 2025-03-07 17:00:38 -06:00
bd8f9ae8a6 Add options.go 2025-03-07 16:26:24 -06:00
511a81cab3 🛠 Add Taskfile 2025-03-07 16:25:40 -06:00
8 changed files with 427 additions and 6 deletions

11
.gitignore vendored
View file

@ -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/

86
Taskfile.yml Normal file
View file

@ -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

99
generators.go Normal file
View file

@ -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)
}

107
generators_test.go Normal file
View file

@ -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)
}

11
go.mod
View file

@ -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
)

12
go.sum Normal file
View file

@ -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=

57
options.go Normal file
View file

@ -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, ".")
}
}

50
options_test.go Normal file
View file

@ -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)
}