From a81f0280547d4ace492a33f69375edafb0b52b46 Mon Sep 17 00:00:00 2001 From: Ludovico Russo Date: Sat, 30 Mar 2024 21:08:49 +0100 Subject: [PATCH] wip: start refactoring using ddd --- pkg/domain/template/template.go | 48 +++++++++++++++++++++++ pkg/sendingpool/email.go | 35 +++++++++++++++++ pkg/sendingpool/sendingpool.go | 20 ++++++++++ pkg/values/email/email.go | 67 ++++++++++++++++++++++++++++++++ pkg/values/email/email_test.go | 68 +++++++++++++++++++++++++++++++++ pkg/values/fqdn/fqdn.go | 32 ++++++++++++++++ pkg/values/fqdn/fqdn_test.go | 46 ++++++++++++++++++++++ pkg/values/id/id.go | 59 ++++++++++++++++++++++++++++ pkg/values/id/id_test.go | 55 ++++++++++++++++++++++++++ pkg/values/meta/meta.go | 23 +++++++++++ pkg/values/ref/ref.go | 43 +++++++++++++++++++++ 11 files changed, 496 insertions(+) create mode 100644 pkg/domain/template/template.go create mode 100644 pkg/sendingpool/email.go create mode 100644 pkg/sendingpool/sendingpool.go create mode 100644 pkg/values/email/email.go create mode 100644 pkg/values/email/email_test.go create mode 100644 pkg/values/fqdn/fqdn.go create mode 100644 pkg/values/fqdn/fqdn_test.go create mode 100644 pkg/values/id/id.go create mode 100644 pkg/values/id/id_test.go create mode 100644 pkg/values/meta/meta.go create mode 100644 pkg/values/ref/ref.go diff --git a/pkg/domain/template/template.go b/pkg/domain/template/template.go new file mode 100644 index 0000000..514eede --- /dev/null +++ b/pkg/domain/template/template.go @@ -0,0 +1,48 @@ +package template + +import ( + "github.com/ludusrusso/kannon/pkg/values/fqdn" + "github.com/ludusrusso/kannon/pkg/values/meta" + "github.com/ludusrusso/kannon/pkg/values/ref" +) + +type Type string + +const ( + TemplateTypeTransient Type = "transient" + TemplateTypeTemplate Type = "template" +) + +type Template struct { + meta.Meta + ref.Ref + html string + tType Type +} + +func NewTemplate(domain fqdn.FQDN, tType Type, title, description string) (Template, error) { + ref, err := ref.NewRef("template", domain) + if err != nil { + return Template{}, err + } + + return Template{ + Meta: meta.NewMeta(title, description), + Ref: ref, + tType: tType, + html: "", + }, nil +} + +func (t Template) Type() Type { + return t.tType +} + +func (t Template) HTML() string { + return t.html +} + +func (t *Template) SetHTML(html string) { + t.html = html + t.Update() +} diff --git a/pkg/sendingpool/email.go b/pkg/sendingpool/email.go new file mode 100644 index 0000000..d200458 --- /dev/null +++ b/pkg/sendingpool/email.go @@ -0,0 +1,35 @@ +package sendingpool + +import ( + "time" + + "github.com/ludusrusso/kannon/pkg/values/email" + "github.com/ludusrusso/kannon/pkg/values/id" +) + +type CustomFields map[string]string + +type SendingPoolEmailStatus string + +const ( + SendingPoolEmailStatusInitializing SendingPoolEmailStatus = "initializing" + SendingPoolEmailStatusToValidate SendingPoolEmailStatus = "to_validate" + SendingPoolEmailStatusValidating SendingPoolEmailStatus = "validating" + SendingPoolEmailStatusSending SendingPoolEmailStatus = "sending" + SendingPoolEmailStatusSent SendingPoolEmailStatus = "sent" + SendingPoolEmailStatusScheduled SendingPoolEmailStatus = "scheduled" + SendingPoolEmailStatusError SendingPoolEmailStatus = "error" +) + +type PoolEmail struct { + ID int32 + ScheduledTime time.Time + OriginalScheduledTime time.Time + SendAttemptsCnt int32 + Email email.Email + MessageID id.ID + Fields CustomFields + Status SendingPoolEmailStatus + CreatedAt time.Time + Domain string +} diff --git a/pkg/sendingpool/sendingpool.go b/pkg/sendingpool/sendingpool.go new file mode 100644 index 0000000..a173f98 --- /dev/null +++ b/pkg/sendingpool/sendingpool.go @@ -0,0 +1,20 @@ +package sendingpool + +import ( + "github.com/ludusrusso/kannon/pkg/values/email" + "github.com/ludusrusso/kannon/pkg/values/fqdn" + "github.com/ludusrusso/kannon/pkg/values/id" +) + +type Sender interface { + Email() email.Email + Alias() string +} + +type PoolMessage struct { + MessageID id.ID + Subject string + Sender Sender + TemplateID id.ID + Domain fqdn.FQDN +} diff --git a/pkg/values/email/email.go b/pkg/values/email/email.go new file mode 100644 index 0000000..ab7df86 --- /dev/null +++ b/pkg/values/email/email.go @@ -0,0 +1,67 @@ +package email + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +var ErrInvalidEmail = errors.New("invalid email") + +type Email string + +func NewEmail(v string) (Email, error) { + if !isValidEmail(v) { + return "", fmt.Errorf("%w: %s", ErrInvalidEmail, v) + } + return Email(v), nil +} + +func NewEmailFromPtr(v *string) (*Email, error) { + if v == nil { + return nil, nil + } + + email, err := NewEmail(*v) + if err != nil { + return nil, err + } + + return &email, nil +} + +func (e Email) String() string { + return string(e) +} + +func (e Email) Username() string { + return strings.Split(e.String(), "@")[0] +} + +func (e Email) Domain() string { + return strings.Split(e.String(), "@")[1] +} + +var reg = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`) + +func isValidEmail(email string) bool { + return reg.MatchString(email) +} + +func SafeToString(e *Email) string { + if e == nil { + return "" + } + + return e.String() +} + +func SafeToStringPtr(e *Email) *string { + if e == nil { + return nil + } + + v := e.String() + return &v +} diff --git a/pkg/values/email/email_test.go b/pkg/values/email/email_test.go new file mode 100644 index 0000000..6e839da --- /dev/null +++ b/pkg/values/email/email_test.go @@ -0,0 +1,68 @@ +package email + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestEmailVaidation(t *testing.T) { + tt := []struct { + email string + expectErr bool + }{ + { + email: "", + expectErr: true, + }, + { + email: "test", + expectErr: true, + }, + { + email: "test@", + expectErr: true, + }, + { + email: "test@test", + expectErr: true, + }, + { + email: "test@test.", + expectErr: true, + }, + { + email: "test@test.c", + expectErr: true, + }, + { + email: "test@emai.com", + expectErr: false, + }, + } + + for _, tc := range tt { + t.Run(tc.email, func(t *testing.T) { + _, err := NewEmail(tc.email) + if tc.expectErr { + if err == nil { + t.Errorf("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + } + }) + } +} + +func TestUsername(t *testing.T) { + email, err := NewEmail("test@test.com") + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + assert.Equal(t, email.Username(), "test") + assert.Equal(t, email.Domain(), "test.com") +} diff --git a/pkg/values/fqdn/fqdn.go b/pkg/values/fqdn/fqdn.go new file mode 100644 index 0000000..612e9ce --- /dev/null +++ b/pkg/values/fqdn/fqdn.go @@ -0,0 +1,32 @@ +package fqdn + +import ( + "fmt" + "regexp" +) + +type FQDN string + +var ErrInvalidFQDN = fmt.Errorf("invalid FQDN") + +// NewFQDN creates a new FQDN +func NewFQDN(fqdn string) (FQDN, error) { + res := FQDN(fqdn) + if !res.IsValid() { + return "", fmt.Errorf("%w: %s", ErrInvalidFQDN, fqdn) + } + return res, nil +} + +// String returns the string representation of the FQDN +func (f FQDN) String() string { + return string(f) +} + +// IsValid checks if the FQDN is valid using regex +var fqdnRegex = regexp.MustCompile(`^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$`) + +// IsValid checks if the FQDN is valid +func (f FQDN) IsValid() bool { + return fqdnRegex.MatchString(f.String()) +} diff --git a/pkg/values/fqdn/fqdn_test.go b/pkg/values/fqdn/fqdn_test.go new file mode 100644 index 0000000..33b21cc --- /dev/null +++ b/pkg/values/fqdn/fqdn_test.go @@ -0,0 +1,46 @@ +package fqdn + +import "testing" + +func TestValidateFQDN(t *testing.T) { + tt := []struct { + fqdn string + expectedErr error + }{ + { + fqdn: "test", + expectedErr: ErrInvalidFQDN, + }, + { + fqdn: "test.com", + expectedErr: nil, + }, + { + fqdn: "test.com.", + expectedErr: ErrInvalidFQDN, + }, + { + fqdn: "exp.test.com", + expectedErr: nil, + }, + { + fqdn: "exp.test.com.", + expectedErr: ErrInvalidFQDN, + }, + } + + for _, tc := range tt { + t.Run(tc.fqdn, func(t *testing.T) { + _, err := NewFQDN(tc.fqdn) + if tc.expectedErr != nil { + if err == nil { + t.Errorf("expected error, got nil") + } + } else { + if err != nil { + t.Errorf("expected no error, got %v", err) + } + } + }) + } +} diff --git a/pkg/values/id/id.go b/pkg/values/id/id.go new file mode 100644 index 0000000..8a096e8 --- /dev/null +++ b/pkg/values/id/id.go @@ -0,0 +1,59 @@ +package id + +import ( + "crypto/rand" + "errors" + "fmt" + + "github.com/lucsky/cuid" + "github.com/ludusrusso/kannon/pkg/values/fqdn" +) + +type ID string + +func (id ID) String() string { + return string(id) +} + +func (id ID) IsEmpty() bool { + return id == "" +} + +func (id ID) Validate() error { + if id.IsEmpty() { + return ErrEmptyID + } + return nil +} + +var ( + ErrCannotCreateID = errors.New("cannot create id") + ErrEmptyID = errors.New("empty id") +) + +func FromString(id string) ID { + return ID(id) +} + +func CreateID(prefix string) (ID, error) { + id, err := cuid.NewCrypto(rand.Reader) + if err != nil { + return "", fmt.Errorf("%w: %w", ErrCannotCreateID, err) + } + + return ID(fmt.Sprintf("%s_%s", prefix, id)), nil +} + +func CreateScopedID(prefix string, d fqdn.FQDN) (ID, error) { + id, err := CreateID(prefix) + if err != nil { + return "", err + } + + return ID(fmt.Sprintf("%s@%s", id, d)), nil +} + +func NewID(id string) (ID, error) { + i := ID(id) + return i, i.Validate() +} diff --git a/pkg/values/id/id_test.go b/pkg/values/id/id_test.go new file mode 100644 index 0000000..ceccd34 --- /dev/null +++ b/pkg/values/id/id_test.go @@ -0,0 +1,55 @@ +package id + +import ( + "strings" + "testing" + + "github.com/ludusrusso/kannon/pkg/values/fqdn" + "github.com/stretchr/testify/assert" +) + +func TestCreateID(t *testing.T) { + id, err := CreateID("test") + if err != nil { + t.Errorf("CreateID() error = %v, want nil", err) + } + + assert.NotEmpty(t, id) +} + +func TestInvalidID(t *testing.T) { + id := ID("") + + t.Run("ID Should Be empty", func(t *testing.T) { + assert.True(t, id.IsEmpty()) + }) + + t.Run("ID Should Not Be Valid", func(t *testing.T) { + err := id.Validate() + assert.Error(t, err) + assert.Equal(t, ErrEmptyID, err) + }) +} + +func TestValidID(t *testing.T) { + id := ID("test") + + t.Run("ID Should Be empty", func(t *testing.T) { + assert.False(t, id.IsEmpty()) + }) + + t.Run("ID Should Not Be Valid", func(t *testing.T) { + err := id.Validate() + assert.NoError(t, err) + }) +} + +func TestCreateScopedID(t *testing.T) { + id, err := CreateScopedID("test", fqdn.FQDN("example.com")) + if err != nil { + t.Errorf("CreateScopedID() error = %v, want nil", err) + } + + assert.True(t, strings.HasPrefix(id.String(), "test_"), "should start with test_") + assert.True(t, strings.HasSuffix(id.String(), "@example.com"), "should end with @example.com") +} diff --git a/pkg/values/meta/meta.go b/pkg/values/meta/meta.go new file mode 100644 index 0000000..553b2bc --- /dev/null +++ b/pkg/values/meta/meta.go @@ -0,0 +1,23 @@ +package meta + +import "time" + +type Meta struct { + Title string + Desciption string + CreatedAt time.Time + UpdatedAt time.Time +} + +func NewMeta(title, description string) Meta { + return Meta{ + Title: title, + Desciption: description, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } +} + +func (m *Meta) Update() { + m.UpdatedAt = time.Now() +} diff --git a/pkg/values/ref/ref.go b/pkg/values/ref/ref.go new file mode 100644 index 0000000..ae6562f --- /dev/null +++ b/pkg/values/ref/ref.go @@ -0,0 +1,43 @@ +package ref + +import ( + "fmt" + + "github.com/ludusrusso/kannon/pkg/values/fqdn" + "github.com/ludusrusso/kannon/pkg/values/id" +) + +type ref struct { + id id.ID + fqdn fqdn.FQDN +} + +type Ref interface { + ID() id.ID + FQDN() fqdn.FQDN + String() string +} + +func (r ref) ID() id.ID { + return r.id +} + +func (r ref) FQDN() fqdn.FQDN { + return r.fqdn +} + +func (r ref) String() string { + return fmt.Sprintf("%s@%s", r.id, r.fqdn) +} + +func NewRef(prefix string, fqdn fqdn.FQDN) (Ref, error) { + i, err := id.CreateID(prefix) + if err != nil { + return nil, err + } + + return ref{ + id: i, + fqdn: fqdn, + }, nil +}