Skip to content

Commit 50f80aa

Browse files
committed
refactor
1 parent 1f15e9f commit 50f80aa

File tree

13 files changed

+341
-164
lines changed

13 files changed

+341
-164
lines changed

go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ go 1.20
44

55
require (
66
github.com/aws/aws-sdk-go v1.44.312
7-
github.com/jedib0t/go-pretty/v6 v6.4.6
7+
github.com/mitchellh/go-wordwrap v1.0.1
8+
github.com/olekukonko/tablewriter v0.0.5
89
github.com/spf13/cobra v1.7.0
910
github.com/spf13/viper v1.16.0
1011
github.com/stretchr/testify v1.8.4
@@ -22,18 +23,19 @@ require (
2223
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2324
github.com/jmespath/go-jmespath v0.4.0 // indirect
2425
github.com/magiconair/properties v1.8.7 // indirect
25-
github.com/mattn/go-runewidth v0.0.13 // indirect
26+
github.com/mattn/go-runewidth v0.0.14 // indirect
2627
github.com/mitchellh/mapstructure v1.5.0 // indirect
2728
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
2829
github.com/pmezard/go-difflib v1.0.0 // indirect
29-
github.com/rivo/uniseg v0.2.0 // indirect
30+
github.com/rivo/uniseg v0.4.4 // indirect
3031
github.com/spf13/afero v1.9.5 // indirect
3132
github.com/spf13/cast v1.5.1 // indirect
3233
github.com/spf13/jwalterweatherman v1.1.0 // indirect
3334
github.com/spf13/pflag v1.0.5 // indirect
3435
github.com/subosito/gotenv v1.4.2 // indirect
3536
github.com/tdewolff/parse/v2 v2.6.6 // indirect
3637
golang.org/x/sys v0.10.0 // indirect
37-
golang.org/x/text v0.9.0 // indirect
38+
golang.org/x/text v0.11.0 // indirect
39+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
3840
gopkg.in/ini.v1 v1.67.0 // indirect
3941
)

go.sum

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
132132
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
133133
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
134134
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
135-
github.com/jedib0t/go-pretty/v6 v6.4.6 h1:v6aG9h6Uby3IusSSEjHaZNXpHFhzqMmjXcPq1Rjl9Jw=
136-
github.com/jedib0t/go-pretty/v6 v6.4.6/go.mod h1:Ndk3ase2CkQbXLLNf5QDHoYb6J9WtVfmHZu9n8rk2xs=
137135
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
138136
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
139137
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -150,20 +148,25 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
150148
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
151149
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
152150
github.com/matryer/try v0.0.0-20161228173917-9ac251b645a2/go.mod h1:0KeJpeMD6o+O4hW7qJOT7vyQPKrWmj26uf5wMc/IiIs=
153-
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
154-
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
151+
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
152+
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
153+
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
154+
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
155+
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
155156
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
156157
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
158+
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
159+
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
157160
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
158161
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
159162
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
160-
github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18=
161163
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
162164
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
163165
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
164166
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
165-
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
166167
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
168+
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
169+
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
167170
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
168171
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
169172
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -187,7 +190,6 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
187190
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
188191
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
189192
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
190-
github.com/stretchr/testify v1.7.4/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
191193
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
192194
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
193195
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
@@ -367,8 +369,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
367369
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
368370
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
369371
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
370-
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
371-
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
372+
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
373+
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
372374
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
373375
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
374376
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -513,8 +515,9 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD
513515
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
514516
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
515517
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
516-
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
517518
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
519+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
520+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
518521
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
519522
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
520523
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=

internal/cmd/init.go

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,61 +4,109 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78

89
"github.com/spf13/cobra"
910
"github.com/spf13/viper"
1011
"github.com/trynoice/iris/internal/config"
1112
)
1213

