Skip to content

Commit 6851d73

Browse files
committed
feat: add example cli tool
1 parent 6347cda commit 6851d73

5 files changed

Lines changed: 200 additions & 6 deletions

File tree

README.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,48 @@ func AllWords() []string
121121

122122
Every known word, sorted. Returns a copy — safe to mutate.
123123

124+
## CLI Tool
125+
126+
`cmd/thesaurus` is a small example CLI over the package's own API — stdlib only,
127+
no dependencies:
128+
129+
```bash
130+
go install github.com/bobadilla-tech/thesaurus-go/cmd/thesaurus@latest
131+
```
132+
133+
```bash
134+
$ thesaurus lookup happy
135+
Synonyms: [joyful cheerful content pleased delighted glad elated blissful]
136+
Antonyms: [sad unhappy miserable sorrowful dejected gloomy melancholy]
137+
138+
$ thesaurus prefix happ
139+
happen
140+
happening
141+
happenstance
142+
happily
143+
happiness
144+
happy
145+
146+
$ thesaurus count
147+
45695
148+
```
149+
124150
## How It Works
125151

126152
1. **Normalize**: input words are trimmed and converted to lowercase.
127153
2. **Curated lookup**: a small hand-maintained dataset is checked first. Curated
128154
entries always take precedence over OEWN.
129-
3. \*_OEWN fallback_: if the word is not present in the curated dataset,
155+
3. **OEWN fallback**: if the word is not present in the curated dataset,
130156
synonyms and antonyms are resolved from the embedded Open English WordNet
131157
indexes.
132158
4. **Embedded data**: the generated JSON indexes are gzip-compressed and
133159
embedded into the binary using `go:embed`.
134160

135161
## Regenerating the Dataset
136162

137-
The repository includes a build-time parser located in `cmd/wnparser`. Data
138-
sources are pluggable through a `Provider` interface (see
139-
`cmd/wnparser/provider.go`) — `oewn` is the only one implemented today, but
163+
The repository includes a build-time preprocessor located in `cmd/datasetbuild`.
164+
Data sources are pluggable through a `Provider` interface (see
165+
`cmd/datasetbuild/provider.go`) — `oewn` is the only one implemented today, but
140166
adding another means implementing `Provider` and registering it, with no changes
141167
to `main()`.
142168

@@ -146,7 +172,7 @@ format) and generates the compressed JSON files embedded by the package.
146172
Example:
147173

