Compare commits

..

43 commits

Author SHA1 Message Date
4e3132a5cf 🔀 Merge branch 'release/1.0.0' into stable 2025-03-31 15:56:49 -05:00
2561fe3cff 📝 Update CHANGELOG 2025-03-31 15:56:34 -05:00
17961ddd41 📝 Add one more link to README 2025-03-31 15:52:10 -05:00
cdb504c1a3 📝 Fill out README 2025-03-31 15:47:36 -05:00
70c74c2b03 💡 Fix a few go doc comments 2025-03-31 15:46:48 -05:00
81e04ea319 🔀 Merge tag 'v0.5.3' into develop
🔖 Bump version just to publish example
2025-03-31 12:18:20 -05:00
b6994e73a8 🔀 Merge branch 'release/0.5.3' into stable 2025-03-31 12:17:45 -05:00
c5a9f09166 💡 Improve go doc comments with internal links 2025-03-31 12:10:05 -05:00
cdf12a767c 🔀 Merge tag 'v0.5.2' into develop
🔖 Bump version to publish documentation
2025-03-23 10:56:09 -05:00
5cbd63a227 🔀 Merge branch 'release/0.5.2' into stable 2025-03-23 10:55:05 -05:00
ac3d1a5565 💡 Add package level doc comment 2025-03-23 10:53:49 -05:00
c32a15f4a1 💡 Add some helpful comments in example 2025-03-23 10:52:54 -05:00
4b9bffb1a6 🔀 Merge tag 'v0.5.1' into develop
🔖 Bump version just to publish example
2025-03-19 18:47:40 -05:00
4b1312e293 🔀 Merge branch 'release/0.5.1' into stable 2025-03-19 18:46:42 -05:00
797c616447 📝 Add package level example 2025-03-19 18:45:36 -05:00
8f02956ecd 🚨 A bunch of small improvements from linter 2025-03-19 18:05:16 -05:00
480e36763f 🛠 Replace all linting with golangci-lint 2025-03-19 17:48:23 -05:00
8072ae267a ✏️ Extra backtick 2025-03-19 16:16:00 -05:00
4faf3a5d2f 🔀 Merge tag 'v0.5.0' into develop
🔖 v0.5.0:  More UUIDs
2025-03-19 14:39:59 -05:00
c17f23595c 🔀 Merge branch 'release/0.5.0' into stable 2025-03-19 14:38:34 -05:00
f20e737f2b 📝 Update CHANGELOG 2025-03-19 14:37:40 -05:00
1abfaa44d1 Config.AddOptions and Generator.MakeWithConfig 2025-03-18 09:22:30 -05:00
d7b14f804c 📝 Redo ts examples to use gen.Make 2025-03-16 15:56:31 -05:00
fee2e3cc2f ♻️ Modify UUID to allow for other versions. 2025-03-16 12:38:36 -05:00
f121b7dbce 🔀 Merge tag 'v0.4.0' into develop
🔖 0.4.0 Release is close to 1.0
2025-03-15 21:08:44 -05:00
55038ea295 🔀 Merge branch 'release/0.4.0' into stable 2025-03-15 21:06:55 -05:00
4d6cd82b74 📝 Update CHANGELOG 2025-03-15 21:06:16 -05:00
7c016df30f Add MultiGeneratorRandomOrder 2025-03-15 21:00:54 -05:00
2440f55563 ♻️ Replace HashType with Hasher
Compatible with crypto.Hash
2025-03-15 20:09:38 -05:00
1008a064d0 Add Make method to Generator 2025-03-15 16:24:37 -05:00
1677a692d1 Add Random Generator 2025-03-15 15:36:43 -05:00
5c1132e414 💡 Fix some doc blocks 2025-03-15 15:35:40 -05:00
10eb3f2491 🔀 Merge tag 'v0.3.0' into develop
🔖 Lots of Generator changes
2025-03-14 21:58:22 -05:00
fd5413ab21 🔀 Merge branch 'release/0.3.0' into stable 2025-03-14 21:57:27 -05:00
46a40031dd 📝 Update CHANGELOG 2025-03-14 21:56:12 -05:00
db2f12522d Add extra Options to Make 2025-03-14 21:46:30 -05:00
9402d80704 Examples for everything 2025-03-14 21:22:29 -05:00
bd448eb5db 🚚 Move remaining examples to nomino_test 2025-03-14 20:12:22 -05:00
1d0f2238b3 🚚 Move Slug/Hash Generators to own file 2025-03-14 19:50:24 -05:00
586fe4f1de ♻️ Refactor Incremental Generator to single function with options 2025-03-14 19:03:22 -05:00
63d538d889 🚚 Rename TS* to Timestamp 2025-03-14 18:09:07 -05:00
61a5199699 ♻️ Refactor Timestamp Generator to single function with options 2025-03-14 17:39:42 -05:00
72791d4fac 🔀 Merge tag 'v0.2.1' into develop
🔖 Add Hash Generator
2025-03-14 15:15:25 -05:00
31 changed files with 1186 additions and 609 deletions