14+
var defaultConfig = &config.Config{
15+
Service: config.ServiceConfig{
16+
AwsSes: &config.AwsSesServiceConfig{
17+
UseSharedConfig: true,
18+
},
19+
RateLimit: 10,
20+
Retries: 3,
21+
},
22+
Message: config.MessageConfig{
23+
Sender: "Iris CLI <[email protected]>",
24+
DefaultDataCsvFile: "default.csv",
25+
RecipientDataCsvFile: "recipients.csv",
26+
RecipientEmailColumnName: "Email",
27+
MinifyHtml: true,
28+
},
29+
}
30+
1331
var defaultEmailFiles = map[string]string{
14-
"subject.txt": "Hello {{ .Name }}",
32+
"subject.txt": "Hello {{.Name}}",
1533
"body.txt": `Iris is a CLI tool for sending templated bulk emails.
1634
17-
You can inject data into templates, e.g. a date - {{ .Date }} or your email - {{ .Recipient }}.`,
35+
You can inject data into templates, e.g. a date - {{.Date}} or your email - {{.Email}}.`,
1836

1937
"body.html": `<!DOCTYPE html>
2038
<html>
2139
<head>
2240
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
2341
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
24-
<title>Hello {{ .Name }}</title>
42+
<title>Hello {{.Name}}</title>
2543
</head>
2644
<body>
2745
<p>Iris is a CLI tool for sending templated bulk emails.</p>
2846
<p>
29-
You can inject data into templates, e.g. a date - {{ .Date }} or your
30-
email - {{ .Recipient }}.
47+
You can inject data into templates, e.g. a date - {{.Date}} or your
48+
email - {{.Email}}.
3149
</p>
3250
</body>
3351
</html>`,
3452

35-
"default.csv": "Date\nJanuary 2006",
36-
"recipient.csv": "Name,Recipient\nJohn,hello@example.test",
53+
"default.csv": "Date\nJanuary 2006",
54+
"recipients.csv": "Name,Email\nJack,[email protected]\nJill,jill@example.test",
3755
}
3856

3957
func InitCommand(v *viper.Viper, configFileName string) *cobra.Command {
4058
c := &cobra.Command{
41-
Use: "init <dir>",
59+
Use: "init [dir]",
4260
Short: "Create working files in the given directory",
43-
Args: cobra.ExactArgs(1),
61+
Args: cobra.MaximumNArgs(1),
4462
RunE: func(cmd *cobra.Command, args []string) error {
45-
if notExists(args[0]) {
46-
cmd.Println("creating directory", args[0])
47-
if err := os.MkdirAll(args[0], os.ModeDir|os.ModePerm); err != nil {
63+
wd := "."
64+
if len(args) > 0 {
65+
wd = args[0]
66+
}
67+
68+
v.AddConfigPath(wd)
69+
cfg, err := config.Read(v)
70+
if err != nil {
71+
return err
72+
}
73+
74+
if notExists(wd) {
75+
cmd.Println("creating directory", wd)
76+
if err := os.MkdirAll(wd, os.ModeDir|os.ModePerm); err != nil {
4877
return fmt.Errorf("failed to create directory: %w", err)
4978
}
5079
}
5180

52-
cfgFile := filepath.Join(args[0], configFileName)
81+
cfgFile := filepath.Join(wd, configFileName)
5382
if notExists(cfgFile) {
83+
// consider this default configuration for generating all files
84+
// if user didn't supply a config.
85+
cfg = defaultConfig
5486
cmd.Println("creating file", cfgFile)
55-
if err := config.WriteDefault(v, cfgFile); err != nil {
87+
if err := config.Write(defaultConfig, cfgFile); err != nil {
5688
return fmt.Errorf("failed to write default config: %w", err)
5789
}
5890
}
5991

6092
for name, content := range defaultEmailFiles {
61-
file := filepath.Join(args[0], name)
93+
switch name {
94+
case "default.csv":
95+
name = cfg.Message.DefaultDataCsvFile
96+
content = strings.ReplaceAll(content, "Email", cfg.Message.RecipientEmailColumnName)
97+
case "recipients.csv":
98+
name = cfg.Message.RecipientDataCsvFile
99+
content = strings.ReplaceAll(content, "Email", cfg.Message.RecipientEmailColumnName)
100+
default:
101+
replacement := fmt.Sprintf("{{.%s}}", cfg.Message.RecipientEmailColumnName)
102+
content = strings.ReplaceAll(content, "{{.Email}}", replacement)
103+
}
104+
105+
if name == "" { // defaults csv is optional
106+
continue
107+
}
108+
109+
file := filepath.Join(wd, name)
62110
if notExists(file) {
63111
cmd.Println("creating file", file)
64112
if err := os.WriteFile(file, []byte(content), os.ModePerm); err != nil {

internal/cmd/init_test.go

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,27 @@ func TestInitCommand(t *testing.T) {
2020
"body.txt",
2121
"body.html",
2222
"default.csv",
23-
"recipient.csv",
23+
"recipients.csv",
2424
cfgFileName,
2525
}
2626

2727
t.Run("WithoutPositionalArg", func(t *testing.T) {
28+
tmpDir := t.TempDir()
29+
testutil.Chdir(t, tmpDir)
30+
2831
c := cmd.InitCommand(viper.New(), cfgFileName)
2932
out := &bytes.Buffer{}
3033
c.SetOut(out)
3134
c.SetErr(out)
3235
err := c.Execute()
33-
assert.Error(t, err)
36+
assert.NoError(t, err)
37+
38+
files, err := os.ReadDir(tmpDir)
39+
require.NoError(t, err)
40+
41+
for _, file := range files {
42+
assert.Contains(t, expectedFiles, file.Name())
43+
}
3444
})
3545

3646
t.Run("WithEmptyDirectory", func(t *testing.T) {
@@ -51,13 +61,65 @@ func TestInitCommand(t *testing.T) {
5161
}
5262
})
5363

64+
t.Run("WithExistingConfig", func(t *testing.T) {
65+
cfgData := []byte(`
66+
message:
67+
recipientDataCsvFile: data.csv
68+
recipientEmailColumnName: Recipient`)
69+
70+
tmpDir := t.TempDir()
71+
testutil.CreateFile(t, tmpDir, cfgFileName, string(cfgData))
72+
73+
v := viper.New()
74+
v.SetConfigName(".iris")
75+
v.SetConfigType("yaml")
76+
77+
c := cmd.InitCommand(v, cfgFileName)
78+
out := &bytes.Buffer{}
79+
c.SetOut(out)
80+
c.SetErr(out)
81+
c.SetArgs([]string{tmpDir})
82+
err := c.Execute()
83+
assert.NoError(t, err)
84+
85+
files, err := os.ReadDir(tmpDir)
86+
require.NoError(t, err)
87+
88+
for _, f := range files {
89+
assert.Contains(t, []string{
90+
"subject.txt",
91+
"body.txt",
92+
"body.html",
93+
"data.csv",
94+
".iris.yaml",
95+
}, f.Name())
96+
}
97+
98+
// must not change config
99+
gotCfgData, err := os.ReadFile(filepath.Join(tmpDir, cfgFileName))
100+
assert.NoError(t, err)
101+
assert.Equal(t, cfgData, gotCfgData)
102+
103+
// must generate files according to the given config
104+
bodyTxtData, err := os.ReadFile(filepath.Join(tmpDir, "body.txt"))
105+
assert.NoError(t, err)
106+
assert.Contains(t, string(bodyTxtData), "{{.Recipient}}")
107+
108+
bodyHtmlData, err := os.ReadFile(filepath.Join(tmpDir, "body.html"))
109+
assert.NoError(t, err)
110+
assert.Contains(t, string(bodyHtmlData), "{{.Recipient}}")
111+
112+
dataCsvData, err := os.ReadFile(filepath.Join(tmpDir, "data.csv"))
113+
assert.NoError(t, err)
114+
assert.Contains(t, string(dataCsvData), "Recipient")
115+
})
116+
54117
for file, content := range map[string]string{
55-
"subject.txt": "test-subject",
56-
"body.txt": "test-text-body",
57-
"body.html": "test-html-body",
58-
"default.csv": "test-default-csv",
59-
"recipient.csv": "test-recipient.csv",
60-
cfgFileName: "test-config",
118+
"subject.txt": "test-subject",
119+
"body.txt": "test-text-body",
120+
"body.html": "test-html-body",
121+
"default.csv": "test-default-csv",
122+
"recipients.csv": "test-recipient.csv",
61123
} {
62124
t.Run("WithExisting__"+file, func(t *testing.T) {
63125
tmpDir := t.TempDir()

0 commit comments

Comments
 (0)