148174
```bash
149-
go run ./cmd/wnparser \
175+
go run ./cmd/datasetbuild \
150176
-input english-wordnet-2025.xml \
151177
-output-dir ./dataset \
152178
-provider oewn

cmd/thesaurus/commands.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
7+
thesaurus "github.com/bobadilla-tech/thesaurus-go"
8+
)
9+
10+
// runLookup prints the synonyms and antonyms for word, or returns an error
11+
// if word has no entry.
12+
func runLookup(word string, out io.Writer) error {
13+
entry, ok := thesaurus.Lookup(word)
14+
if !ok {
15+
return fmt.Errorf("%q: not found", word)
16+
}
17+
18+
fmt.Fprintln(out, "Synonyms:", entry.Synonyms)
19+
fmt.Fprintln(out, "Antonyms:", entry.Antonyms)
20+
return nil
21+
}
22+
23+
// runPrefix prints every known word starting with prefix, one per line.
24+
func runPrefix(prefix string, out io.Writer) error {
25+
matches := thesaurus.WordsWithPrefix(prefix)
26+
for _, word := range matches {
27+
fmt.Fprintln(out, word)
28+
}
29+
return nil
30+
}
31+
32+
// runCount prints the total number of words the dataset can look up.
33+
func runCount(out io.Writer) error {
34+
fmt.Fprintln(out, thesaurus.Count())
35+
return nil
36+
}

cmd/thesaurus/commands_test.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"strconv"
6+
"strings"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
thesaurus "github.com/bobadilla-tech/thesaurus-go"
13+
)
14+
15+
func TestRunLookup_KnownWord(t *testing.T) {
16+
var buf bytes.Buffer
17+
18+
err := runLookup("happy", &buf)
19+
20+
require.NoError(t, err)
21+
out := buf.String()
22+
assert.Contains(t, out, "Synonyms:")
23+
assert.Contains(t, out, "Antonyms:")
24+
}
25+
26+
func TestRunLookup_UnknownWord(t *testing.T) {
27+
var buf bytes.Buffer
28+
29+
err := runLookup("zzzznotaword", &buf)
30+
31+
assert.Error(t, err, "expected an error for an unknown word")
32+
assert.Empty(t, buf.String(), "should not print anything for an unknown word")
33+
}
34+
35+
func TestRunPrefix_KnownPrefix(t *testing.T) {
36+
var buf bytes.Buffer
37+
38+
err := runPrefix("happ", &buf)
39+
40+
require.NoError(t, err)
41+
lines := strings.Fields(buf.String())
42+
assert.NotEmpty(t, lines)
43+
for _, w := range lines {
44+
assert.True(t, strings.HasPrefix(w, "happ"), "%q does not start with 'happ'", w)
45+
}
46+
}
47+
48+
func TestRunPrefix_UnknownPrefix(t *testing.T) {
49+
var buf bytes.Buffer
50+
51+
err := runPrefix("zzzznotaprefix", &buf)
52+
53+
require.NoError(t, err, "an unmatched prefix is not an error")
54+
assert.Empty(t, buf.String())
55+
}
56+
57+
func TestRunCount_MatchesLibrary(t *testing.T) {
58+
var buf bytes.Buffer
59+
60+
err := runCount(&buf)
61+
62+
require.NoError(t, err)
63+
got, convErr := strconv.Atoi(strings.TrimSpace(buf.String()))
64+
require.NoError(t, convErr)
65+
assert.Equal(t, thesaurus.Count(), got)
66+
}

cmd/thesaurus/main.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
// Command thesaurus is a small example CLI over the thesaurus package,
2+
// demonstrating Lookup, WordsWithPrefix, and Count from the terminal.
3+
//
4+
// Usage:
5+
//
6+
// thesaurus lookup <word>
7+
// thesaurus prefix <prefix>
8+
// thesaurus count
9+
package main
10+
11+
import (
12+
"fmt"
13+
"os"
14+
)
15+
16+
func usage() {
17+
fmt.Fprintln(os.Stderr, "usage:")
18+
fmt.Fprintln(os.Stderr, " thesaurus lookup <word>")
19+
fmt.Fprintln(os.Stderr, " thesaurus prefix <prefix>")
20+
fmt.Fprintln(os.Stderr, " thesaurus count")
21+
}
22+
23+
func main() {
24+
if len(os.Args) < 2 {
25+
usage()
26+
os.Exit(2)
27+
}
28+
29+
var err error
30+
switch cmd := os.Args[1]; cmd {
31+
case "lookup":
32+
if len(os.Args) != 3 {
33+
usage()
34+
os.Exit(2)
35+
}
36+
err = runLookup(os.Args[2], os.Stdout)
37+
case "prefix":
38+
if len(os.Args) != 3 {
39+
usage()
40+
os.Exit(2)
41+
}
42+
err = runPrefix(os.Args[2], os.Stdout)
43+
case "count":
44+
if len(os.Args) != 2 {
45+
usage()
46+
os.Exit(2)
47+
}
48+
err = runCount(os.Stdout)
49+
default:
50+
fmt.Fprintf(os.Stderr, "unknown command %q\n", cmd)
51+
usage()
52+
os.Exit(2)
53+
}
54+
55+
if err != nil {
56+
fmt.Fprintln(os.Stderr, err)
57+
os.Exit(1)
58+
}
59+
}

oewn_embed.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ var antonymsOEWNData map[string][]string
3737
var allWords []string
3838

3939
func init() {
40-
4140
if err := json.Unmarshal(curatedRaw, &curatedData); err != nil {
4241
panic(err)
4342
}
@@ -50,20 +49,25 @@ func init() {
5049

5150
func buildAllWords(curated map[string]Entry, synonymsOEWN, antonymsOEWN map[string][]string) []string {
5251
seen := make(map[string]struct{}, len(curated)+len(synonymsOEWN)+len(antonymsOEWN))
52+
5353
for word := range curated {
5454
seen[word] = struct{}{}
5555
}
56+
5657
for word := range synonymsOEWN {
5758
seen[word] = struct{}{}
5859
}
60+
5961
for word := range antonymsOEWN {
6062
seen[word] = struct{}{}
6163
}
6264

6365
words := make([]string, 0, len(seen))
66+
6467
for word := range seen {
6568
words = append(words, word)
6669
}
70+
6771
sort.Strings(words)
6872
return words
6973
}
@@ -75,18 +79,21 @@ func buildAllWords(curated map[string]Entry, synonymsOEWN, antonymsOEWN map[stri
7579
// or missing dataset.
7680
func mustLoadGzipJSON(compressed []byte) map[string][]string {
7781
gz, err := gzip.NewReader(bytes.NewReader(compressed))
82+
7883
if err != nil {
7984
panic(err)
8085
}
8186

8287
defer gz.Close()
8388

8489
decompressed, err := io.ReadAll(gz)
90+
8591
if err != nil {
8692
panic(err)
8793
}
8894

8995
var data map[string][]string
96+
9097
if err := json.Unmarshal(decompressed, &data); err != nil {
9198
panic(err)
9299
}

0 commit comments

Comments
 (0)