39
.golangci.yaml Normal file
View file

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

View file

@ -1,54 +1,111 @@
# Changelog
### [0.2.1] - 2025-03-14
### [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
- Add Hash Generator
- ✨ 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
- 🛠 Added a task to serve docs
- 🛠 Added tasts to check code complexity
#### Miscellaneous
- Fixed some `go vet` complaints
- 💚 Fixed some `go vet` complaints
### [0.2.0] - 2025-03-14
### [0.2.0] - 2025-03-14 - ✨ New `Generator`s
#### Features
- Add `IncrementalFormat`* Generators
- Add `Slug`* Generators
- Add `WithOriginalSlug`* Options
- Change signature of `Generator` function
- 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
## [0.0.3] - 2025-03-11 - ✨ `WithSeparator`
### Features
- Added `WithSeparator` to allow for different separators between the parts of the generated filename.
- Added `WithSeparator` to allow for different separators between the parts of the generated filename.
## [0.0.2] - 2025-03-11
## [0.0.2] - 2025-03-11 - 🐛 Bugfix
Bugfix release
### Fixes
- Extension being ignored. Original included twice.
- 🐛 Extension being ignored. Original included twice.
## [0.0.1] - 2025-03-10
## [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!

View file

@ -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, check [official documentation](https://pkg.go.dev/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

View file

@ -5,7 +5,7 @@ tasks:
cmds:
- task: fmt
- task: test
- task: build
- task: lint
fmt:
desc: Format go code
@ -22,67 +22,16 @@ tasks:
cmds:
- go generate ./...
vet:
desc: Vet go code
sources:
- '**/*.go'
deps: [gen]
cmds:
- go vet ./...
critic:
desc: Critique go code
sources:
- '**/*.go'
cmds:
- gocritic check ./...
staticcheck:
desc: Static check go code
sources:
- '**/*.go'
cmds:
- staticcheck ./...
cog-complex:
desc: Calculate cognitive complexity
sources:
- '**/*.go'
cmds:
- gocognit -over 5 .
cyc-complex:
desc: Calculate cyclomatic complexity
sources:
- '**/*.go'
cmds:
- gocyclo -over 5 .
complex:
desc: Calculate complexities
deps:
- cog-complex
- cyc-complex
vuln:
desc: Check for vulnerabilities
sources:
- '**/*.go'
cmds:
- govulncheck ./...
lint:
desc: Do static analysis
deps:
- vet
- critic
- staticcheck
- complex
- vuln
sources:
- '**/*.go'
cmds:
- golangci-lint run
test:
desc: Run unit tests
deps: [fmt, vet, gen]
deps: [fmt]
sources:
- '**/*.go'
generates:

View file

@ -1,5 +1,6 @@
package nomino
// Config controls how the generatred filename is created.
type Config struct {
original string
prefix string
@ -9,14 +10,25 @@ type Config struct {
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",
separator: "_",
generator: uuidGen,
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
}

View file

@ -20,3 +20,12 @@ func TestNewConfWithOpts(t *testing.T) {
assert.Equal(t, "", c.extension)
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)
}

60
examples_test.go Normal file
View file

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

75
gen_file.go Normal file
View file

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

56
gen_file_examples_test.go Normal file
View file

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

37
gen_file_test.go Normal file
View file

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

55
gen_int.go Normal file
View file

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

121
gen_int_examples_test.go Normal file
View file

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

91
gen_rand.go Normal file
View file

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

40
gen_rand_examples_test.go Normal file
View file

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

41
gen_rand_test.go Normal file
View file

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

59
gen_ts.go Normal file
View file

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

41
gen_ts_examples_test.go Normal file
View file

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

15
gen_ts_test.go Normal file
View file

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

View file

@ -1,17 +1,8 @@
package nomino
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"errors"
"fmt"
"hash"
"strconv"
"time"
"github.com/google/uuid"
"github.com/gosimple/slug"
"math/rand"
)
// Generator is a function that returns the "random" portion of the returned filename.
@ -19,14 +10,25 @@ import (
// for example.
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(*Config) (string, error) {
@ -34,7 +36,7 @@ func missingGen(*Config) (string, error) {
}
// 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
@ -52,181 +54,20 @@ func MultiGeneratorInOrder(gens ...Generator) Generator {
}
}
func uuidGen(*Config) (string, error) {
u, err := uuid.NewRandom()
if err != nil {
return "", err
}
return u.String(), nil
// 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
}
// 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(*Config) (string, error) {
return t.Format(f), nil
}
}
// FileTimestampNoTZ 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)
}
// Incremental generates a name that is a series of integers starting at 0
func Incremental() Generator {
return IncrementalWithStartAndStep(0, 1)
}
// IncrementalFormat generates a name that is a series of integers starting at 0, formatted with Printf
// This is mostly likely useful with a format like "%02d"
func IncrementalFormat(format string) Generator {
return IncrementalFormatWithStartAndStep(0, 1, format)
}
// IncrementalWithStart generates a name that is a series of integers starting at the specified number
func IncrementalWithStart(start int) Generator {
return IncrementalWithStartAndStep(start, 1)
}
// IncrementalFormatWithStart generates a name that is a series of integers starting at the specified number, formatted with Printf
func IncrementalFormatWithStart(start int, format string) Generator {
return IncrementalFormatWithStartAndStep(start, 1, format)
}
// IncrementalWithStep generates a name that is a series of integers, starting at 0, and increasing the specified number each time
func IncrementalWithStep(step int) Generator {
return IncrementalWithStartAndStep(0, step)
}
// IncrementalFormatWithStep generates a name that is a series of integers, starting at 0, and increasing the specified number each time,
// formatted with Printf
func IncrementalFormatWithStep(step int, format string) Generator {
return IncrementalFormatWithStartAndStep(0, step, format)
}
func incrementalHelper(start, step int, cb func(int) string) Generator {
next := start
return func(*Config) (string, error) {
out := cb(next)
next += step
return out, nil
}
}
// InrementalWithStartAndStep generates a name that is a series of integers, starting at the specified number,
// and increasing the specified step each time
func IncrementalWithStartAndStep(start, step int) Generator {
return incrementalHelper(start, step, strconv.Itoa)
}
// IncrementalFormatWithStartAndStep generates a name that is a series of integers, starting at the specified number,
// and increasing the specified step each time, formatted with Printf
func IncrementalFormatWithStartAndStep(start, step int, format string) Generator {
return incrementalHelper(start, step, func(i int) string {
return fmt.Sprintf(format, i)
})
}
// 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.
func Slug() Generator {
return func(c *Config) (string, error) {
name, err := getOriginal(c)
return slug.Make(name), err
}
}
// SlugWithLang generates a name from the original filename, accounting for the given language.
// When this is used, the original filename will be removed from the final filename.
func SlugWithLang(lang string) Generator {
return func(c *Config) (string, error) {
name, err := getOriginal(c)
return slug.MakeLang(name, lang), err
}
}
// HashingFunc is a function that generates a hash.Hash
type HashingFunc func() hash.Hash
//go:generate stringer -type=HashType
// HashType represents a particular hashing algorithm
type HashType uint8
const (
MD5 HashType = iota + 1
SHA1
SHA256
)
// ErrInvalidHashType is returned by the Hash generator when an invalid HashType is passed
var ErrInvalidHashType = errors.New("invalid hash type")
var hashMap = map[HashType]HashingFunc{
MD5: md5.New,
SHA1: sha1.New,
SHA256: sha256.New,
}
// 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(t HashType) Generator {
f, ok := hashMap[t]
return func(c *Config) (string, error) {
if !ok {
return "", fmt.Errorf("%w: %s", ErrInvalidHashType, t)
}
name, err := getOriginal(c)
if err != nil {
return "", err
}
hs := f()
hs.Write([]byte(name))
return fmt.Sprintf("%x", hs.Sum(nil)), nil
//nolint:gosec // This is not security sensitive, so a weak number generator is fine.
idx := rand.Int() % len(gens)
return gens[idx](c)
}
}

