diff --git a/CHANGELOG.md b/CHANGELOG.md index 0611f05..c2674b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +### [0.4.0] - 2025-03-15 + +#### Features + +- Add Random Generator +- Add Make method to Generator +- Add MultiGeneratorRandomOrder + +#### Changes + +- Replace HashType with Hasher: This supports all crypto.Hash + +#### Support + +- Add some missing doc comments + ### [0.3.0] - 2025-03-14 #### Features diff --git a/Taskfile.yml b/Taskfile.yml index 474a888..35792f6 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -26,7 +26,6 @@ tasks: desc: Vet go code sources: - '**/*.go' - deps: [gen] cmds: - go vet ./... @@ -82,7 +81,7 @@ tasks: test: desc: Run unit tests - deps: [fmt, vet, gen] + deps: [fmt, vet] sources: - '**/*.go' generates: diff --git a/config.go b/config.go index 5c87729..a7da5f5 100644 --- a/config.go +++ b/config.go @@ -9,6 +9,7 @@ type Config struct { generator Generator } +// NewConfig returns a new Config with the specified Options. func NewConfig(options ...Option) Config { conf := Config{ extension: ".txt", diff --git a/gen_file.go b/gen_file.go index ef239cb..497fdcc 100644 --- a/gen_file.go +++ b/gen_file.go @@ -1,9 +1,7 @@ package nomino import ( - "crypto/md5" - "crypto/sha1" - "crypto/sha256" + "crypto" "errors" "fmt" "hash" @@ -42,39 +40,35 @@ func Slug(lang ...string) Generator { // HashingFunc is a function that generates a hash.Hash type HashingFunc func() hash.Hash -//go:generate stringer -type=HashType -trimprefix=Hash - -// HashType represents a particular hashing algorithm -type HashType uint8 - -const ( - HashMD5 HashType = iota + 1 - HashSHA1 - HashSHA256 -) - -// 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{ - HashMD5: md5.New, - HashSHA1: sha1.New, - HashSHA256: sha256.New, +// 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 HashType 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(t HashType) Generator { - f, ok := hashMap[t] +func Hash(h Hasher) Generator { + if h == nil { + h = crypto.MD5 + } return func(c *Config) (string, error) { - if !ok { - return "", fmt.Errorf("%w: %s", ErrInvalidHashType, t) + if h == crypto.MD5SHA1 { + return "", ErrInvalidHash } name, err := getOriginal(c) if err != nil { return "", err } - hs := f() + hs := h.New() hs.Write([]byte(name)) return fmt.Sprintf("%x", hs.Sum(nil)), nil } diff --git a/gen_file_examples_test.go b/gen_file_examples_test.go index 3d8036f..76f3260 100644 --- a/gen_file_examples_test.go +++ b/gen_file_examples_test.go @@ -1,66 +1,56 @@ package nomino_test import ( + "crypto" + "crypto/hmac" "fmt" + "hash" "codeberg.org/danjones000/nomino" ) func ExampleSlug() { - conf := nomino.NewConfig( - nomino.WithOriginal("My name is Jimmy"), - nomino.WithGenerator(nomino.Slug()), - ) - str, _ := nomino.Make(conf) + str, _ := nomino.Slug().Make(nomino.WithOriginal("My name is Jimmy")) fmt.Println(str) // Output: my-name-is-jimmy.txt } func ExampleSlug_withLang() { - conf := nomino.NewConfig( - nomino.WithOriginal("Diese & Dass"), - nomino.WithGenerator(nomino.Slug("de")), - ) - - str, _ := nomino.Make(conf) + str, _ := nomino.Slug("de"). + Make(nomino.WithOriginal("Diese & Dass")) fmt.Println(str) // Output: diese-und-dass.txt } func ExampleHash_mD5() { - conf := nomino.NewConfig( - nomino.WithOriginal("foobar"), - nomino.WithGenerator( - nomino.Hash(nomino.HashMD5), - ), - ) - str, _ := nomino.Make(conf) + str, _ := nomino.Hash(crypto.MD5). + Make(nomino.WithOriginal("foobar")) fmt.Println(str) // Output: 3858f62230ac3c915f300c664312c63f.txt } func ExampleHash_sHA1() { - conf := nomino.NewConfig( - nomino.WithOriginal("foobar"), - nomino.WithGenerator( - nomino.Hash(nomino.HashSHA1), - ), - ) - str, _ := nomino.Make(conf) + str, _ := nomino.Hash(crypto.SHA1). + Make(nomino.WithOriginal("foobar")) fmt.Println(str) // Output: 8843d7f92416211de9ebb963ff4ce28125932878.txt } func ExampleHash_sHA256() { - conf := nomino.NewConfig( - nomino.WithOriginal("foobar"), - nomino.WithGenerator( - nomino.Hash(nomino.HashSHA256), - ), - ) - str, _ := nomino.Make(conf) + 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 index 0d03d16..cd0a8bf 100644 --- a/gen_file_test.go +++ b/gen_file_test.go @@ -1,6 +1,7 @@ package nomino import ( + "crypto" "testing" "github.com/stretchr/testify/assert" @@ -22,21 +23,15 @@ func TestSlugRemovesOriginal(t *testing.T) { } func TestHashBadHash(t *testing.T) { - conf := NewConfig(WithOriginal("foobar"), WithGenerator(Hash(0))) + conf := NewConfig(WithOriginal("foobar"), WithGenerator(Hash(crypto.MD5SHA1))) st, err := conf.generator(&conf) assert.Equal(t, "", st) - assert.ErrorIs(t, err, ErrInvalidHashType) - assert.ErrorContains(t, err, "invalid hash type: HashType(0)") + assert.ErrorIs(t, err, ErrInvalidHash) } func TestHashMissingOriginal(t *testing.T) { - conf := NewConfig(WithGenerator(Hash(HashMD5))) + conf := NewConfig(WithGenerator(Hash(nil))) st, err := conf.generator(&conf) assert.Equal(t, "", st) assert.ErrorIs(t, err, ErrMissingOriginal) } - -func TestHashTypeStringer(t *testing.T) { - s := HashMD5.String() - assert.Equal(t, "MD5", s) -} diff --git a/gen_rand.go b/gen_rand.go new file mode 100644 index 0000000..8b4ce5a --- /dev/null +++ b/gen_rand.go @@ -0,0 +1,60 @@ +package nomino + +import ( + "crypto/rand" + "strings" + + "github.com/deatil/go-encoding/base62" + "github.com/google/uuid" +) + +func uuidGen(*Config) (string, error) { + u, err := uuid.NewRandom() + if err != nil { + return "", err + } + return u.String(), nil +} + +// UUID generates a UUIDv4. +func UUID() Generator { + return uuidGen +} + +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] +} + +// 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) + for buff.Len() < c.length { + buff.Write(getRandomBytes(c.length - buff.Len())) + } + return buff.String(), nil + } +} diff --git a/gen_rand_examples_test.go b/gen_rand_examples_test.go new file mode 100644 index 0000000..45cb7fe --- /dev/null +++ b/gen_rand_examples_test.go @@ -0,0 +1,36 @@ +package nomino_test + +import ( + "fmt" + + "codeberg.org/danjones000/nomino" +) + +func ExampleUUID() { + option := nomino.WithGenerator(nomino.UUID()) + + str, _ := nomino.Make(nomino.NewConfig(option)) + fmt.Println(str) + + str, _ = nomino.Make(nomino.NewConfig(option)) + fmt.Println(str) + + str, _ = nomino.Make(nomino.NewConfig(option)) + fmt.Println(str) +} + +func ExampleRandom() { + option := nomino.WithGenerator(nomino.Random()) + + str, _ := nomino.Make(nomino.NewConfig(option)) + fmt.Println(str) +} + +func ExampleRandomLength() { + option := nomino.WithGenerator(nomino.Random( + nomino.RandomLength(32), + )) + + str, _ := nomino.Make(nomino.NewConfig(option)) + fmt.Println(str) +} diff --git a/gen_rand_test.go b/gen_rand_test.go new file mode 100644 index 0000000..832275b --- /dev/null +++ b/gen_rand_test.go @@ -0,0 +1,42 @@ +package nomino + +import ( + "errors" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" +) + +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 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/generators.go b/generators.go index 25cd36e..c7ad4e8 100644 --- a/generators.go +++ b/generators.go @@ -2,8 +2,7 @@ package nomino import ( "errors" - - "github.com/google/uuid" + "math/rand" ) // Generator is a function that returns the "random" portion of the returned filename. @@ -11,6 +10,12 @@ import ( // for example. type Generator func(conf *Config) (string, error) +// Make allows you to generate a new string directly from a generator. +func (g Generator) Make(opts ...Option) (string, error) { + opts = append(opts, WithGenerator(g)) + return Make(NewConfig(opts...)) +} + // WithGenerator sets the specified generator func WithGenerator(g Generator) Option { return func(c *Config) { @@ -44,15 +49,19 @@ func MultiGeneratorInOrder(gens ...Generator) Generator { } } -func uuidGen(*Config) (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] + } + + return func(c *Config) (string, error) { + idx := rand.Int() % len(gens) + return gens[idx](c) + } } diff --git a/generators_examples_test.go b/generators_examples_test.go index c9d8710..5efc4d8 100644 --- a/generators_examples_test.go +++ b/generators_examples_test.go @@ -7,18 +7,14 @@ import ( ) func ExampleWithGenerator_customGenerator() { - gen := func(*nomino.Config) (string, error) { + var gen nomino.Generator = func(*nomino.Config) (string, error) { return "hello", nil } - option := nomino.WithGenerator(gen) - str, _ := nomino.Make(nomino.NewConfig(option)) + str, _ := gen.Make() fmt.Println(str) - str, _ = nomino.Make(nomino.NewConfig( - option, - nomino.WithoutExtension(), - )) + str, _ = gen.Make(nomino.WithoutExtension()) fmt.Println(str) // Output: @@ -26,6 +22,19 @@ func ExampleWithGenerator_customGenerator() { // 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 "hello", nil @@ -34,15 +43,14 @@ func ExampleMultiGeneratorInOrder() { return "goodbye", nil } gen := nomino.MultiGeneratorInOrder(gen1, gen2) - option := nomino.WithGenerator(gen) - str, _ := nomino.Make(nomino.NewConfig(option)) + str, _ := gen.Make() fmt.Println(str) - str, _ = nomino.Make(nomino.NewConfig(option)) + str, _ = gen.Make() fmt.Println(str) - str, _ = nomino.Make(nomino.NewConfig(option)) + str, _ = gen.Make() fmt.Println(str) // Output: @@ -51,15 +59,21 @@ func ExampleMultiGeneratorInOrder() { // hello.txt } -func ExampleUUID() { - option := nomino.WithGenerator(nomino.UUID()) +func ExampleMultiGeneratorRandomOrder() { + gen1 := func(*nomino.Config) (string, error) { + return "hello", nil + } + gen2 := func(*nomino.Config) (string, error) { + return "goodbye", nil + } + gen := nomino.MultiGeneratorRandomOrder(gen1, gen2) - str, _ := nomino.Make(nomino.NewConfig(option)) + str, _ := gen.Make() fmt.Println(str) - str, _ = nomino.Make(nomino.NewConfig(option)) + str, _ = gen.Make() fmt.Println(str) - str, _ = nomino.Make(nomino.NewConfig(option)) + str, _ = gen.Make() fmt.Println(str) } diff --git a/generators_test.go b/generators_test.go index 7886ded..f8a81fa 100644 --- a/generators_test.go +++ b/generators_test.go @@ -4,7 +4,6 @@ import ( "errors" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/assert" ) @@ -17,26 +16,34 @@ func TestWithGenerator(t *testing.T) { assert.Equal(t, "abc", st) } +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 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) { @@ -62,23 +69,38 @@ func TestMultiGeneratorInOrderMissing(t *testing.T) { assert.ErrorIs(t, err, ErrMissingGenerators) } -func TestUUID(t *testing.T) { - st, err := UUID()(nil) +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) + assert.Equal(t, st1, st) + st, err = g(nil) + assert.NoError(t, err) + assert.Equal(t, st1, st) } -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 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 6917d49..c733bcf 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8638c59..209d634 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/hashtype_string.go b/hashtype_string.go deleted file mode 100644 index a4ce423..0000000 --- a/hashtype_string.go +++ /dev/null @@ -1,26 +0,0 @@ -// Code generated by "stringer -type=HashType -trimprefix=Hash"; 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[HashMD5-1] - _ = x[HashSHA1-2] - _ = x[HashSHA256-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]] -} diff --git a/make.go b/make.go index 640ec39..2e9d754 100644 --- a/make.go +++ b/make.go @@ -2,9 +2,9 @@ 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. +// 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)