parent
71192c3cfc
commit
693748ee37
|
@ -0,0 +1,52 @@
|
|||
package sketchbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.l--n.de/nclazz/soundwerft/internal/app/types"
|
||||
"git.l--n.de/nclazz/soundwerft/pkg/errx"
|
||||
)
|
||||
|
||||
type CreateSketchCommand struct {
|
||||
Sketchbook string
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (s *Service) CreateSketch(ctx context.Context, cmd CreateSketchCommand) (*Sketch, error) {
|
||||
var input struct {
|
||||
sketchbook types.ID
|
||||
title Title
|
||||
description Description
|
||||
}
|
||||
{
|
||||
var errs errx.ErrMap
|
||||
var err error
|
||||
input.sketchbook, err = types.ParseId(cmd.Sketchbook)
|
||||
if err != nil {
|
||||
errs.Set("sketchbook", err)
|
||||
}
|
||||
input.title, err = ParseTitle(cmd.Title)
|
||||
if err != nil {
|
||||
errs.Set("title", err)
|
||||
}
|
||||
input.description, err = ParseDescription(cmd.Description)
|
||||
if err != nil {
|
||||
errs.Set("description", err)
|
||||
}
|
||||
if errs != nil {
|
||||
return nil, errs
|
||||
}
|
||||
}
|
||||
sketchbook, err := s.repo.GetByID(ctx, input.sketchbook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sketchbook == nil {
|
||||
return nil, ErrSketchbookNotFound
|
||||
}
|
||||
sketch := NewSketch(sketchbook.ID, input.title, input.description)
|
||||
if err := s.repo.AddSketch(ctx, sketch); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sketch, nil
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package sketchbook
|
||||
|
||||
import (
|
||||
"context"
|
||||
"git.l--n.de/nclazz/soundwerft/internal/app/types"
|
||||
"git.l--n.de/nclazz/soundwerft/internal/app/user"
|
||||
"git.l--n.de/nclazz/soundwerft/pkg/errx"
|
||||
"git.l--n.de/nclazz/soundwerft/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestService_CreateSketch(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
repo := NewInMemoryRepository()
|
||||
userRepo := user.NewInMemoryRepository()
|
||||
svc := NewService(repo, userRepo)
|
||||
|
||||
sketchbook := &Sketchbook{
|
||||
ID: types.GenerateId(),
|
||||
Owner: types.GenerateId(),
|
||||
Audit: types.NewAudit(),
|
||||
}
|
||||
repo.sketchbooks[sketchbook.ID] = sketchbook
|
||||
|
||||
t.Run("returns validation errors for invalid input", func(t *testing.T) {
|
||||
repo.Reset()
|
||||
userRepo.Reset()
|
||||
sk, err := svc.CreateSketch(ctx, CreateSketchCommand{
|
||||
Sketchbook: "invalid",
|
||||
Title: "",
|
||||
Description: utils.RandomString(DescriptionMaxLength + 1),
|
||||
})
|
||||
assert.Nil(t, sk)
|
||||
var errs errx.ErrMap
|
||||
if assert.ErrorAs(t, err, &errs) {
|
||||
assert.Error(t, errs.Get("sketchbook"))
|
||||
assert.Error(t, errs.Get("title"))
|
||||
assert.Error(t, errs.Get("description"))
|
||||
}
|
||||
})
|
||||
t.Run("returns ErrSketchbookNotFound for non existent sketchbook", func(t *testing.T) {
|
||||
repo.Reset()
|
||||
userRepo.Reset()
|
||||
sk, err := svc.CreateSketch(ctx, CreateSketchCommand{
|
||||
Sketchbook: types.GenerateId().String(),
|
||||
Title: "My Sketch",
|
||||
Description: "The sketch description",
|
||||
})
|
||||
assert.Nil(t, sk)
|
||||
assert.ErrorIs(t, err, ErrSketchbookNotFound)
|
||||
})
|
||||
|
||||
t.Run("creates sketch and adds it to repository", func(t *testing.T) {
|
||||
repo.Reset()
|
||||
userRepo.Reset()
|
||||
repo.sketchbooks[sketchbook.ID] = sketchbook
|
||||
sk, err := svc.CreateSketch(ctx, CreateSketchCommand{
|
||||
Sketchbook: sketchbook.ID.String(),
|
||||
Title: "My Sketch",
|
||||
Description: "Sketch description",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
if assert.NotNil(t, sk) {
|
||||
assert.False(t, sk.ID.IsNil())
|
||||
assert.Equal(t, Title("My Sketch"), sk.Title)
|
||||
assert.Equal(t, Description("Sketch description"), sk.Description)
|
||||
}
|
||||
assert.Len(t, repo.sketches, 1)
|
||||
})
|
||||
}
|
|
@ -8,4 +8,8 @@ import (
|
|||
var (
|
||||
ErrSketchbookError = errors.New("sketchbook")
|
||||
ErrOwnerAlreadyHasSketchbook = fmt.Errorf("%w: owner already has sketchbook", ErrSketchbookError)
|
||||
ErrTitleTooLong = fmt.Errorf("%w: title too long", ErrSketchbookError)
|
||||
ErrEmptyTitle = fmt.Errorf("%w: title can not be empty", ErrSketchbookError)
|
||||
ErrDescriptionTooLong = fmt.Errorf("%w: description too long", ErrSketchbookError)
|
||||
ErrSketchbookNotFound = fmt.Errorf("%w: sketchbook not found", ErrSketchbookError)
|
||||
)
|
||||
|
|
|
@ -6,10 +6,12 @@ import (
|
|||
)
|
||||
|
||||
type Reader interface {
|
||||
GetByID(ctx context.Context, id types.ID) (*Sketchbook, error)
|
||||
GetByOwnerID(ctx context.Context, owner types.ID) (*Sketchbook, error)
|
||||
}
|
||||
type Writer interface {
|
||||
AddSketchbook(ctx context.Context, sk *Sketchbook) error
|
||||
AddSketch(ctx context.Context, sketch *Sketch) error
|
||||
}
|
||||
type ReadWriter interface {
|
||||
Reader
|
||||
|
@ -20,6 +22,7 @@ var _ ReadWriter = &InMemoryRepository{}
|
|||
|
||||
type InMemoryRepository struct {
|
||||
sketchbooks map[types.ID]*Sketchbook
|
||||
sketches map[types.ID]*Sketch
|
||||
}
|
||||
|
||||
func NewInMemoryRepository(initialSketchbooks ...Sketchbook) *InMemoryRepository {
|
||||
|
@ -46,6 +49,17 @@ func (i *InMemoryRepository) AddSketchbook(ctx context.Context, sk *Sketchbook)
|
|||
return nil
|
||||
}
|
||||
|
||||
func (i *InMemoryRepository) GetByID(ctx context.Context, id types.ID) (*Sketchbook, error) {
|
||||
sk := i.sketchbooks[id]
|
||||
return sk, nil
|
||||
}
|
||||
|
||||
func (i *InMemoryRepository) AddSketch(ctx context.Context, sketch *Sketch) error {
|
||||
i.sketches[sketch.ID] = sketch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *InMemoryRepository) Reset() {
|
||||
i.sketchbooks = make(map[types.ID]*Sketchbook)
|
||||
i.sketches = make(map[types.ID]*Sketch)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
package sketchbook
|
||||
|
||||
import "git.l--n.de/nclazz/soundwerft/internal/app/types"
|
||||
import (
|
||||
"git.l--n.de/nclazz/soundwerft/internal/app/types"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
TitleMaxLength = 128
|
||||
DescriptionMaxLength = 4069
|
||||
)
|
||||
|
||||
type Sketchbook struct {
|
||||
ID types.ID
|
||||
|
@ -15,3 +24,53 @@ func New(owner types.ID) *Sketchbook {
|
|||
Audit: types.NewAudit(),
|
||||
}
|
||||
}
|
||||
|
||||
type Sketch struct {
|
||||
ID types.ID
|
||||
Sketchbook types.ID
|
||||
Title Title
|
||||
Description Description
|
||||
types.Audit
|
||||
}
|
||||
|
||||
func NewSketch(sketchbook types.ID, title Title, description Description) *Sketch {
|
||||
return &Sketch{
|
||||
ID: types.GenerateId(),
|
||||
Sketchbook: sketchbook,
|
||||
Title: title,
|
||||
Description: description,
|
||||
Audit: types.NewAudit(),
|
||||
}
|
||||
}
|
||||
|
||||
type Title string
|
||||
|
||||
func ParseTitle(title string) (Title, error) {
|
||||
trimmed := strings.TrimSpace(title)
|
||||
length := utf8.RuneCountInString(trimmed)
|
||||
if length > TitleMaxLength {
|
||||
return "", ErrTitleTooLong
|
||||
}
|
||||
if length < 1 {
|
||||
return "", ErrEmptyTitle
|
||||
}
|
||||
return Title(trimmed), nil
|
||||
}
|
||||
|
||||
func (t Title) String() string {
|
||||
return string(t)
|
||||
}
|
||||
|
||||
type Description string
|
||||
|
||||
func ParseDescription(description string) (Description, error) {
|
||||
trimmed := strings.TrimSpace(description)
|
||||
if utf8.RuneCountInString(trimmed) > DescriptionMaxLength {
|
||||
return "", ErrDescriptionTooLong
|
||||
}
|
||||
return Description(trimmed), nil
|
||||
}
|
||||
|
||||
func (d Description) String() string {
|
||||
return string(d)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@ CREATE INDEX IF NOT EXISTS users_name ON users (name);
|
|||
CREATE INDEX IF NOT EXISTS users_email ON users (email);
|
||||
|
||||
-- sketchbooks
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sketchbooks
|
||||
(
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
|
@ -22,3 +21,15 @@ CREATE TABLE IF NOT EXISTS sketchbooks
|
|||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sketches
|
||||
(
|
||||
id CHAR(36) NOT NULL PRIMARY KEY,
|
||||
sketchbook CHAR(36) NOT NULL
|
||||
REFERENCES sketchbooks(id)
|
||||
ON DELETE CASCADE,
|
||||
title VARCHAR(128) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP,
|
||||
updated_at TIMESTAMP
|
||||
);
|
||||
|
|
|
@ -45,6 +45,29 @@ func (s *SketchbookRepository) AddSketchbook(ctx context.Context, sk *sketchbook
|
|||
return err
|
||||
}
|
||||
|
||||
func (s *SketchbookRepository) GetByID(ctx context.Context, id types.ID) (*sketchbook.Sketchbook, error) {
|
||||
query := `SELECT * FROM sketchbooks where id = $1`
|
||||
var dto SketchbookDTO
|
||||
err := s.dbx.Get(&dto, query, id.String())
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dto.ToSketchbook(), nil
|
||||
}
|
||||
|
||||
func (s *SketchbookRepository) AddSketch(ctx context.Context, sketch *sketchbook.Sketch) error {
|
||||
if sketch == nil {
|
||||
return repository.ErrNoDataProvided
|
||||
}
|
||||
query := `INSERT INTO sketches VALUES(:id,:sketchbook,:title,:description,:created_at,:updated_at)`
|
||||
dto := NewSketchDTO(sketch)
|
||||
_, err := s.dbx.NamedExec(query, dto)
|
||||
return err
|
||||
}
|
||||
|
||||
type SketchbookDTO struct {
|
||||
ID string `db:"id"`
|
||||
Owner string `db:"owner"`
|
||||
|
@ -71,3 +94,36 @@ func (s SketchbookDTO) ToSketchbook() *sketchbook.Sketchbook {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
type SketchDTO struct {
|
||||
ID string `db:"id"`
|
||||
Sketchbook string `db:"sketchbook"`
|
||||
Title string `db:"title"`
|
||||
Description string `db:"description"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
}
|
||||
|
||||
func NewSketchDTO(sk *sketchbook.Sketch) SketchDTO {
|
||||
return SketchDTO{
|
||||
ID: sk.ID.String(),
|
||||
Sketchbook: sk.Sketchbook.String(),
|
||||
Title: sk.Title.String(),
|
||||
Description: sk.Description.String(),
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
}
|
||||
}
|
||||
|
||||
func (s SketchDTO) ToSketch() *sketchbook.Sketch {
|
||||
return &sketchbook.Sketch{
|
||||
ID: types.ID(uuid.MustParse(s.ID)),
|
||||
Sketchbook: types.ID(uuid.MustParse(s.Sketchbook)),
|
||||
Title: sketchbook.Title(s.Title),
|
||||
Description: sketchbook.Description(s.Description),
|
||||
Audit: types.Audit{
|
||||
CreatedAt: s.CreatedAt,
|
||||
UpdatedAt: s.UpdatedAt,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,10 @@ func TestSketchbookRepository(t *testing.T, create func() sketchbook.ReadWriter)
|
|||
testAddSketchbook(t, repo, ctx)
|
||||
})
|
||||
t.Run("GetSketchbookByOwnerID", func(t *testing.T) {
|
||||
|
||||
testGetSketchbookByOwnerID(t, repo, ctx)
|
||||
})
|
||||
t.Run("AddSketch", func(t *testing.T) {
|
||||
testAddSketch(t, repo, ctx)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -77,3 +80,36 @@ func testGetSketchbookByOwnerID(t *testing.T, repo sketchbook.ReadWriter, ctx co
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testAddSketch(t *testing.T, repo sketchbook.ReadWriter, ctx context.Context) {
|
||||
sk := &sketchbook.Sketchbook{
|
||||
ID: types.GenerateId(),
|
||||
Owner: types.GenerateId(),
|
||||
}
|
||||
if !assert.NoError(t, repo.AddSketchbook(ctx, sk)) {
|
||||
t.Fatalf("failed to add test sketchbook")
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
sketch *sketchbook.Sketch
|
||||
wantErr error
|
||||
}{
|
||||
{name: "no data",
|
||||
wantErr: repository.ErrNoDataProvided},
|
||||
{name: "for existent sketchbook",
|
||||
sketch: &sketchbook.Sketch{
|
||||
ID: types.GenerateId(),
|
||||
Sketchbook: sk.ID,
|
||||
Title: "AddSketchTest",
|
||||
Description: "Created from integration test",
|
||||
Audit: types.NewAudit(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
err := repo.AddSketch(ctx, test.sketch)
|
||||
assert.ErrorIs(t, err, test.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue