Skip to content

Commit d94de94

Browse files
committed
feat: Generate names as a JSON array.
- Support custom dictionary. - Various code cleanup.
1 parent 3f92167 commit d94de94

6 files changed

Lines changed: 206 additions & 66 deletions

File tree

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ pleasant-joy
108108
eligible-tenant
109109
```
110110

111+
Generate names as a JSON array (useful for scripting):
112+
113+
```sh
114+
$ fname --format json --quantity 3
115+
["influential-length","direct-ear","cultural-storage"]
116+
117+
$ fname -f json
118+
["extinct-green"]
119+
```
120+
111121
### Library
112122

113123
#### Install
@@ -147,13 +157,42 @@ import (
147157
)
148158

149159
func main() {
150-
rng := fname.NewGenerator(fname.WithDelimiter("__"), fname.WithSize(3))
160+
sizeOpt, err := fname.WithSize(3)
161+
if err != nil {
162+
panic(err)
163+
}
164+
rng := fname.NewGenerator(fname.WithDelimiter("__"), sizeOpt)
151165
phrase, err := rng.Generate()
152166
fmt.Println(phrase)
153167
// => "established__shark__destroyed"
154168
}
155169
```
156170

171+
#### Custom Dictionary
172+
173+
```go
174+
package main
175+
176+
import (
177+
"fmt"
178+
179+
"github.com/splode/fname"
180+
)
181+
182+
func main() {
183+
dict := fname.NewCustomDictionary(
184+
[]string{"blazing", "frozen"}, // adjectives
185+
nil, // adverbs (uses default)
186+
[]string{"comet", "nebula"}, // nouns
187+
nil, // verbs (uses default)
188+
)
189+
rng := fname.NewGenerator(fname.WithDictionary(dict))
190+
phrase, err := rng.Generate()
191+
fmt.Println(phrase)
192+
// => "blazing-nebula"
193+
}
194+
```
195+
157196
## Disclaimers
158197

159198
fname is not cryptographically secure, and should not be used for anything that requires a truly unique identifier. It is meant to be a fun, human-friendly alternative to UUIDs.

