Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support CSV for recipients #43

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading