Skip to content

Commit

Permalink
Support CSV for recipients
Browse files Browse the repository at this point in the history
CSV is a classical input format.
  • Loading branch information
Guilhem Bonnefille committed Nov 14, 2024
1 parent 2fe3ddd commit 14e409e
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 4 deletions.
8 changes: 8 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ type ConfigFile struct {
// Validation
DKIM map[string]interface{}

// CSV parsing
CSV CSVConfig

// Directories
ContentDir string
LayoutDir string
Expand All @@ -66,6 +69,11 @@ type BuildInfo struct {
BuildDate string
}

// Configuration for CSV parsing
type CSVConfig struct {
Comma string
}

func (i BuildInfo) String() string {
return fmt.Sprintf("v%s %s/%s (%s)", i.Version, runtime.GOOS, runtime.GOARCH, i.BuildDate)
}
Expand Down
11 changes: 10 additions & 1 deletion config/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"os"
"path/filepath"
"slices"
"strings"

"github.com/spf13/afero"
)

var (
contentExts = []string{".md"}
listExts = []string{".yaml", ".yml"}
listExts = []string{".yaml", ".yml", ".csv"}
)

type Fs struct {
Expand Down Expand Up @@ -99,3 +100,11 @@ func (f *Fs) isDir(dir string) bool {
s, err := f.Stat(dir)
return err == nil && s.IsDir()
}

func (f *Fs) IsYaml(path string) bool {
return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
}

func (f *Fs) IsCsv(path string) bool {
return strings.HasSuffix(path, ".csv")
}
26 changes: 26 additions & 0 deletions config/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package config

import "testing"

func TestIsYaml(t *testing.T) {
fs := Fs{}
if !fs.IsYaml("file.yaml") {
t.Error("file.yaml should be yaml")
}
if !fs.IsYaml("file.yml") {
t.Error("file.yml should be yaml")
}
if fs.IsYaml("file.json") {
t.Error("file.json should not be yaml")
}
}

func TestIsCsv(t *testing.T) {
fs := Fs{}
if !fs.IsCsv("file.csv") {
t.Error("file.csv should be csv")
}
if fs.IsYaml("file.json") {
t.Error("file.json should not be csv")
}
}
13 changes: 10 additions & 3 deletions mail/campaign.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"path/filepath"
"text/template"

"github.com/ghodss/yaml"
"github.com/go-gomail/gomail"
"github.com/jtacoma/uritemplates"
"github.com/microcosm-cc/bluemonday"
Expand Down Expand Up @@ -201,11 +200,19 @@ func parseRecipients(appFs *config.Fs, path string) ([]*ctxRecipient, error) {
fmt.Println("Loading recipients", path)
raw, err := afero.ReadFile(appFs, path)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to read file %s: %w", path, err)
}

var data []map[string]interface{}
if err := yaml.Unmarshal(raw, &data); err != nil {

if appFs.IsYaml(path) {
data, err = unmarshalYamlRecipients(appFs, raw)
} else if appFs.IsCsv(path) {
data, err = unmarshalCsvRecipients(appFs, raw)
} else {
return nil, fmt.Errorf("unsupported recipient format: %s", path)
}
if err != nil {
return nil, err
}

Expand Down
69 changes: 69 additions & 0 deletions mail/campaign_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package mail

import (
"path/filepath"
"testing"

"github.com/rykov/paperboy/config"
"github.com/spf13/afero"
)

func TestParseRecipientsCsv(t *testing.T) {
aFs := afero.NewMemMapFs()

// Write and load fake configuration
cPath, _ := filepath.Abs("./recipients.csv")
afero.WriteFile(aFs, cPath, []byte(`email,name,extra
[email protected],J Doe,Extra Data
`), 0644)

appFs := config.Fs{
Config: &config.AConfig{
ConfigFile: config.ConfigFile{
CSV: config.CSVConfig{
Comma: ",",
},
},
},
Fs: aFs,
}

recipients, err := parseRecipients(&appFs, cPath)
if err != nil {
t.Error(err)
}
if len(recipients) != 1 {
t.Errorf("Expected 1 recipient, got %d", len(recipients))
}
}

func TestParseRecipientsYaml(t *testing.T) {
aFs := afero.NewMemMapFs()

// Write and load fake configuration
cPath, _ := filepath.Abs("./recipients.yml")
afero.WriteFile(aFs, cPath, []byte(`---
- email: [email protected]
name: J Doe
extra: Extra Data
`), 0644)

appFs := config.Fs{
Config: &config.AConfig{
ConfigFile: config.ConfigFile{
CSV: config.CSVConfig{
Comma: ",",
},
},
},
Fs: aFs,
}

recipients, err := parseRecipients(&appFs, cPath)
if err != nil {
t.Error(err)
}
if len(recipients) != 1 {
t.Errorf("Expected 1 recipient, got %d", len(recipients))
}
}
51 changes: 51 additions & 0 deletions mail/recipients_csv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package mail

import (
"bytes"
"encoding/csv"
"io"

"github.com/rykov/paperboy/config"
)

func unmarshalCsvRecipients(appFs *config.Fs, raw []byte) ([]map[string]interface{}, error) {
var data []map[string]interface{}

// CSV
csvReader := newCSVReader(appFs.Config.ConfigFile.CSV, bytes.NewReader(raw))

// Read header
header, err := csvReader.Read()
if err != nil {
return nil, err
}

// Read CSV line by line
for {
record, err := csvReader.Read()
if err == io.EOF {
break
} else if err != nil {
return nil, err
}

// Create a map for each record
rec := make(map[string]interface{})
for i, h := range header {
rec[h] = record[i]
}

data = append(data, rec)
}

return data, nil
}

func newCSVReader(cfg config.CSVConfig, r io.Reader) *csv.Reader {
csvReader := csv.NewReader(r)
// Deal with separator
if len(cfg.Comma) > 0 {
csvReader.Comma = []rune(cfg.Comma)[0]
}
return csvReader
}
31 changes: 31 additions & 0 deletions mail/recipients_csv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package mail

import (
"testing"

"github.com/rykov/paperboy/config"
)

func TestUnmarshalCsv(t *testing.T) {
content := []byte(`email,name,extra
[email protected],J Doe,Extra Data
`)

appFs := config.Fs{
Config: &config.AConfig{
ConfigFile: config.ConfigFile{
CSV: config.CSVConfig{
Comma: ",",
},
},
},
}

recipients, err := unmarshalCsvRecipients(&appFs, content)
if err != nil {
t.Error(err)
}
if len(recipients) != 1 {
t.Errorf("Expected 1 recipient, got %d", len(recipients))
}
}
16 changes: 16 additions & 0 deletions mail/recipients_yaml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package mail

import (
"github.com/ghodss/yaml"
"github.com/rykov/paperboy/config"
)

func unmarshalYamlRecipients(appFs *config.Fs, raw []byte) ([]map[string]interface{}, error) {
var data []map[string]interface{}

if err := yaml.Unmarshal(raw, &data); err != nil {
return nil, err
}

return data, nil
}

0 comments on commit 14e409e

Please sign in to comment.