View file

@ -1,17 +1,20 @@
package nomino
package nomino_test
import "fmt"
import (
"fmt"
func ExampleWithGenerator_custom_generator() {
gen := func(*Config) (string, error) {
"codeberg.org/danjones000/nomino"
)
func ExampleWithGenerator_customGenerator() {
var gen nomino.Generator = func(*nomino.Config) (string, error) {
return "hello", nil
}
option := WithGenerator(gen)
str, _ := Make(NewConfig(option))
str, _ := gen.Make()
fmt.Println(str)
str, _ = Make(NewConfig(option, WithoutExtension()))
str, _ = gen.Make(nomino.WithoutExtension())
fmt.Println(str)
// Output:
@ -19,175 +22,58 @@ func ExampleWithGenerator_custom_generator() {
// hello
}
func ExampleIncremental() {
conf := NewConfig(WithPrefix("foo"), WithGenerator(Incremental()))
func ExampleGenerator_Make() {
g := nomino.Incremental()
st, _ := g.Make()
fmt.Println(st)
str, _ := Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
st, _ = g.Make(nomino.WithPrefix("foo"))
fmt.Println(st)
// Output:
// foo_0.txt
// 0.txt
// foo_1.txt
// foo_2.txt
}
func ExampleIncrementalWithStart() {
conf := NewConfig(WithPrefix("foo"), WithGenerator(IncrementalWithStart(42)))
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, _ := Make(conf)
str, _ := gen.Make()
fmt.Println(str)
str, _ = Make(conf)
str, _ = gen.Make()
fmt.Println(str)
str, _ = Make(conf)
str, _ = gen.Make()
fmt.Println(str)
// Output:
// foo_42.txt
// foo_43.txt
// foo_44.txt
// bonjour.txt
// goodbye.txt
// bonjour.txt
}
func ExampleIncrementalWithStep() {
conf := NewConfig(WithPrefix("foo"), WithGenerator(IncrementalWithStep(2)))
str, _ := Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
// Output:
// foo_0.txt
// foo_2.txt
// foo_4.txt
func ExampleMultiGeneratorRandomOrder() {
gen1 := func(*nomino.Config) (string, error) {
return "guten-tag", nil
}
func ExampleIncrementalWithStartAndStep() {
conf := NewConfig(WithPrefix("foo"), WithGenerator(IncrementalWithStartAndStep(42, 2)))
str, _ := Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
// Output:
// foo_42.txt
// foo_44.txt
// foo_46.txt
gen2 := func(*nomino.Config) (string, error) {
return "wiedersehen", nil
}
gen := nomino.MultiGeneratorRandomOrder(gen1, gen2)
func ExampleIncrementalFormat() {
conf := NewConfig(
WithPrefix("foo"),
WithGenerator(IncrementalFormat("%03d")),
)
str, _ := Make(conf)
str, _ := gen.Make()
fmt.Println(str)
str, _ = Make(conf)
str, _ = gen.Make()
fmt.Println(str)
str, _ = Make(conf)
str, _ = gen.Make()
fmt.Println(str)
// Output:
// foo_000.txt
// foo_001.txt
// foo_002.txt
}
func ExampleIncrementalFormatWithStart() {
conf := NewConfig(
WithPrefix("foo"),
WithGenerator(IncrementalFormatWithStart(9, "%02d")),
)
str, _ := Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
// Output:
// foo_09.txt
// foo_10.txt
}
func ExampleIncrementalFormatWithStep() {
conf := NewConfig(
WithPrefix("foo"),
WithGenerator(IncrementalFormatWithStep(10, "%02d")),
)
str, _ := Make(conf)
fmt.Println(str)
str, _ = Make(conf)
fmt.Println(str)
// Output:
// foo_00.txt
// foo_10.txt
}
func ExampleSlug() {
conf := NewConfig(WithGenerator(Slug()), WithOriginal("My name is Jimmy"))
str, _ := Make(conf)
fmt.Println(str)
// Output: my-name-is-jimmy.txt
}
func ExampleSlugWithLang() {
conf := NewConfig(WithGenerator(SlugWithLang("de")), WithOriginal("Diese & Dass"))
str, _ := Make(conf)
fmt.Println(str)
// Output: diese-und-dass.txt
}
func ExampleHash_mD5() {
conf := NewConfig(
WithOriginal("foobar"),
WithGenerator(Hash(MD5)),
)
str, _ := Make(conf)
fmt.Println(str)
// Output: 3858f62230ac3c915f300c664312c63f.txt
}
func ExampleHash_sHA1() {
conf := NewConfig(
WithOriginal("foobar"),
WithGenerator(Hash(SHA1)),
)
str, _ := Make(conf)
fmt.Println(str)
// Output: 8843d7f92416211de9ebb963ff4ce28125932878.txt
}
func ExampleHash_sHA256() {
conf := NewConfig(
WithOriginal("foobar"),
WithGenerator(Hash(SHA256)),
)
str, _ := Make(conf)
fmt.Println(str)
// Output: c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2.txt
}

View file

@ -2,56 +2,60 @@ package nomino
import (
"errors"
"fmt"
"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(*Config) (string, error) { return "abc", nil }
g := func(*Config) (string, error) { return out1, nil }
var c Config
WithGenerator(g)(&c)
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(*Config) (string, error) { return st1, nil }
g2 := func(*Config) (string, error) { return st2, nil }
g3 := func(*Config) (string, error) { return "", er1 }
g := MultiGeneratorInOrder(g1, g2, g3)
g := MultiGeneratorInOrder(gens...)
st, err := g(nil)
assert.NoError(t, err)
assert.Equal(t, st1, st)
assert.Equal(t, out1, st)
st, err = g(nil)
assert.NoError(t, err)
assert.Equal(t, st2, st)
assert.Equal(t, out2, st)
st, err = g(nil)
assert.Zero(t, st)
assert.ErrorIs(t, err, er1)
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(*Config) (string, error) { return st1, nil }
g1 := func(*Config) (string, error) { return out1, nil }
g := MultiGeneratorInOrder(g1)
st, err := g(nil)
assert.NoError(t, err)
assert.Equal(t, st1, st)
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) {
@ -64,80 +68,38 @@ func TestMultiGeneratorInOrderMissing(t *testing.T) {
assert.ErrorIs(t, err, ErrMissingGenerators)
}
func TestUUID(t *testing.T) {
st, err := UUID()(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()(nil)
assert.Equal(t, errors.New("sorry"), err)
}
func TestTimestamp(t *testing.T) {
n := time.Now()
st, err := Timestamp()(nil)
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)(nil)
assert.NoError(t, err)
assert.Equal(t, d.Format(FileTimestamp), st)
}
func TestTimestampUTC(t *testing.T) {
n := time.Now()
st, err := TimestampUTC()(nil)
assert.NoError(t, err)
assert.Equal(t, n.UTC().Format(FileTimestampNoTZ), st)
}
func TestSlugMissingFilename(t *testing.T) {
conf := NewConfig(WithGenerator(Slug()))
st, err := conf.generator(&conf)
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, ErrMissingOriginal)
assert.ErrorIs(t, err, err1)
} else {
assert.Contains(t, outs, st)
}
}
}
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)
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)
assert.Equal(t, st1, st)
st, err = g(nil)
assert.NoError(t, err)
assert.Equal(t, st1, st)
}
func TestHashBadHash(t *testing.T) {
conf := NewConfig(WithOriginal("foobar"), WithGenerator(Hash(0)))
st, err := conf.generator(&conf)
assert.Equal(t, "", st)
assert.ErrorIs(t, err, ErrInvalidHashType)
assert.ErrorContains(t, err, "invalid hash type: HashType(0)")
}
func TestHashMissingOriginal(t *testing.T) {
conf := NewConfig(WithGenerator(Hash(MD5)))
st, err := conf.generator(&conf)
assert.Equal(t, "", st)
assert.ErrorIs(t, err, ErrMissingOriginal)
}
func TestHashTypeStringer(t *testing.T) {
s := fmt.Sprintf("%s", MD5)
assert.Equal(t, "MD5", s)
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)
}

1
go.mod
View file

@ -3,6 +3,7 @@ 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

2
go.sum
View file

@ -1,5 +1,7 @@
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=

View file

@ -1,26 +0,0 @@
// Code generated by "stringer -type=HashType"; DO NOT EDIT.
package nomino
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[MD5-1]
_ = x[SHA1-2]
_ = x[SHA256-3]
}
const _HashType_name = "MD5SHA1SHA256"
var _HashType_index = [...]uint8{0, 3, 7, 13}
func (i HashType) String() string {
i -= 1
if i >= HashType(len(_HashType_index)-1) {
return "HashType(" + strconv.FormatInt(int64(i+1), 10) + ")"
}
return _HashType_name[_HashType_index[i]:_HashType_index[i+1]]
}

