Initial commit
This commit is contained in:
commit
afed023152
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,147 @@
|
||||
package items
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Field length limits enforced on input.
|
||||
const (
|
||||
maxTitleLen = 200
|
||||
maxDescriptionLen = 2000
|
||||
)
|
||||
|
||||
// Service holds the business rules for items. It depends only on the
|
||||
// Repository interface; main wires the Bun implementation in.
|
||||
type Service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
// NewService returns a Service over the given repository.
|
||||
func NewService(repo Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
// CreateInput is the request body for creating an item.
|
||||
type CreateInput struct {
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Status Status `json:"status"`
|
||||
}
|
||||
|
||||
// UpdateInput patches only provided fields (nil = leave untouched), which
|
||||
// is why every field is a pointer.
|
||||
type UpdateInput struct {
|
||||
Title *string `json:"title"`
|
||||
Description *string `json:"description"`
|
||||
Status *Status `json:"status"`
|
||||
}
|
||||
|
||||
// List validates the filter and delegates to the repository.
|
||||
func (s *Service) List(ctx context.Context, f Filter) ([]Item, error) {
|
||||
f, err := normalizeFilter(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.List(ctx, f)
|
||||
}
|
||||
|
||||
// Get returns one item.
|
||||
func (s *Service) Get(ctx context.Context, id int64) (Item, error) {
|
||||
return s.repo.Get(ctx, id)
|
||||
}
|
||||
|
||||
// Create validates input and stores a new item.
|
||||
func (s *Service) Create(ctx context.Context, in CreateInput) (Item, error) {
|
||||
item := Item{
|
||||
Title: strings.TrimSpace(in.Title),
|
||||
Description: strings.TrimSpace(in.Description),
|
||||
}
|
||||
if item.Title == "" {
|
||||
return Item{}, &ValidationError{Field: "title", Reason: "required"}
|
||||
}
|
||||
if len(item.Title) > maxTitleLen {
|
||||
return Item{}, &ValidationError{Field: "title", Reason: "must be at most 200 characters"}
|
||||
}
|
||||
if len(item.Description) > maxDescriptionLen {
|
||||
return Item{}, &ValidationError{Field: "description", Reason: "must be at most 2000 characters"}
|
||||
}
|
||||
|
||||
switch in.Status {
|
||||
case "":
|
||||
item.Status = StatusActive
|
||||
case StatusActive, StatusArchived:
|
||||
item.Status = in.Status
|
||||
default:
|
||||
return Item{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
|
||||
}
|
||||
|
||||
if err := s.repo.Create(ctx, &item); err != nil {
|
||||
return Item{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Update applies a partial patch to an existing item.
|
||||
func (s *Service) Update(ctx context.Context, id int64, in UpdateInput) (Item, error) {
|
||||
if in.Title == nil && in.Description == nil && in.Status == nil {
|
||||
return Item{}, &ValidationError{Field: "body", Reason: "at least one field must be set"}
|
||||
}
|
||||
|
||||
item, err := s.repo.Get(ctx, id)
|
||||
if err != nil {
|
||||
return Item{}, err
|
||||
}
|
||||
|
||||
if in.Title != nil {
|
||||
item.Title = strings.TrimSpace(*in.Title)
|
||||
if item.Title == "" {
|
||||
return Item{}, &ValidationError{Field: "title", Reason: "required"}
|
||||
}
|
||||
if len(item.Title) > maxTitleLen {
|
||||
return Item{}, &ValidationError{Field: "title", Reason: "must be at most 200 characters"}
|
||||
}
|
||||
}
|
||||
if in.Description != nil {
|
||||
item.Description = strings.TrimSpace(*in.Description)
|
||||
if len(item.Description) > maxDescriptionLen {
|
||||
return Item{}, &ValidationError{Field: "description", Reason: "must be at most 2000 characters"}
|
||||
}
|
||||
}
|
||||
if in.Status != nil {
|
||||
if !in.Status.Valid() {
|
||||
return Item{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
|
||||
}
|
||||
item.Status = *in.Status
|
||||
}
|
||||
|
||||
if err := s.repo.Update(ctx, &item); err != nil {
|
||||
return Item{}, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
// Delete removes an item.
|
||||
func (s *Service) Delete(ctx context.Context, id int64) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
// normalizeFilter applies paging defaults and validates the status filter.
|
||||
func normalizeFilter(f Filter) (Filter, error) {
|
||||
switch f.Status {
|
||||
case "":
|
||||
case StatusActive, StatusArchived:
|
||||
default:
|
||||
return Filter{}, &ValidationError{Field: "status", Reason: "must be active or archived"}
|
||||
}
|
||||
if f.Limit <= 0 {
|
||||
f.Limit = DefaultLimit
|
||||
}
|
||||
if f.Limit > MaxLimit {
|
||||
f.Limit = MaxLimit
|
||||
}
|
||||
if f.Offset < 0 {
|
||||
f.Offset = 0
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
Reference in new issue
Block a user