Second checkpoint
This commit is contained in:
116
internal/service/category.go
Normal file
116
internal/service/category.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Package service holds the application's business logic. Services depend on
|
||||
// repository and storage interfaces (declared here) rather than concrete types,
|
||||
// and they translate between transport input and domain entities.
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"gis/internal/domain"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// CategoryRepository is the persistence behaviour CategoryService needs.
|
||||
type CategoryRepository interface {
|
||||
Create(ctx context.Context, c domain.Category) (domain.Category, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (domain.Category, error)
|
||||
List(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error)
|
||||
Update(ctx context.Context, c domain.Category) (domain.Category, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
// CategoryInput carries the mutable fields of a category.
|
||||
type CategoryInput struct {
|
||||
ParentID *uuid.UUID
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
// CategoryService implements category business rules.
|
||||
type CategoryService struct {
|
||||
repo CategoryRepository
|
||||
}
|
||||
|
||||
// NewCategoryService returns a CategoryService backed by repo.
|
||||
func NewCategoryService(repo CategoryRepository) *CategoryService {
|
||||
return &CategoryService{repo: repo}
|
||||
}
|
||||
|
||||
// Create validates the parent (if any) and stores a new category.
|
||||
func (s *CategoryService) Create(ctx context.Context, in CategoryInput) (domain.Category, error) {
|
||||
if err := s.ensureParentExists(ctx, in.ParentID); err != nil {
|
||||
return domain.Category{}, err
|
||||
}
|
||||
return s.repo.Create(ctx, domain.Category{
|
||||
ParentID: in.ParentID,
|
||||
Name: in.Name,
|
||||
Description: in.Description,
|
||||
})
|
||||
}
|
||||
|
||||
// Get returns a category by id.
|
||||
func (s *CategoryService) Get(ctx context.Context, id uuid.UUID) (domain.Category, error) {
|
||||
return s.repo.GetByID(ctx, id)
|
||||
}
|
||||
|
||||
// List returns categories, optionally filtered to a parent's direct children.
|
||||
func (s *CategoryService) List(ctx context.Context, parentID *uuid.UUID) ([]domain.Category, error) {
|
||||
return s.repo.List(ctx, parentID)
|
||||
}
|
||||
|
||||
// Update validates the parent change (existence + no cycles) and stores it.
|
||||
func (s *CategoryService) Update(ctx context.Context, id uuid.UUID, in CategoryInput) (domain.Category, error) {
|
||||
if _, err := s.repo.GetByID(ctx, id); err != nil {
|
||||
return domain.Category{}, err
|
||||
}
|
||||
if err := s.ensureParentExists(ctx, in.ParentID); err != nil {
|
||||
return domain.Category{}, err
|
||||
}
|
||||
if err := s.ensureNoCycle(ctx, id, in.ParentID); err != nil {
|
||||
return domain.Category{}, err
|
||||
}
|
||||
return s.repo.Update(ctx, domain.Category{
|
||||
ID: id,
|
||||
ParentID: in.ParentID,
|
||||
Name: in.Name,
|
||||
Description: in.Description,
|
||||
})
|
||||
}
|
||||
|
||||
// Delete removes a category.
|
||||
func (s *CategoryService) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
return s.repo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *CategoryService) ensureParentExists(ctx context.Context, parentID *uuid.UUID) error {
|
||||
if parentID == nil {
|
||||
return nil
|
||||
}
|
||||
if _, err := s.repo.GetByID(ctx, *parentID); err != nil {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("%w: parent category does not exist", domain.ErrValidation)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureNoCycle walks up the proposed parent's ancestry; if it reaches id, the
|
||||
// move would create a cycle.
|
||||
func (s *CategoryService) ensureNoCycle(ctx context.Context, id uuid.UUID, parentID *uuid.UUID) error {
|
||||
cursor := parentID
|
||||
for cursor != nil {
|
||||
if *cursor == id {
|
||||
return fmt.Errorf("%w: category cannot be its own ancestor", domain.ErrValidation)
|
||||
}
|
||||
parent, err := s.repo.GetByID(ctx, *cursor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cursor = parent.ParentID
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user