18
make.go
View file

@ -2,15 +2,24 @@ 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) {
// 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.extension), nil
}
func seperateConf(conf *Config) {
if conf.prefix != "" {
conf.prefix += conf.separator
}
@ -19,8 +28,5 @@ func Make(conf Config) (string, error) {
}
if conf.suffix != "" {
conf.suffix = conf.separator + conf.suffix
}
return fmt.Sprintf("%s%s%s%s%s", conf.prefix, name, conf.original, conf.suffix, conf.extension), nil
}

View file

@ -1,9 +1,30 @@
package nomino
package nomino_test
import "fmt"
import (
"fmt"
"codeberg.org/danjones000/nomino"
)
func ExampleMake_basic() {
// Use default config
out, _ := Make(NewConfig())
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
}

View file

@ -7,6 +7,8 @@ import (
"github.com/stretchr/testify/assert"
)
var errTest = errors.New("sorry")
func TestMake(t *testing.T) {
genOpt := WithGenerator(func(*Config) (string, error) { return "abc", nil })
testcases := []struct {
@ -48,11 +50,10 @@ func TestMake(t *testing.T) {
}
func TestMakeErr(t *testing.T) {
retErr := errors.New("oops")
conf := NewConfig(WithGenerator(func(*Config) (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) {
@ -66,3 +67,18 @@ func TestMakeDoesntChangeConf(t *testing.T) {
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)
}

7
nomino.go Normal file
View file

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

View file

@ -6,7 +6,7 @@ import (
"github.com/gosimple/slug"
)
// Option sets configuration parameters for Config.
// Option sets configuration parameters for [Config].
type Option func(c *Config)
// WithOriginal sets the original filename.
@ -18,7 +18,7 @@ func WithOriginal(o string) Option {
}
// WithOriginal sets the original filename as a slug.
// This should not be used with the Slug Generator (as it would be redundant)
// 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)
@ -26,7 +26,7 @@ func WithOriginalSlug(o string) Option {
}
// 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)
// 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)
@ -47,7 +47,7 @@ func WithSuffix(s string) Option {
}
}
// 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 = ""

View file

@ -1,11 +1,91 @@
package nomino
package nomino_test
import "fmt"
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, _ := Make(NewConfig(
WithOriginalSlug("Hello, World"),
WithGenerator(Incremental()),
st, _ := nomino.Make(nomino.NewConfig(
nomino.WithOriginalSlug("Hello, World"),
nomino.WithGenerator(nomino.Incremental()),
))
fmt.Println(st)
@ -13,9 +93,9 @@ func ExampleWithOriginalSlug() {
}
func ExampleWithOriginalSlugLang() {
st, _ := Make(NewConfig(
WithOriginalSlugLang("Diese & Dass", "de"),
WithGenerator(Incremental()),
st, _ := nomino.Make(nomino.NewConfig(
nomino.WithOriginalSlugLang("Diese & Dass", "de"),
nomino.WithGenerator(nomino.Incremental()),
))
fmt.Println(st)

View file

@ -1,48 +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)
}
func TestWithSeparator(t *testing.T) {
var c Config
sep := "---"
WithSeparator(sep)(&c)
assert.Equal(t, sep, c.separator)
}