Compare commits

...

12 commits

Author SHA1 Message Date
ee0b3beeb5 🔀 Merge branch 'release/0.0.1' into stable 2025-03-10 17:05:17 -05:00
899fbedd9a 🔖 Version 0.0.1 2025-03-10 17:03:52 -05:00
7126ef97a4 Add Make function
This is the important one
2025-03-10 14:53:59 -05:00
7bd5503613 ♻️ Export Config 2025-03-10 14:52:50 -05:00
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
15 changed files with 529 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/

14
CHANGELOG.md Normal file
View file

@ -0,0 +1,14 @@
# Changelog
## [0.0.1] - 2025-03-10
Initial Release! Hope you like it!
### Added
- nomino.Make
- nomino.Config
- nomino.Generator
+ We needs more of these until I'm ready
- Lots of tests!

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2025, Dan Jones <danjones@goodevilgenius.org>.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

11
README.md Normal file
View file

@ -0,0 +1,11 @@
# nomino - A filename generator
The purpose of nomino is to generate (probably random) filenames, for example, if you want to save an uploaded file to storage under a new name.
It takes a lot of inspiration (although no actual code) from [Onym](https://github.com/Blaspsoft/onym).
## TODO
I'll fill this out more in depth later.
For now, add it to a new project, and run `go doc codeberg.org/danjones000/nomino`

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

20
config.go Normal file
View file

@ -0,0 +1,20 @@
package nomino
type Config struct {
original string
prefix string
suffix string
extension string
generator Generator
}
func NewConfig(options ...Option) Config {
conf := Config{
extension: ".txt",
generator: uuidGen,
}
for _, opt := range options {
opt(&conf)
}
return conf
}

22
config_test.go Normal file
View file

@ -0,0 +1,22 @@
package nomino
import (
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
)
func TestNewConf(t *testing.T) {
c := NewConfig()
assert.Equal(t, ".txt", c.extension)
st, _ := c.generator()
_, parseErr := uuid.Parse(st)
assert.NoError(t, parseErr)
}
func TestNewConfWithOpts(t *testing.T) {
c := NewConfig(WithoutExtension(), WithPrefix("foobar"))
assert.Equal(t, "", c.extension)
assert.Equal(t, "foobar_", c.prefix)
}

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=

15
make.go Normal file
View file

@ -0,0 +1,15 @@
package nomino
import "fmt"
// Make generates a random filename. The behavior can be controlled by specifying Options
// In general, the final filename will be [prefix]_[generated_string]_[original_filename]_[suffix].[extension].
// If the name generator returns an error (generally, it shouldn't), that will be returned instead.
func Make(conf Config) (string, error) {
name, err := conf.generator()
if err != nil {
return "", err
}
return fmt.Sprintf("%s%s%s%s%s", conf.prefix, name, conf.original, conf.suffix, conf.original), nil
}

23
make_test.go Normal file
View file

@ -0,0 +1,23 @@
package nomino
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestMake(t *testing.T) {
conf := NewConfig(WithGenerator(func() (string, error) { return "abc", nil }))
st, err := Make(conf)
assert.NoError(t, err)
assert.Equal(t, "abc", st)
}
func TestMakeErr(t *testing.T) {
retErr := errors.New("oops")
conf := NewConfig(WithGenerator(func() (string, error) { return "foobar", retErr }))
st, err := Make(conf)
assert.Zero(t, st)
assert.ErrorIs(t, err, retErr)
}

42
options.go Normal file
View file

@ -0,0 +1,42 @@
package nomino
import "strings"
// Option sets configuration parameters for Config.
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, ".")
}
}

41
options_test.go Normal file
View file

@ -0,0 +1,41 @@
package nomino
import (
"testing"
"github.com/stretchr/testify/assert"
)
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)
}