diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..273e2af --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,39 @@ +linters: + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - copyloopvar + - dupl + - err113 + - errname + - exptostd + - fatcontext + - funlen + - gocognit + - goconst + - gocritic + - gocyclo + - godot + - godox + - gosec + - perfsprint + - testifylint + +linters-settings: + testifylint: + enable-all: true + disable: + - require-error + gocognit: + min-complexity: 5 + gocyclo: + min-complexity: 5 + gocritic: + enable-all: true + settings: + hugeParam: + sizeThreshold: 255 diff --git a/CHANGELOG.md b/CHANGELOG.md index b47e994..9020008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,111 @@ # Changelog -## [0.0.1] - 2025-03-10 +### [1.0.0] - 2025-03-31 - 🚀 Stable release! + +#### Support + +- 📝 Vastly improved [go docs](https://pkg.go.dev/codeberg.org/danjones000/nomino). +- 📝 Fill out README with examples and links. + +#### Tools + +- 🛠 Replace all linting with golangci-lint + +#### Code quality + +- 🚨 A bunch of small improvements from new linter + +### [0.5.0] - 2025-03-19 - ✨ Different types of UUIDs + +#### Features + +- ✨ Allow for different types of UUIDs in the UUID `Generator` +- ✨ `Config.AddOptions` method +- ✨ `Generator.MakeWithConfig` method + +#### Support + +- 📝 Some better examples + +### [0.4.0] - 2025-03-15 - ✨ More Generators, and `Generator.Make` method + +#### Features + +- ✨ Add Random Generator +- ✨ Add Make method to Generator +- ✨ Add MultiGeneratorRandomOrder + +#### Changes + +- 💔 Breaking changes: Replace HashType with Hasher: This supports all crypto.Hash + +#### Support + +- 📝 Add some missing doc comments + +### [0.3.0] - 2025-03-14 - ♻️ Refactor multiple Generators into one + +#### Features + +- ♻️ Simplified multiple `Generator` functions to single function with options +- 📝 Added a lot of examples for docs +- ✨ Can add extra `Option`s to `Make` + +Multiple breaking changes around Generators. + +#### Bugs + +- 🐛 Fixed date formats + +### [0.2.1] - 2025-03-14 - ✨ New Hash Generator + +#### Features + +- ✨ Add Hash Generator + +#### Dev Tooling + +- 🛠 Added a task to serve docs +- 🛠 Added tasts to check code complexity + +#### Miscellaneous + +- 💚 Fixed some `go vet` complaints + +### [0.2.0] - 2025-03-14 - ✨ New `Generator`s + +#### Features + +- ✨ Add `IncrementalFormat`* Generators +- ✨ Add `Slug`* Generators +- ✨ Add `WithOriginalSlug`* Options +- 💔 Change signature of `Generator` function + +Note that this last change **is** a breaking change from 0.0.3, but only for custom Generators. + +## [0.0.3] - 2025-03-11 - ✨ `WithSeparator` + +### Features + +- ✨ Added `WithSeparator` to allow for different separators between the parts of the generated filename. + +## [0.0.2] - 2025-03-11 - 🐛 Bugfix + +Bugfix release + +### Fixes + +- 🐛 Extension being ignored. Original included twice. + +## [0.0.1] - 2025-03-10 - 🚀 Initial Release Initial Release! Hope you like it! ### Added -- nomino.Make -- nomino.Config -- nomino.Generator +- ✨ `nomino.Make`` +- ✨ `nomino.Config` +- ✨ `nomino.Generator` + We needs more of these until I'm ready -- Lots of tests! +- ✅ Lots of tests! diff --git a/README.md b/README.md index e364bfc..e0b3f51 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,80 @@ The purpose of nomino is to generate (probably random) filenames, for example, i It takes a lot of inspiration (although no actual code) from [Onym](https://github.com/Blaspsoft/onym). -## TODO +Make sure to check out the [official documentation][docs]. -I'll fill this out more in depth later. +## Installation -For now, add it to a new project, and run `go doc codeberg.org/danjones000/nomino` +Add `codeberg.org/danjones000/nomino` to your project. You can do `go get codeberg.org/danjones000/nomino`, or simply add it to an import and run `go mod tidy`. + +But, you probably already know this. + +## Usage + +There are **a lot** of examples in the [official documentation][examples]. Take a look through these to get an idea. + +The main concept in `nomino` is the [Generator][]. The simplest way to generate a random filename, is to use the [Generator][] directly, in this way: + +```go +import ( + "fmt" + "codeberg.org/danjones000/nomino" +) + +func main() { + name, _ := nomino.Random().Make() + fmt.Println(name) // Du3Sfh8p.txt +} +``` + +The second argument is an error. Most of the generators are always successful, and that error will be `nil`, but you should check the errors if you're not sure. + +The second way to generate a new filename is using the [Make][] function. + +```go +import ( + "fmt" + "codeberg.org/danjones000/nomino" +) + +func main() { + config := nomino.NewConfig(nomino.WithGenerator(nomino.Random())) + name, _ := nomino.Make(config) + fmt.Println(name) // Du3Sfh8p.txt +} +``` + +Although in these examples, `nomino.Make` is more verbose, it can be beneficial to using that function if you have some customizations to how you generate the filenames. + +### Configuration + +The [Config][] allows you to customize how the generated filename works with various [Options][Option]. The [options][Option] allows you to customize things like [adding a prefix](https://pkg.go.dev/codeberg.org/danjones000/nomino#WithPrefix), or changing the [extension](https://pkg.go.dev/codeberg.org/danjones000/nomino#WithExtension) of the generated filename (by default, it uses `.txt`). + +Have a look at [all the Options][Option]. + +### Generator + +The [Generator][] is the piece that returns the "random" portion of the generated filename, although, it doesn't actually have to be random. + +Here are the built-in [Generators][Generator]: + +- [UUID](https://pkg.go.dev/codeberg.org/danjones000/nomino#UUID) generates a UUID. This is the default if none is specified. +- [Random](https://pkg.go.dev/codeberg.org/danjones000/nomino#Random) generates a random string. By default, it's 8 characters long, but can be whatever length you provide. +- [Incremental](https://pkg.go.dev/codeberg.org/danjones000/nomino#Incremental) will generate just a series of integers, starting at 0. +- [Timestamp](https://pkg.go.dev/codeberg.org/danjones000/nomino#Timestamp) generates a string from the current time. It will look like "2009-11-10T23-00-00+0000.txt", although this can be customized. +- Both [Slug](https://pkg.go.dev/codeberg.org/danjones000/nomino#Slug) and [Hash](https://pkg.go.dev/codeberg.org/danjones000/nomino#Hash) work on the original name provided by [WithOriginal](https://pkg.go.dev/codeberg.org/danjones000/nomino#WithOriginal). Slug generats a slug from the name, while Hash hashes it. By default, it uses MD5. + +You can also use multiple generators, either [in order](https://pkg.go.dev/codeberg.org/danjones000/nomino#MultiGeneratorInOrder), or [in a random order](https://pkg.go.dev/codeberg.org/danjones000/nomino#MultiGeneratorRandomOrder). + +Finally, you can create a [custom generator](https://pkg.go.dev/codeberg.org/danjones000/nomino#example-WithGenerator-CustomGenerator) as well. + +## RTFM (Read the fabulous manual) + +[Official docs][docs], especially the [examples][]. Especially check out the [full example](https://pkg.go.dev/codeberg.org/danjones000/nomino#example-package), which includes how to use a global configuration. + +[docs]: https://pkg.go.dev/codeberg.org/danjones000/nomino +[examples]: https://pkg.go.dev/codeberg.org/danjones000/nomino#pkg-examples +[Generator]: https://pkg.go.dev/codeberg.org/danjones000/nomino#Generator +[Config]: https://pkg.go.dev/codeberg.org/danjones000/nomino#Config +[Option]: https://pkg.go.dev/codeberg.org/danjones000/nomino#Option +[Make]: https://pkg.go.dev/codeberg.org/danjones000/nomino#Make diff --git a/Taskfile.yml b/Taskfile.yml index 025ff4b..ef9f12e 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,7 +5,7 @@ tasks: cmds: - task: fmt - task: test - - task: build + - task: lint fmt: desc: Format go code @@ -22,45 +22,16 @@ tasks: 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 + sources: + - '**/*.go' + cmds: + - golangci-lint run test: desc: Run unit tests - deps: [fmt, vet] + deps: [fmt] sources: - '**/*.go' generates: @@ -83,4 +54,9 @@ tasks: deps: [coverage-report] cmds: - ip addr list | grep inet - - php -S 0.0.0.0:3265 -t build + - php -S 0.0.0.0:3434 -t build + + serve-docs: + desc: Serve the current docs + cmds: + - godoc -http=0.0.0.0:3434 -play diff --git a/config.go b/config.go index dc6af3d..54456bf 100644 --- a/config.go +++ b/config.go @@ -1,20 +1,34 @@ package nomino +// Config controls how the generatred filename is created. type Config struct { original string prefix string suffix string extension string + separator string generator Generator } +// NewConfig returns a new [Config] with each [Option] specified. +// With no Options, the Config uses an extension of .txt, a separator +// of _, and the [UUID] [Generator]. func NewConfig(options ...Option) Config { conf := Config{ extension: ".txt", - generator: uuidGen, + separator: "_", + generator: UUID(nil), } for _, opt := range options { opt(&conf) } return conf } + +// AddOptions creates a new [Config] with each [Option] added. +func (c Config) AddOptions(options ...Option) Config { + for _, opt := range options { + opt(&c) + } + return c +} diff --git a/config_test.go b/config_test.go index bb88b53..f6ed21d 100644 --- a/config_test.go +++ b/config_test.go @@ -10,7 +10,7 @@ import ( func TestNewConf(t *testing.T) { c := NewConfig() assert.Equal(t, ".txt", c.extension) - st, _ := c.generator() + st, _ := c.generator(&c) _, parseErr := uuid.Parse(st) assert.NoError(t, parseErr) } @@ -18,5 +18,14 @@ func TestNewConf(t *testing.T) { func TestNewConfWithOpts(t *testing.T) { c := NewConfig(WithoutExtension(), WithPrefix("foobar")) assert.Equal(t, "", c.extension) - assert.Equal(t, "foobar_", c.prefix) + assert.Equal(t, "foobar", c.prefix) +} + +func TestConfAddOpts(t *testing.T) { + c := Config{original: "hi"} + c2 := c.AddOptions(WithOriginalSlug("Hello, my dear"), WithPrefix("yo")) + assert.Equal(t, "", c.prefix) + assert.Equal(t, "hi", c.original) + assert.Equal(t, "hello-my-dear", c2.original) + assert.Equal(t, "yo", c2.prefix) } diff --git a/examples_test.go b/examples_test.go new file mode 100644 index 0000000..c929985 --- /dev/null +++ b/examples_test.go @@ -0,0 +1,60 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +// Define a global Config. +func NominoConfig() nomino.Config { + return nomino.NewConfig( + nomino.WithSeparator("-"), + nomino.WithPrefix("upload"), + nomino.WithGenerator(nomino.UUID( + nomino.UUIDv7, + )), + ) +} + +// HandleImgUploads generates new filenames for images with a png extension. +func HandleImgUploads(orig string) string { + // Here, we use nomino.Make function to generate the filename. + // We use our global config, and add in a few extra Options specific to this. + newName, _ := nomino.Make( + NominoConfig(), + nomino.WithExtension("png"), + nomino.WithOriginalSlug(orig), + ) + + return newName +} + +// HandleVidUploads generates a new filename for videos. +// We ignore the original filename and use a timestamp for the generated part +// with a webm extension. +func HandleVidUploads() string { + // Because we're using a different Generator, we chose to use the Make method on the Generator. + // We add in additional Options with the `AddOptions` method on the `Config` + newName, _ := nomino.Timestamp(nomino.TimestampUTC()). + MakeWithConfig(NominoConfig().AddOptions( + nomino.WithExtension("webm"), + )) + return newName +} + +// This example shows how to use nomino. +func Example() { + // Pretend we have an image upload + filename := "George" + uploadImgName := HandleImgUploads(filename) + + // Upload to storage + fmt.Println(uploadImgName) + + // New Video Upload + uploadVidName := HandleVidUploads() + + // Upload to storage + fmt.Println(uploadVidName) +} diff --git a/gen_file.go b/gen_file.go new file mode 100644 index 0000000..c701f11 --- /dev/null +++ b/gen_file.go @@ -0,0 +1,75 @@ +package nomino + +import ( + "crypto" + "encoding/hex" + "errors" + "hash" + + "github.com/gosimple/slug" +) + +// ErrMissingOriginal is the error returned by [Slug] if there is no filename. +var ErrMissingOriginal = errors.New("missing original filename") + +func getOriginal(c *Config) (string, error) { + if c.original == "" { + return "", ErrMissingOriginal + } + name := c.original + c.original = "" + return name, nil +} + +// Slug generates a name from the original filename. +// When this is used, the original filename will be removed from the final filename. +// If a language is specified, that may affect the resulting slug. +func Slug(lang ...string) Generator { + ret := slug.Make + if len(lang) > 0 { + ret = func(in string) string { + return slug.MakeLang(in, lang[0]) + } + } + return func(c *Config) (string, error) { + name, err := getOriginal(c) + return ret(name), err + } +} + +// HashingFunc is a function that generates a [hash.Hash]. +type HashingFunc func() hash.Hash + +// New allows [HashingFunc] to be used as a [Hasher]. +func (hf HashingFunc) New() hash.Hash { + return hf() +} + +// Hasher is a type returns a [hash.Hash]. +// All [crypto.Hash] may be used. +type Hasher interface { + New() hash.Hash +} + +// ErrInvalidHash is returned by the [Hash] [Generator] when an invalid [Hasher] is passed. +var ErrInvalidHash = errors.New("invalid hash type") + +// Hash generates a name from a hash of the filename. +// When this is used, the original filename will be removed from the final filename. +func Hash(h Hasher) Generator { + if h == nil { + h = crypto.MD5 + } + return func(c *Config) (string, error) { + if h == crypto.MD5SHA1 { + return "", ErrInvalidHash + } + name, err := getOriginal(c) + if err != nil { + return "", err + } + hs := h.New() + hs.Write([]byte(name)) + return hex.EncodeToString(hs.Sum(nil)), nil + } +} diff --git a/gen_file_examples_test.go b/gen_file_examples_test.go new file mode 100644 index 0000000..76f3260 --- /dev/null +++ b/gen_file_examples_test.go @@ -0,0 +1,56 @@ +package nomino_test + +import ( + "crypto" + "crypto/hmac" + "fmt" + "hash" + + "codeberg.org/danjones000/nomino" +) + +func ExampleSlug() { + str, _ := nomino.Slug().Make(nomino.WithOriginal("My name is Jimmy")) + fmt.Println(str) + + // Output: my-name-is-jimmy.txt +} + +func ExampleSlug_withLang() { + str, _ := nomino.Slug("de"). + Make(nomino.WithOriginal("Diese & Dass")) + fmt.Println(str) + + // Output: diese-und-dass.txt +} + +func ExampleHash_mD5() { + str, _ := nomino.Hash(crypto.MD5). + Make(nomino.WithOriginal("foobar")) + fmt.Println(str) + // Output: 3858f62230ac3c915f300c664312c63f.txt +} + +func ExampleHash_sHA1() { + str, _ := nomino.Hash(crypto.SHA1). + Make(nomino.WithOriginal("foobar")) + fmt.Println(str) + // Output: 8843d7f92416211de9ebb963ff4ce28125932878.txt +} + +func ExampleHash_sHA256() { + str, _ := nomino.Hash(crypto.SHA256). + Make(nomino.WithOriginal("foobar")) + fmt.Println(str) + // Output: c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2.txt +} + +func ExampleHashingFunc_hMAC() { + var hasher nomino.HashingFunc = func() hash.Hash { + return hmac.New(crypto.SHA1.New, []byte("hello")) + } + g := nomino.Hash(hasher) + str, _ := g.Make(nomino.WithOriginal("foobar")) + fmt.Println(str) + // Output: 85f767c284c80a3a59a9635194321d20dd90f31b.txt +} diff --git a/gen_file_test.go b/gen_file_test.go new file mode 100644 index 0000000..cd0a8bf --- /dev/null +++ b/gen_file_test.go @@ -0,0 +1,37 @@ +package nomino + +import ( + "crypto" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSlugMissingFilename(t *testing.T) { + conf := NewConfig(WithGenerator(Slug())) + st, err := conf.generator(&conf) + assert.Zero(t, st) + assert.ErrorIs(t, err, ErrMissingOriginal) +} + +func TestSlugRemovesOriginal(t *testing.T) { + conf := NewConfig(WithGenerator(Slug()), WithOriginal("Hello, World")) + st, err := conf.generator(&conf) + assert.Zero(t, conf.original) + assert.Equal(t, "hello-world", st) + assert.NoError(t, err) +} + +func TestHashBadHash(t *testing.T) { + conf := NewConfig(WithOriginal("foobar"), WithGenerator(Hash(crypto.MD5SHA1))) + st, err := conf.generator(&conf) + assert.Equal(t, "", st) + assert.ErrorIs(t, err, ErrInvalidHash) +} + +func TestHashMissingOriginal(t *testing.T) { + conf := NewConfig(WithGenerator(Hash(nil))) + st, err := conf.generator(&conf) + assert.Equal(t, "", st) + assert.ErrorIs(t, err, ErrMissingOriginal) +} diff --git a/gen_int.go b/gen_int.go new file mode 100644 index 0000000..bb57261 --- /dev/null +++ b/gen_int.go @@ -0,0 +1,55 @@ +package nomino + +import ( + "fmt" + "strconv" +) + +type incConf struct { + start int + step int + cb func(int) string +} + +// IncrementalOption sets an option for the [Incremental] [Generator]. +type IncrementalOption func(c *incConf) + +// Incremental generates a name that is a series of integers. +// By default it begins at 0 and increments by 1 each time. +func Incremental(opts ...IncrementalOption) Generator { + c := incConf{step: 1, cb: strconv.Itoa} + for _, opt := range opts { + opt(&c) + } + + next := c.start + return func(*Config) (string, error) { + out := c.cb(next) + next += c.step + return out, nil + } +} + +// IncrementalStart sets the starting integer for [Incremental]. +func IncrementalStart(start int) IncrementalOption { + return func(c *incConf) { + c.start = start + } +} + +// IncrementalStepsets the step by which [Incremental] increases with each invocation. +func IncrementalStep(step int) IncrementalOption { + return func(c *incConf) { + c.step = step + } +} + +// IncrementalFormatsets the format for the number generated by [Incremental]. +// It will be formatted with Printf. This is mostly likely useful with a format like "%02d". +func IncrementalFormat(format string) IncrementalOption { + return func(c *incConf) { + c.cb = func(i int) string { + return fmt.Sprintf(format, i) + } + } +} diff --git a/gen_int_examples_test.go b/gen_int_examples_test.go new file mode 100644 index 0000000..f3e61c7 --- /dev/null +++ b/gen_int_examples_test.go @@ -0,0 +1,121 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleIncremental() { + conf := nomino.NewConfig( + nomino.WithPrefix("foo"), + nomino.WithGenerator(nomino.Incremental()), + ) + + str, _ := nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + // Output: + // foo_0.txt + // foo_1.txt + // foo_2.txt +} + +func ExampleIncrementalStart() { + conf := nomino.NewConfig( + nomino.WithPrefix("foo"), + nomino.WithGenerator(nomino.Incremental( + nomino.IncrementalStart(42), + )), + ) + + str, _ := nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + // Output: + // foo_42.txt + // foo_43.txt + // foo_44.txt +} + +func ExampleIncrementalStep() { + conf := nomino.NewConfig( + nomino.WithPrefix("foo"), + nomino.WithGenerator(nomino.Incremental( + nomino.IncrementalStep(2), + )), + ) + + str, _ := nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + // Output: + // foo_0.txt + // foo_2.txt + // foo_4.txt +} + +func ExampleIncremental_withStartAndStep() { + conf := nomino.NewConfig( + nomino.WithPrefix("foo"), + nomino.WithGenerator(nomino.Incremental( + nomino.IncrementalStart(42), + nomino.IncrementalStep(2), + )), + ) + + str, _ := nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + // Output: + // foo_42.txt + // foo_44.txt + // foo_46.txt +} + +func ExampleIncrementalFormat() { + conf := nomino.NewConfig( + nomino.WithPrefix("foo"), + nomino.WithGenerator(nomino.Incremental( + nomino.IncrementalFormat("%03d"), + )), + ) + + str, _ := nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + str, _ = nomino.Make(conf) + fmt.Println(str) + + // Output: + // foo_000.txt + // foo_001.txt + // foo_002.txt +} diff --git a/gen_rand.go b/gen_rand.go new file mode 100644 index 0000000..69f0487 --- /dev/null +++ b/gen_rand.go @@ -0,0 +1,91 @@ +package nomino + +import ( + "crypto/rand" + "strings" + + "github.com/deatil/go-encoding/base62" + "github.com/google/uuid" +) + +// UUIDer is an interface for generating UUIDs, by the [UUID] [Generator]. +// It is recommended that you use either the [UUIDv4] or [UUIDv7] variables. +type UUIDer interface { + UUID() (uuid.UUID, error) +} + +// UUIDFunc is a function that generates a UUID. +type UUIDFunc func() (uuid.UUID, error) + +// UUID allows [UUIDFunc] to be used as a [UUIDer]. +func (u UUIDFunc) UUID() (uuid.UUID, error) { + return u() +} + +var ( + // UUIDv1. You probably don't want to use this. It is included for completeness sake. + UUIDv1 = UUIDFunc(uuid.NewUUID) + // UUIDv4 is the default. + UUIDv4 = UUIDFunc(uuid.NewRandom) + // UUIDv6 is primarily a replacement for UUIDv1. You probably should use 4 or 7. + UUIDv6 = UUIDFunc(uuid.NewV6) + // UUIDv7 should be used if you want it sortable by time. + UUIDv7 = UUIDFunc(uuid.NewV7) +) + +// UUID generates a UUID. If nil is passed as an argument, +// a UUIDv4 is generated. +func UUID(u UUIDer) Generator { + if u == nil { + u = UUIDv4 + } + return func(*Config) (string, error) { + uu, err := u.UUID() + if err != nil { + return "", err + } + return uu.String(), nil + } +} + +type randConf struct { + length int +} + +// RandomOption is an option for the [Random] [Generator]. +type RandomOption func(*randConf) + +// RandomLength controls the length of the string generated by [Random]. +func RandomLength(length int) RandomOption { + return func(c *randConf) { + c.length = length + } +} + +func getRandomBytes(l int) []byte { + key := make([]byte, l) + _, _ = rand.Read(key) + e := base62.StdEncoding.Encode(key) + return e[:l] +} + +func fillBuffer(buff *strings.Builder, length int) { + for buff.Len() < length { + buff.Write(getRandomBytes(length - buff.Len())) + } +} + +// Random generates a random string containing the characters A-Za-z0-9. +// By default, it will be eight characters long. +func Random(opts ...RandomOption) Generator { + c := randConf{8} + for _, opt := range opts { + opt(&c) + } + return func(*Config) (string, error) { + var buff strings.Builder + buff.Grow(c.length) + fillBuffer(&buff, c.length) + return buff.String(), nil + } +} diff --git a/gen_rand_examples_test.go b/gen_rand_examples_test.go new file mode 100644 index 0000000..29737fa --- /dev/null +++ b/gen_rand_examples_test.go @@ -0,0 +1,40 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleUUID() { + gen := nomino.UUID(nil) + + str, _ := gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) +} + +func ExampleUUID_v7() { + gen := nomino.UUID(nomino.UUIDv7) + + str, _ := gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) +} + +func ExampleRandom() { + str, _ := nomino.Random().Make() + fmt.Println(str) +} + +func ExampleRandomLength() { + str, _ := nomino.Random(nomino.RandomLength(32)).Make() + fmt.Println(str) +} diff --git a/gen_rand_test.go b/gen_rand_test.go new file mode 100644 index 0000000..193ac51 --- /dev/null +++ b/gen_rand_test.go @@ -0,0 +1,41 @@ +package nomino + +import ( + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +func TestUUID(t *testing.T) { + st, err := UUID(nil)(nil) + assert.NoError(t, err) + _, parseErr := uuid.Parse(st) + assert.NoError(t, parseErr) +} + +type badRead struct{} + +func (badRead) Read([]byte) (int, error) { + return 0, errTest +} + +func TestUUIDFail(t *testing.T) { + uuid.SetRand(badRead{}) + defer uuid.SetRand(nil) + + _, err := UUID(nil)(nil) + assert.ErrorIs(t, err, errTest) +} + +func TestRand(t *testing.T) { + st, err := Random()(nil) + assert.NoError(t, err) + assert.Len(t, st, 8) +} + +func TestRandLen(t *testing.T) { + st, err := Random(RandomLength(32))(nil) + assert.NoError(t, err) + assert.Len(t, st, 32) +} diff --git a/gen_ts.go b/gen_ts.go new file mode 100644 index 0000000..62fe703 --- /dev/null +++ b/gen_ts.go @@ -0,0 +1,59 @@ +package nomino + +import "time" + +// FileTimestamp is the default format for [Timestamp]. +const FileTimestamp string = "2006-01-02T15-04-05-0700" + +// FileTimestampNoTZ is the default format when using the [TimestampUTC] [TimestampOption]. +const FileTimestampNoTZ string = "2006-01-02T15-04-05" + +type timestampConf struct { + format string + ts time.Time + utc bool +} + +// TimestampOption provides options for the [Timestamp] [Generator]. +type TimestampOption func(c *timestampConf) + +// Timestamp generates a a date and time. By default, it uses the current time, and will. +// be formatted accourding to FileTimestamp. +func Timestamp(opts ...TimestampOption) Generator { + c := timestampConf{format: FileTimestamp, ts: time.Now()} + for _, opt := range opts { + opt(&c) + } + + if c.utc { + c.ts = c.ts.UTC() + } + + return func(*Config) (string, error) { + return c.ts.Format(c.format), nil + } +} + +// TimestampFormat sets the format for the generated name. +// Consult [time.Time.Format] for details on the format. +func TimestampFormat(format string) TimestampOption { + return func(c *timestampConf) { + c.format = format + } +} + +// TimestampTime sets the time for the generated name. +// By default, [Timestamp] uses the current time. +func TimestampTime(t time.Time) TimestampOption { + return func(c *timestampConf) { + c.ts = t + } +} + +// TimestampUTC uses the time in UTC, while also stripping the timezone from the format. +func TimestampUTC() TimestampOption { + return func(c *timestampConf) { + c.utc = true + c.format = FileTimestampNoTZ + } +} diff --git a/gen_ts_examples_test.go b/gen_ts_examples_test.go new file mode 100644 index 0000000..26f8a91 --- /dev/null +++ b/gen_ts_examples_test.go @@ -0,0 +1,41 @@ +package nomino_test + +import ( + "fmt" + "time" + + "codeberg.org/danjones000/nomino" +) + +func ExampleTimestamp() { + gen := nomino.Timestamp() + s, _ := gen.Make() + fmt.Println(s) +} + +func ExampleTimestampTime() { + tz, _ := time.LoadLocation("America/New_York") + ts := time.Date(2009, time.January, 20, 12, 5, 0, 0, tz) + gen := nomino.Timestamp(nomino.TimestampTime(ts)) + s, _ := gen.Make() + fmt.Println(s) + // Output: 2009-01-20T12-05-00-0500.txt +} + +func ExampleTimestampFormat() { + tz, _ := time.LoadLocation("America/New_York") + ts := time.Date(2009, time.January, 20, 12, 5, 0, 0, tz) + gen := nomino.Timestamp(nomino.TimestampTime(ts), nomino.TimestampFormat("2006#01#02<>15|04|05-0700")) + s, _ := gen.Make() + fmt.Println(s) + // Output: 2009#01#20<>12|05|00-0500.txt +} + +func ExampleTimestampUTC() { + tz, _ := time.LoadLocation("America/New_York") + ts := time.Date(2009, time.January, 20, 12, 5, 0, 0, tz) + gen := nomino.Timestamp(nomino.TimestampTime(ts), nomino.TimestampUTC()) + s, _ := gen.Make() + fmt.Println(s) + // Output: 2009-01-20T17-05-00.txt +} diff --git a/gen_ts_test.go b/gen_ts_test.go new file mode 100644 index 0000000..2997702 --- /dev/null +++ b/gen_ts_test.go @@ -0,0 +1,15 @@ +package nomino + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTimestamp(t *testing.T) { + n := time.Now() + st, err := Timestamp()(nil) + assert.NoError(t, err) + assert.Equal(t, n.Format(FileTimestamp), st) +} diff --git a/generators.go b/generators.go index fdbdabd..28b666b 100644 --- a/generators.go +++ b/generators.go @@ -2,32 +2,41 @@ package nomino import ( "errors" - "time" - - "github.com/google/uuid" + "math/rand" ) // 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) +type Generator func(conf *Config) (string, error) -// WithGenerator sets the specified generator +// Make allows you to generate a new string directly from a [Generator]. +func (g Generator) Make(opts ...Option) (string, error) { + return g.MakeWithConfig(NewConfig(opts...)) +} + +// MakeWithConfig allows you to generate a new string directly from a [Generator] +// with a pre-existing Config. +func (g Generator) MakeWithConfig(c Config) (string, error) { + return Make(c.AddOptions(WithGenerator(g))) +} + +// 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. +// ErrMissingGenerators is returned by a multi-generator if a [Generator] isn't supplied. var ErrMissingGenerators = errors.New("no generators supplied") -func missingGen() (string, error) { +func missingGen(*Config) (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. +// If none are passed, the generator will always return [ErrMissingGenerators]. func MultiGeneratorInOrder(gens ...Generator) Generator { if len(gens) == 0 { return missingGen @@ -38,62 +47,27 @@ func MultiGeneratorInOrder(gens ...Generator) Generator { } var idx int - return func() (string, error) { - st, err := gens[idx]() + return func(c *Config) (string, error) { + st, err := gens[idx](c) idx = (idx + 1) % len(gens) return st, err } } -func uuidGen() (string, error) { - u, err := uuid.NewRandom() - if err != nil { - return "", err +// MultiGeneratorRandomOrder allows the use of multiple generators. Each new invokation will use one of the generators randomly. +// If none are passed, the generator will always return [ErrMissingGenerators]. +func MultiGeneratorRandomOrder(gens ...Generator) Generator { + if len(gens) == 0 { + return missingGen } - return u.String(), nil -} -// UUID generates a UUIDv4. -func UUID() Generator { - return uuidGen -} + if len(gens) == 1 { + return gens[0] + } -// 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 + return func(c *Config) (string, error) { + //nolint:gosec // This is not security sensitive, so a weak number generator is fine. + idx := rand.Int() % len(gens) + return gens[idx](c) } } - -// 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_examples_test.go b/generators_examples_test.go new file mode 100644 index 0000000..b552089 --- /dev/null +++ b/generators_examples_test.go @@ -0,0 +1,79 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleWithGenerator_customGenerator() { + var gen nomino.Generator = func(*nomino.Config) (string, error) { + return "hello", nil + } + + str, _ := gen.Make() + fmt.Println(str) + + str, _ = gen.Make(nomino.WithoutExtension()) + fmt.Println(str) + + // Output: + // hello.txt + // hello +} + +func ExampleGenerator_Make() { + g := nomino.Incremental() + st, _ := g.Make() + fmt.Println(st) + + st, _ = g.Make(nomino.WithPrefix("foo")) + fmt.Println(st) + + // Output: + // 0.txt + // foo_1.txt +} + +func ExampleMultiGeneratorInOrder() { + gen1 := func(*nomino.Config) (string, error) { + return "bonjour", nil + } + gen2 := func(*nomino.Config) (string, error) { + return "goodbye", nil + } + gen := nomino.MultiGeneratorInOrder(gen1, gen2) + + str, _ := gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) + + // Output: + // bonjour.txt + // goodbye.txt + // bonjour.txt +} + +func ExampleMultiGeneratorRandomOrder() { + gen1 := func(*nomino.Config) (string, error) { + return "guten-tag", nil + } + gen2 := func(*nomino.Config) (string, error) { + return "wiedersehen", nil + } + gen := nomino.MultiGeneratorRandomOrder(gen1, gen2) + + str, _ := gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) + + str, _ = gen.Make() + fmt.Println(str) +} diff --git a/generators_test.go b/generators_test.go index 7282656..4c257ab 100644 --- a/generators_test.go +++ b/generators_test.go @@ -3,105 +3,103 @@ package nomino import ( "errors" "testing" - "time" - "github.com/google/uuid" "github.com/stretchr/testify/assert" ) +const ( + out1 string = "abc" + out2 string = "def" +) + +var ( + outs = []string{out1, out2} + err1 = errors.New("oops") + gen1 Generator = func(*Config) (string, error) { return out1, nil } + gen2 Generator = func(*Config) (string, error) { return out2, nil } + gen3 Generator = func(*Config) (string, error) { return "", err1 } + gens = []Generator{gen1, gen2, gen3} +) + func TestWithGenerator(t *testing.T) { - g := func() (string, error) { return "abc", nil } + g := func(*Config) (string, error) { return out1, nil } var c Config WithGenerator(g)(&c) - st, err := c.generator() + st, err := c.generator(&c) assert.NoError(t, err) - assert.Equal(t, "abc", st) + assert.Equal(t, out1, 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() + g := MultiGeneratorInOrder(gens...) + st, err := g(nil) assert.NoError(t, err) - assert.Equal(t, st1, st) - st, err = g() + assert.Equal(t, out1, st) + st, err = g(nil) assert.NoError(t, err) - assert.Equal(t, st2, st) - st, err = g() + assert.Equal(t, out2, st) + st, err = g(nil) assert.Zero(t, st) - assert.ErrorIs(t, err, er1) - st, err = g() + assert.ErrorIs(t, err, err1) + st, err = g(nil) assert.NoError(t, err) - assert.Equal(t, st1, st) + assert.Equal(t, out1, st) } func TestMultiGeneratorInOrderOne(t *testing.T) { - st1 := "abc" - g1 := func() (string, error) { return st1, nil } + g1 := func(*Config) (string, error) { return out1, nil } g := MultiGeneratorInOrder(g1) - st, err := g() + st, err := g(nil) assert.NoError(t, err) - assert.Equal(t, st1, st) - st, err = g() + assert.Equal(t, out1, st) + st, err = g(nil) assert.NoError(t, err) - assert.Equal(t, st1, st) + assert.Equal(t, out1, st) } func TestMultiGeneratorInOrderMissing(t *testing.T) { g := MultiGeneratorInOrder() - st, err := g() + st, err := g(nil) assert.Zero(t, st) assert.ErrorIs(t, err, ErrMissingGenerators) - st, err = g() + st, err = g(nil) assert.Zero(t, st) assert.ErrorIs(t, err, ErrMissingGenerators) } -func TestUUID(t *testing.T) { - st, err := UUID()() +func TestMultiGeneratorRandomOrder(t *testing.T) { + g := MultiGeneratorRandomOrder(gens...) + for i := 0; i < 4; i++ { + st, err := g(nil) + if err != nil { + assert.Zero(t, st) + assert.ErrorIs(t, err, err1) + } else { + assert.Contains(t, outs, st) + } + } +} + +func TestMultiGeneratorRandomOrderOne(t *testing.T) { + st1 := "abc" + g1 := func(*Config) (string, error) { return st1, nil } + g := MultiGeneratorRandomOrder(g1) + + st, err := g(nil) 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.Equal(t, st1, st) + st, err = g(nil) assert.NoError(t, err) - assert.Equal(t, n.Format(FileTimestamp), st) + assert.Equal(t, st1, 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) +func TestMultiGeneratorRandomOrderMissing(t *testing.T) { + g := MultiGeneratorRandomOrder() + st, err := g(nil) + assert.Zero(t, st) + assert.ErrorIs(t, err, ErrMissingGenerators) + st, err = g(nil) + assert.Zero(t, st) + assert.ErrorIs(t, err, ErrMissingGenerators) } diff --git a/go.mod b/go.mod index de6af60..c733bcf 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,15 @@ module codeberg.org/danjones000/nomino go 1.23.6 require ( + github.com/deatil/go-encoding v1.0.3003 github.com/google/uuid v1.6.0 + github.com/gosimple/slug v1.15.0 github.com/stretchr/testify v1.10.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gosimple/unidecode v1.0.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 index 14c872b..209d634 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ 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/deatil/go-encoding v1.0.3003 h1:2b05UO+5JfVcXcOa8n/X3pm8aC6L6ET0mBZCb1kj3ck= +github.com/deatil/go-encoding v1.0.3003/go.mod h1:lTMMKsG0RRPGZzdW2EPVJCA7HQy4o1ZQKPf5CmVDy2k= 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/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= +github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= +github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= +github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= 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= diff --git a/make.go b/make.go index 3cd537a..2e9d754 100644 --- a/make.go +++ b/make.go @@ -2,14 +2,31 @@ package nomino import "fmt" -// Make generates a random filename. The behavior can be controlled by specifying Options +// 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 the name generator returns an error (generally, it shouldn't), that error will be returned instead. +func Make(conf Config, opts ...Option) (string, error) { + for _, opt := range opts { + opt(&conf) + } + + name, err := conf.generator(&conf) if err != nil { return "", err } + seperateConf(&conf) - return fmt.Sprintf("%s%s%s%s%s", conf.prefix, name, conf.original, conf.suffix, conf.original), nil + return fmt.Sprintf("%s%s%s%s%s", conf.prefix, name, conf.original, conf.suffix, conf.extension), nil +} + +func seperateConf(conf *Config) { + if conf.prefix != "" { + conf.prefix += conf.separator + } + if conf.original != "" { + conf.original = conf.separator + conf.original + } + if conf.suffix != "" { + conf.suffix = conf.separator + conf.suffix + } } diff --git a/make_examples_test.go b/make_examples_test.go new file mode 100644 index 0000000..88b8521 --- /dev/null +++ b/make_examples_test.go @@ -0,0 +1,30 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleMake_basic() { + // Use default config + out, _ := nomino.Make(nomino.NewConfig()) + fmt.Println(out) +} + +func ExampleMake_withExtraOptions() { + gen := nomino.Incremental() + conf := nomino.NewConfig( + nomino.WithGenerator(gen), + nomino.WithPrefix("pre"), + ) + + st, _ := nomino.Make(conf, nomino.WithOriginal("foobar")) + fmt.Println(st) + st, _ = nomino.Make(conf, nomino.WithOriginal("baz")) + fmt.Println(st) + + // Output: + // pre_0_foobar.txt + // pre_1_baz.txt +} diff --git a/make_test.go b/make_test.go index e9cc006..1ebb48e 100644 --- a/make_test.go +++ b/make_test.go @@ -7,17 +7,78 @@ import ( "github.com/stretchr/testify/assert" ) +var errTest = errors.New("sorry") + 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) + genOpt := WithGenerator(func(*Config) (string, error) { return "abc", nil }) + testcases := []struct { + name string + opts []Option + exp string + }{ + {"basic", nil, "abc.txt"}, + {"with prefix", []Option{WithPrefix("foo")}, "foo_abc.txt"}, + {"with suffix", []Option{WithSuffix("bar")}, "abc_bar.txt"}, + {"with original", []Option{WithOriginal("file")}, "abc_file.txt"}, + {"without ext", []Option{WithoutExtension()}, "abc"}, + {"with ext", []Option{WithExtension("xml")}, "abc.xml"}, + {"with sep", []Option{WithOriginal("file"), WithSeparator("---")}, "abc---file.txt"}, + { + "with all", + []Option{ + WithPrefix("pre"), + WithOriginal("file"), + WithSuffix("suff"), + WithExtension("svg"), + WithSeparator("+"), + }, + "pre+abc+file+suff.svg", + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(sub *testing.T) { + opts := testcase.opts + opts = append(opts, genOpt) + + conf := NewConfig(opts...) + st, err := Make(conf) + assert.NoError(t, err) + assert.Equal(t, testcase.exp, st) + }) + } } func TestMakeErr(t *testing.T) { - retErr := errors.New("oops") - conf := NewConfig(WithGenerator(func() (string, error) { return "foobar", retErr })) + conf := NewConfig(WithGenerator(func(*Config) (string, error) { return "foobar", errTest })) st, err := Make(conf) assert.Zero(t, st) - assert.ErrorIs(t, err, retErr) + assert.ErrorIs(t, err, errTest) +} + +func TestMakeDoesntChangeConf(t *testing.T) { + gen := func(c *Config) (string, error) { + c.original = "" + return "foo", nil + } + conf := NewConfig(WithGenerator(gen), WithOriginal("foobar")) + st, err := Make(conf) + assert.Equal(t, "foobar", conf.original) + assert.Equal(t, "foo.txt", st) + assert.NoError(t, err) +} + +func TestMakeOptsDoesntChangeConf(t *testing.T) { + gen := Incremental() + conf := NewConfig(WithGenerator(gen), WithPrefix("pre")) + + st, err := Make(conf, WithOriginal("foobar")) + assert.Equal(t, "", conf.original) + assert.Equal(t, "pre_0_foobar.txt", st) + assert.NoError(t, err) + + st, err = Make(conf, WithOriginal("baz")) + assert.Equal(t, "", conf.original) + assert.Equal(t, "pre_1_baz.txt", st) + assert.NoError(t, err) } diff --git a/nomino.go b/nomino.go new file mode 100644 index 0000000..185fcc3 --- /dev/null +++ b/nomino.go @@ -0,0 +1,7 @@ +// Package nomino is a utility that allows us to generate random filenames. +// +// There are two main methods of using nomino. +// +// 1. Using the [Make] function. +// 2. Creating a generator, and using its [Generator.Make] method. +package nomino diff --git a/options.go b/options.go index fa6c81c..3550b02 100644 --- a/options.go +++ b/options.go @@ -1,33 +1,53 @@ package nomino -import "strings" +import ( + "strings" -// Option sets configuration parameters for Config. + "github.com/gosimple/slug" +) + +// 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 + c.original = o + } +} + +// WithOriginal sets the original filename as a slug. +// This should not be used with the [Slug] [Generator] (as it would be redundant). +func WithOriginalSlug(o string) Option { + return func(c *Config) { + c.original = slug.Make(o) + } +} + +// WithOriginal sets the original filename as a slug, taking the language into account. +// This should not be used with the [Slug] [Generator] (as it would be redundant). +func WithOriginalSlugLang(o, lang string) Option { + return func(c *Config) { + c.original = slug.MakeLang(o, lang) } } // WithPrefix sets a prefix for the generated name. func WithPrefix(p string) Option { return func(c *Config) { - c.prefix = p + "_" + 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 + c.suffix = s } } -// WithoutExtension sets no extension for the generated filename. By default, it will be txt +// WithoutExtension sets no extension for the generated filename. By default, it will be txt. func WithoutExtension() Option { return func(c *Config) { c.extension = "" @@ -40,3 +60,10 @@ func WithExtension(ext string) Option { c.extension = "." + strings.TrimPrefix(ext, ".") } } + +// WithSeparator sets the separator for the generated filename. +func WithSeparator(sep string) Option { + return func(c *Config) { + c.separator = sep + } +} diff --git a/options_examples_test.go b/options_examples_test.go new file mode 100644 index 0000000..8263fe9 --- /dev/null +++ b/options_examples_test.go @@ -0,0 +1,103 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleWithExtension() { + st, _ := nomino.Make(nomino.NewConfig( + nomino.WithExtension("xml"), + nomino.WithGenerator(nomino.Incremental()), + )) + + fmt.Println(st) + // Output: 0.xml +} + +func ExampleWithoutExtension() { + st, _ := nomino.Make(nomino.NewConfig( + nomino.WithoutExtension(), + nomino.WithGenerator(nomino.Incremental()), + )) + + fmt.Println(st) + // Output: 0 +} + +func ExampleWithPrefix() { + conf := nomino.NewConfig( + nomino.WithPrefix("pref"), + nomino.WithGenerator(nomino.Incremental()), + ) + st, _ := nomino.Make(conf) + fmt.Println(st) + + st, _ = nomino.Make(conf) + fmt.Println(st) + // Output: + // pref_0.txt + // pref_1.txt +} + +func ExampleWithSeparator() { + conf := nomino.NewConfig( + nomino.WithPrefix("pref"), + nomino.WithSeparator("---"), + nomino.WithGenerator(nomino.Incremental()), + ) + st, _ := nomino.Make(conf) + fmt.Println(st) + + st, _ = nomino.Make(conf) + fmt.Println(st) + // Output: + // pref---0.txt + // pref---1.txt +} + +func ExampleWithSuffix() { + conf := nomino.NewConfig( + nomino.WithSuffix("suff"), + nomino.WithGenerator(nomino.Incremental()), + ) + st, _ := nomino.Make(conf) + fmt.Println(st) + + st, _ = nomino.Make(conf) + fmt.Println(st) + // Output: + // 0_suff.txt + // 1_suff.txt +} + +func ExampleWithOriginal() { + st, _ := nomino.Make(nomino.NewConfig( + nomino.WithOriginal("Hello, World"), + nomino.WithGenerator(nomino.Incremental()), + )) + + fmt.Println(st) + // Output: 0_Hello, World.txt +} + +func ExampleWithOriginalSlug() { + st, _ := nomino.Make(nomino.NewConfig( + nomino.WithOriginalSlug("Hello, World"), + nomino.WithGenerator(nomino.Incremental()), + )) + + fmt.Println(st) + // Output: 0_hello-world.txt +} + +func ExampleWithOriginalSlugLang() { + st, _ := nomino.Make(nomino.NewConfig( + nomino.WithOriginalSlugLang("Diese & Dass", "de"), + nomino.WithGenerator(nomino.Incremental()), + )) + + fmt.Println(st) + // Output: 0_diese-und-dass.txt +} diff --git a/options_test.go b/options_test.go deleted file mode 100644 index 12a2bb5..0000000 --- a/options_test.go +++ /dev/null @@ -1,41 +0,0 @@ -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) -}