Skip to content

Commit

Permalink
Refactor and cleanup (#9)
Browse files Browse the repository at this point in the history
* Refactor some more
  • Loading branch information
kkoutsilis authored Jan 17, 2024
1 parent e372d7e commit c18462e
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 51 deletions.
124 changes: 77 additions & 47 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"gopkg.in/gomail.v2"
)

var version string = "0.1.0"
var version string = "0.2.0"

type Data struct {
Name string `json:"name"`
Expand Down Expand Up @@ -50,7 +50,7 @@ func generateSecretSantaMatches(data []Data) []MatchPair {
}

func circlularMatchingAlgorithm(data []Data) []MatchPair {
var matches []MatchPair
var matches = make([]MatchPair, 0, len(data))
for i := 0; i < len(data); i++ {
from := data[i]
to := data[(i+1)%len(data)]
Expand All @@ -59,7 +59,54 @@ func circlularMatchingAlgorithm(data []Data) []MatchPair {
return matches
}

func sendEmail(to, subject, body string) error {
func createEmailMessage(to, body string) *gomail.Message {
subject := "Your Secret Santa Match!"

message := gomail.NewMessage()

message.SetHeader("From", os.Getenv("EMAIL_FROM"))
message.SetHeader("To", to)
message.SetHeader("Subject", subject)
message.SetBody("text/html", body)

return message

}

func populateEmailBody(match MatchPair, tmpl *template.Template) (string, error) {
var emailBodyBuffer = &strings.Builder{}
err := tmpl.Execute(emailBodyBuffer, match)
if err != nil {
return "", errors.Wrap(err, "failed to execute template for email body")
}
emailBody := emailBodyBuffer.String()

return emailBody, nil
}

func loadEmailTemplate(filePath string) (*template.Template, error) {
tmpl, err := template.ParseFiles(filePath)
if err != nil {
return nil, errors.Wrap(err, "failed to parse email template")
}
return tmpl, nil
}

func generateEmailMessages(matches []MatchPair, tmpl *template.Template) ([]*gomail.Message, error) {

emailMessages := make([]*gomail.Message, 0, len(matches))
for _, match := range matches {
emailBody, err := populateEmailBody(match, tmpl)
if err != nil {
return nil, errors.Wrap(err, "failed to generate email body")
}
emailMessages = append(emailMessages, createEmailMessage(match.From.Email, emailBody))
}

return emailMessages, nil
}

func sendEmails(emailMessages ...*gomail.Message) error {
host := os.Getenv("EMAIL_HOST")
strPort := os.Getenv("EMAIL_PORT")
user := os.Getenv("EMAIL_USER")
Expand All @@ -75,45 +122,14 @@ func sendEmail(to, subject, body string) error {
if err != nil {
return errors.Wrap(err, "error dialing email server")
}
defer s.Close()

m := gomail.NewMessage()
m.SetHeader("From", os.Getenv("EMAIL_FROM"))
m.SetHeader("To", to)
m.SetHeader("Subject", subject)
m.SetBody("text/html", body)

return gomail.Send(s, m)
}

func sendEmails(matches []MatchPair) ([]string, error) {
// TODO: add retry logic for failed emails (maybe use a queue)
// instead of sending each email individually,
// generate all the emails and send them in bulk to avoid individual errors
// or prompt the user to retry
emailIssues := make([]string, 0, len(matches))
subject := "Your Secret Santa Match!"

templateFile := "./templates/email_template.html"
tmpl, err := template.ParseFiles(templateFile)
err = gomail.Send(s, emailMessages...)
if err != nil {
return emailIssues, errors.Wrap(err, "failed to parse email template")
return err
}
return nil

for _, match := range matches {
var emailBodyBuffer = &strings.Builder{}
err := tmpl.Execute(emailBodyBuffer, match)
if err != nil {
emailIssues = append(emailIssues, fmt.Sprintf("failed to execute template for %s: %v", match.From.Email, err))
continue

}
emailBody := emailBodyBuffer.String()

if err := sendEmail(match.From.Email, subject, emailBody); err != nil {
emailIssues = append(emailIssues, fmt.Sprintf("failed to send email to %s: %v", match.From.Email, err))
}
}
return emailIssues, nil
}

var rootCmd = &cobra.Command{
Expand All @@ -123,14 +139,22 @@ var rootCmd = &cobra.Command{
Version: version,
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
defaultValues := struct {
FilePath string
TemplatePath string
}{
FilePath: "data.json",
TemplatePath: "./templates/email_template.html",
}

var filePath string
if len(args) == 0 {
filePath = "data.json"
filePath = defaultValues.FilePath
} else {
filePath = args[0]
}
if filePath == "" {
filePath = "data.json"
filePath = defaultValues.FilePath
}

if !checkFileExists(filePath) {
Expand Down Expand Up @@ -158,15 +182,21 @@ var rootCmd = &cobra.Command{
}

matches := generateSecretSantaMatches(payload)
emailIssues, err := sendEmails(matches)
// TODO: Give users the option to use their own template
templateFilePath := defaultValues.TemplatePath
tmpl, err := loadEmailTemplate(templateFilePath)
if err != nil {
return errors.Wrap(err, "error sending emails")
return errors.Wrap(err, "error loading email template")
}
if len(emailIssues) > 0 {
for _, issue := range emailIssues {
fmt.Println(issue)
}
fmt.Println("Some emails failed to send, please check the logs for more details")

emailMessages, err := generateEmailMessages(matches, tmpl)
if err != nil {
return errors.Wrap(err, "error generating email messages")
}

err = sendEmails(emailMessages...)
if err != nil {
return errors.Wrap(err, "error sending emails")
}

return nil
Expand Down
75 changes: 72 additions & 3 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package cmd

import (
"html/template"
"reflect"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheckFileExistsReturnsTrueWhenFileExists(t *testing.T) {
Expand Down Expand Up @@ -31,7 +34,7 @@ func TestCheckIsJsonReturnsFalseWhenFileIsNotJson(t *testing.T) {
}

func TestGenerateSecretSantaMachesReturnsAppropriateLenghtOfMatchPairs(t *testing.T) {
var payload []Data
payload := make([]Data, 0, 3)
payload = append(payload, Data{Name: "A", Email: "[email protected]"})
payload = append(payload, Data{Name: "B", Email: "[email protected]"})
payload = append(payload, Data{Name: "C", Email: "[email protected]"})
Expand All @@ -44,12 +47,12 @@ func TestGenerateSecretSantaMachesReturnsAppropriateLenghtOfMatchPairs(t *testin
}

func TestCircularMatchingAlgorithmReturnsExpectedMatchPairs(t *testing.T) {
var payload []Data
payload := make([]Data, 0, 3)
payload = append(payload, Data{Name: "A", Email: "[email protected]"})
payload = append(payload, Data{Name: "B", Email: "[email protected]"})
payload = append(payload, Data{Name: "C", Email: "[email protected]"})

var expected []MatchPair
expected := make([]MatchPair, 0, 3)

expected = append(expected, MatchPair{From: payload[0], To: payload[1]})
expected = append(expected, MatchPair{From: payload[1], To: payload[2]})
Expand All @@ -62,3 +65,69 @@ func TestCircularMatchingAlgorithmReturnsExpectedMatchPairs(t *testing.T) {
}

}

func TestPopulateEmailBodyReturnsExpectedString(t *testing.T) {
p1 := Data{Name: "A", Email: "[email protected]"}
p2 := Data{Name: "B", Email: "[email protected]"}

matchPair := MatchPair{From: p1, To: p2}

tmpl, err := template.New("test").Parse("Hi {{.From.Name}} your Secret Santa match is {{.To.Name}}!")
if err != nil {
t.Error("Expected template to be parsed without error")
}

emailBody, err := populateEmailBody(matchPair, tmpl)

if err != nil {
t.Error("Expected populateEmailBody to not return an error")
}

assert.Equal(t, "Hi A your Secret Santa match is B!", emailBody)

}

func TestPopulateEmailBodyReturnsErrorWhenInvalidTemplateProvided(t *testing.T) {
p1 := Data{Name: "A", Email: "[email protected]"}
p2 := Data{Name: "B", Email: "[email protected]"}

matchPair := MatchPair{From: p1, To: p2}

tmpl, err := template.New("test").Parse("Hi {{.Something.Unexpected}}!")
if err != nil {
t.Error("Expected template to be parsed without error")
}

emailBody, err := populateEmailBody(matchPair, tmpl)

assert.Equal(t, "", emailBody)
if err == nil {
t.Error("Expected populateEmailBody to return an error when invalid template is provided")
}

}

func TestGenerateEmailMessagesReturnsExpectedNumberOfMessages(t *testing.T) {
payload := make([]Data, 0, 3)
payload = append(payload, Data{Name: "A", Email: "[email protected]"})
payload = append(payload, Data{Name: "B", Email: "[email protected]"})
payload = append(payload, Data{Name: "C", Email: "[email protected]"})

matchPairs := make([]MatchPair, 0, 3)

matchPairs = append(matchPairs, MatchPair{From: payload[0], To: payload[1]})
matchPairs = append(matchPairs, MatchPair{From: payload[1], To: payload[2]})
matchPairs = append(matchPairs, MatchPair{From: payload[2], To: payload[0]})

tmpl, err := template.New("test").Parse("Hi {{.From.Name}} your Secret Santa match is {{.To.Name}}!")
if err != nil {
t.Error("Expected template to be parsed without error")
}
messages, err := generateEmailMessages(matchPairs, tmpl)

if err != nil {
t.Error("Expected generateEmailMessages to not return an error")
}

assert.Equal(t, len(matchPairs), len(messages))
}
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ go 1.21.4

require github.com/spf13/cobra v1.8.0

require github.com/pkg/errors v0.9.1 // indirect
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
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/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 comments on commit c18462e

Please sign in to comment.