cmd/fname/fname.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
_ "embed"
5+
"encoding/json"
56
"fmt"
67
"log"
78
"os"
@@ -45,6 +46,7 @@ func main() {
4546
var (
4647
casing string = "lower"
4748
delimiter string = "-"
49+
format string = "plain"
4850
help bool
4951
ver bool
5052
quantity int = 1
@@ -54,6 +56,7 @@ func main() {
5456

5557
pflag.StringVarP(&casing, "casing", "c", casing, "set the casing of the generated name <title|upper|lower>")
5658
pflag.StringVarP(&delimiter, "delimiter", "d", delimiter, "set the delimiter used to join words")
59+
pflag.StringVarP(&format, "format", "f", format, "set the output format <plain|json>")
5760
pflag.IntVarP(&quantity, "quantity", "q", quantity, "set the number of names to generate")
5861
pflag.UintVarP(&size, "size", "z", size, "set the number of words in the generated name (minimum 2, maximum 4)")
5962
pflag.Int64VarP(&seed, "seed", "s", 0, "random generator seed")
@@ -71,7 +74,15 @@ func main() {
7174
os.Exit(0)
7275
}
7376

74-
c, err := fname.CasingFromString(casing)
77+
if quantity <= 0 {
78+
log.Fatalf("error: quantity must be greater than 0, got %d", quantity)
79+
}
80+
81+
if format != "plain" && format != "json" {
82+
log.Fatalf("error: invalid format %q, must be plain or json", format)
83+
}
84+
85+
c, err := fname.ParseCasing(casing)
7586
handleError(err)
7687

7788
opts := []fname.GeneratorOption{
@@ -83,15 +94,29 @@ func main() {
8394
opts = append(opts, fname.WithSeed(seed))
8495
}
8596
if size != 2 {
86-
opts = append(opts, fname.WithSize(size))
97+
sizeOpt, err := fname.WithSize(size)
98+
handleError(err)
99+
opts = append(opts, sizeOpt)
87100
}
88101

89102
rng := fname.NewGenerator(opts...)
90103

104+
names := make([]string, 0, quantity)
91105
for i := 0; i < quantity; i++ {
92106
name, err := rng.Generate()
93107
handleError(err)
94-
fmt.Println(name)
108+
names = append(names, name)
109+
}
110+
111+
switch format {
112+
case "json":
113+
out, err := json.Marshal(names)
114+
handleError(err)
115+
fmt.Println(string(out))
116+
default:
117+
for _, name := range names {
118+
fmt.Println(name)
119+
}
95120
}
96121
}
97122

dictionary.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
package fname
33

44
import (
5-
"bufio"
65
_ "embed"
76
"strings"
87
)
@@ -31,9 +30,9 @@ type Dictionary struct {
3130
verbs []string
3231
}
3332

34-
// NewDictionary creates a new dictionary.
33+
// NewDictionary creates a new Dictionary backed by the default embedded word lists.
34+
// To use custom word lists, use NewCustomDictionary and pass it via WithDictionary.
3535
func NewDictionary() *Dictionary {
36-
// TODO: allow for custom dictionary
3736
return &Dictionary{
3837
adjectives: adjective,
3938
adverbs: adverb,
@@ -42,6 +41,25 @@ func NewDictionary() *Dictionary {
4241
}
4342
}
4443

44+
// NewCustomDictionary creates a Dictionary with caller-supplied word lists.
45+
// Any nil slice falls back to the corresponding default embedded word list.
46+
func NewCustomDictionary(adjectives, adverbs, nouns, verbs []string) *Dictionary {
47+
d := NewDictionary()
48+
if adjectives != nil {
49+
d.adjectives = adjectives
50+
}
51+
if adverbs != nil {
52+
d.adverbs = adverbs
53+
}
54+
if nouns != nil {
55+
d.nouns = nouns
56+
}
57+
if verbs != nil {
58+
d.verbs = verbs
59+
}
60+
return d
61+
}
62+
4563
// LengthAdjective returns the number of adjectives in the dictionary.
4664
func (d *Dictionary) LengthAdjective() int {
4765
return len(d.adjectives)
@@ -63,11 +81,5 @@ func (d *Dictionary) LengthVerb() int {
6381
}
6482

6583
func split(s string) []string {
66-
scanner := bufio.NewScanner(strings.NewReader(s))
67-
scanner.Split(bufio.ScanLines)
68-
var lines []string
69-
for scanner.Scan() {
70-
lines = append(lines, scanner.Text())
71-
}
72-
return lines
84+
return strings.Split(strings.TrimRight(s, "\n"), "\n")
7385
}

generator.go

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ func (c Casing) String() string {
3131
}
3232
}
3333

34-
func CasingFromString(casing string) (Casing, error) {
34+
// ParseCasing parses a casing string and returns the corresponding Casing value.
35+
func ParseCasing(casing string) (Casing, error) {
3536
switch strings.ToLower(casing) {
3637
case Lower.String():
3738
return Lower, nil
@@ -44,6 +45,13 @@ func CasingFromString(casing string) (Casing, error) {
4445
}
4546
}
4647

48+
// Deprecated: Use ParseCasing instead.
49+
func CasingFromString(casing string) (Casing, error) {
50+
return ParseCasing(casing)
51+
}
52+
53+
// Generator generates random name phrases. It is not safe for concurrent use
54+
// from multiple goroutines; create a separate Generator per goroutine instead.
4755
type Generator struct {
4856
casing Casing
4957
dict *Dictionary
@@ -69,6 +77,16 @@ func WithDelimiter(delimiter string) GeneratorOption {
6977
}
7078
}
7179

80+
// WithDictionary sets a custom Dictionary on the Generator.
81+
// If d is nil, the default embedded Dictionary is used.
82+
func WithDictionary(d *Dictionary) GeneratorOption {
83+
return func(g *Generator) {
84+
if d != nil {
85+
g.dict = d
86+
}
87+
}
88+
}
89+
7290
// WithSeed sets the seed used to generate random numbers.
7391
func WithSeed(seed int64) GeneratorOption {
7492
return func(g *Generator) {
@@ -77,10 +95,14 @@ func WithSeed(seed int64) GeneratorOption {
7795
}
7896

7997
// WithSize sets the number of words in the generated name.
80-
func WithSize(size uint) GeneratorOption {
98+
// Returns an error if size is outside the valid range [2, 4].
99+
func WithSize(size uint) (GeneratorOption, error) {
100+
if size < 2 || size > 4 {
101+
return nil, fmt.Errorf("invalid size: %d", size)
102+
}
81103
return func(g *Generator) {
82104
g.size = size
83-
}
105+
}, nil
84106
}
85107

86108
// NewGenerator creates a new Generator.
@@ -100,16 +122,9 @@ func NewGenerator(opts ...GeneratorOption) *Generator {
100122

101123
// Generate generates a random name.
102124
func (g *Generator) Generate() (string, error) {
103-
if g.size < 2 || g.size > 4 {
104-
return "", fmt.Errorf("invalid size: %d", g.size)
105-
}
106-
107125
words := make([]string, 0, g.size)
108126
adjectiveIndex := g.rand.Intn(g.dict.LengthAdjective())
109127
nounIndex := g.rand.Intn(g.dict.LengthNoun())
110-
for adjectiveIndex == nounIndex {
111-
nounIndex = g.rand.Intn(g.dict.LengthNoun())
112-
}
113128

114129
words = append(words, g.dict.adjectives[adjectiveIndex], g.dict.nouns[nounIndex])
115130

@@ -124,19 +139,18 @@ func (g *Generator) Generate() (string, error) {
124139
return strings.Join(g.applyCasing(words), g.delimiter), nil
125140
}
126141

142+
var titleCaser = cases.Title(language.English)
143+
127144
func (g *Generator) applyCasing(words []string) []string {
128-
if fn, ok := casingMap[g.casing]; ok {
129-
for i, word := range words {
130-
words[i] = fn(word)
145+
for i, word := range words {
146+
switch g.casing {
147+
case Lower:
148+
words[i] = strings.ToLower(word)
149+
case Upper:
150+
words[i] = strings.ToUpper(word)
151+
case Title:
152+
words[i] = titleCaser.String(word)
131153
}
132154
}
133155
return words
134156
}
135-
136-
var titleCaser = cases.Title(language.English)
137-
138-
var casingMap = map[Casing]func(string) string{
139-
Lower: strings.ToLower,
140-
Upper: strings.ToUpper,
141-
Title: titleCaser.String,
142-
}

generator_test.go

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ func TestNewGenerator(t *testing.T) {
2424

2525
t.Log("\tWhen creating a new Generator with custom values")
2626
{
27-
g := NewGenerator(WithCasing(Title), WithDelimiter("_"), WithSize(3), WithSeed(12345))
27+
sizeOpt, err := WithSize(3)
28+
if err != nil {
29+
t.Fatal("\t\tShould be able to create a size option without error.")
30+
}
31+
g := NewGenerator(WithCasing(Title), WithDelimiter("_"), sizeOpt, WithSeed(12345))
2832
if g == nil {
2933
t.Fatal("\t\tShould be able to create a Generator instance.")
3034
}
@@ -114,7 +118,11 @@ func TestGenerate(t *testing.T) {
114118

115119
t.Log("\tWhen generating a phrase with a custom size")
116120
{
117-
g3 := NewGenerator(WithSize(3))
121+
size3Opt, err := WithSize(3)
122+
if err != nil {
123+
t.Fatal("\t\tShould be able to create a size-3 option without error.")
124+
}
125+
g3 := NewGenerator(size3Opt)
118126
phrase, err := g3.Generate()
119127
if err != nil {
120128
t.Fatal("\t\tShould be able to generate a phrase without error.")
@@ -132,7 +140,11 @@ func TestGenerate(t *testing.T) {
132140
}
133141
t.Log("\t\tShould be able to generate a phrase with 3 parts.")
134142

135-
g4 := NewGenerator(WithSize(4))
143+
size4Opt, err := WithSize(4)
144+
if err != nil {
145+
t.Fatal("\t\tShould be able to create a size-4 option without error.")
146+
}
147+
g4 := NewGenerator(size4Opt)
136148
phrase, err = g4.Generate()
137149
if err != nil {
138150
t.Fatal("\t\tShould be able to generate a phrase without error.")
@@ -175,12 +187,50 @@ func TestGenerate(t *testing.T) {
175187

176188
t.Log("\tWhen generating a phrase with an invalid size")
177189
{
178-
g := NewGenerator(WithSize(1))
179-
_, err := g.Generate()
190+
_, err := WithSize(1)
180191
if err == nil {
181-
t.Fatal("\t\tShould not be able to generate a phrase with an invalid size.")
192+
t.Fatal("\t\tShould not be able to create a size option with an invalid size.")
193+
}
194+
t.Log("\t\tShould not be able to create a size option with an invalid size.")
195+
}
196+
}
197+
}
198+
199+
func TestWithDictionary(t *testing.T) {
200+
t.Log("Given the need to test the WithDictionary option")
201+
{
202+
t.Log("\tWhen generating with a custom dictionary")
203+
{
204+
custom := NewCustomDictionary(
205+
[]string{"fast"},
206+
nil,
207+
[]string{"rocket"},
208+
nil,
209+
)
210+
g := NewGenerator(WithDictionary(custom), WithSeed(1))
211+
phrase, err := g.Generate()
212+
if err != nil {
213+
t.Fatal("\t\tShould be able to generate a phrase without error.")
214+
}
215+
t.Log("\t\tShould be able to generate a phrase without error.")
216+
217+
if phrase != "fast-rocket" {
218+
t.Fatalf("\t\tShould generate phrase from custom words, got: %s", phrase)
219+
}
220+
t.Log("\t\tShould generate phrase from custom words.")
221+
}
222+
223+
t.Log("\tWhen passing nil dictionary")
224+
{
225+
g := NewGenerator(WithDictionary(nil))
226+
phrase, err := g.Generate()
227+
if err != nil {
228+
t.Fatal("\t\tShould fall back to default dictionary without error.")
229+
}
230+
if len(phrase) == 0 {
231+
t.Fatal("\t\tShould produce a non-empty phrase with default dictionary.")
182232
}
183-
t.Log("\t\tShould not be able to generate a phrase with an invalid size.")
233+
t.Log("\t\tShould fall back to default dictionary.")
184234
}
185235
}
186236
}

0 commit comments

Comments
 (0)