Skip to content

Commit

Permalink
refactor(asset): improve asset and group entity handling and error ma…
Browse files Browse the repository at this point in the history
…nagement
  • Loading branch information
kasugamirai committed Jan 21, 2025
1 parent b88d638 commit ae883ae
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 66 deletions.
6 changes: 4 additions & 2 deletions asset/domain/builder/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ func (b *AssetBuilder) Build() (*entity.Asset, error) {
}
if b.a.CreatedAt().IsZero() {
now := time.Now()
b = b.CreatedAt(now).UpdatedAt(now)
b.a.SetCreatedAt(now)
b.a.SetUpdatedAt(now)
}
if b.a.Status() == "" {
b = b.Status(entity.StatusPending)
b.a.UpdateStatus(entity.StatusPending, b.a.Error())
}
return b.a, nil
}
Expand Down Expand Up @@ -108,5 +109,6 @@ func (b *AssetBuilder) CreatedAt(createdAt time.Time) *AssetBuilder {
}

func (b *AssetBuilder) UpdatedAt(updatedAt time.Time) *AssetBuilder {
b.a.SetUpdatedAt(updatedAt)
return b
}
2 changes: 1 addition & 1 deletion asset/domain/builder/tests/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,6 @@ func TestGroupBuilder_InvalidSetters(t *testing.T) {
Name("test-group").
Policy("")
group, err = b.Build()
assert.NoError(t, err) // Empty policy is allowed during build
assert.NoError(t, err)
assert.NotNil(t, group)
}
7 changes: 6 additions & 1 deletion asset/domain/entity/asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func NewAsset(id id.ID, name string, size int64, contentType string) *Asset {
}

// Validate implements the Validator interface
func (a *Asset) Validate(ctx context.Context) validation.ValidationResult {
func (a *Asset) Validate(ctx context.Context) validation.Result {
validationCtx := validation.NewValidationContext(
&validation.RequiredRule{Field: "id"},
&validation.RequiredRule{Field: "name"},
Expand Down Expand Up @@ -123,3 +123,8 @@ func (a *Asset) SetSize(size int64) {
func (a *Asset) SetCreatedAt(createdAt time.Time) {
a.createdAt = createdAt
}

// SetUpdatedAt is an internal setter for updatedAt, only used by builder
func (a *Asset) SetUpdatedAt(updatedAt time.Time) {
a.updatedAt = updatedAt
}
2 changes: 1 addition & 1 deletion asset/domain/entity/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func NewGroup(id id.GroupID, name string) *Group {
}

// Validate implements the Validator interface
func (g *Group) Validate(ctx context.Context) validation.ValidationResult {
func (g *Group) Validate(ctx context.Context) validation.Result {
validationCtx := validation.NewValidationContext(
&validation.RequiredRule{Field: "id"},
&validation.RequiredRule{Field: "name"},
Expand Down
60 changes: 39 additions & 21 deletions asset/domain/validation/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package validation

import (
"context"
"errors"
"fmt"
)

// ValidationError represents a validation error
// Error ValidationError represents a validation error
type Error struct {
Field string
Message string
Expand All @@ -15,28 +16,28 @@ func (e *Error) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// ValidationResult represents the result of a validation
type ValidationResult struct {
// Result ValidationResult represents the result of a validation
type Result struct {
IsValid bool
Errors []*Error
}

// NewValidationResult creates a new validation result
func NewValidationResult(isValid bool, errors ...*Error) ValidationResult {
return ValidationResult{
func NewValidationResult(isValid bool, errors ...*Error) Result {
return Result{
IsValid: isValid,
Errors: errors,
}
}

// Valid creates a valid validation result
func Valid() ValidationResult {
return ValidationResult{IsValid: true}
func Valid() Result {
return Result{IsValid: true}
}

// Invalid creates an invalid validation result with errors
func Invalid(errors ...*Error) ValidationResult {
return ValidationResult{
func Invalid(errors ...*Error) Result {
return Result{
IsValid: false,
Errors: errors,
}
Expand All @@ -51,7 +52,7 @@ type ValidationRule interface {
// Validator defines the interface for entities that can be validated
type Validator interface {
// Validate performs all validation rules and returns the result
Validate(ctx context.Context) ValidationResult
Validate(ctx context.Context) Result
}

// ValidationContext holds the context for validation
Expand All @@ -67,24 +68,40 @@ func NewValidationContext(rules ...ValidationRule) *ValidationContext {
}

// Validate executes all validation rules in the context
func (c *ValidationContext) Validate(ctx context.Context, value interface{}) ValidationResult {
var errors []*Error
func (c *ValidationContext) Validate(ctx context.Context, value interface{}) Result {
var validationErrors []*Error

// If value is a map, validate each field with its corresponding rules
if fields, ok := value.(map[string]interface{}); ok {
for _, rule := range c.Rules {
if r, ok := rule.(*RequiredRule); ok {
if fieldValue, exists := fields[r.Field]; exists {
if err := rule.Validate(ctx, fieldValue); err != nil {
errors = append(errors, err.(*Error))
var verr *Error
if errors.As(err, &verr) {
validationErrors = append(validationErrors, verr)
} else {
validationErrors = append(validationErrors, &Error{
Field: r.Field,
Message: err.Error(),
})
}
}
} else {
errors = append(errors, NewValidationError(r.Field, "field is required"))
validationErrors = append(validationErrors, NewValidationError(r.Field, "field is required"))
}
} else if r, ok := rule.(*MaxLengthRule); ok {
if fieldValue, exists := fields[r.Field]; exists {
if err := rule.Validate(ctx, fieldValue); err != nil {
errors = append(errors, err.(*Error))
var verr *Error
if errors.As(err, &verr) {
validationErrors = append(validationErrors, verr)
} else {
validationErrors = append(validationErrors, &Error{
Field: r.Field,
Message: err.Error(),
})
}
}
}
}
Expand All @@ -93,24 +110,25 @@ func (c *ValidationContext) Validate(ctx context.Context, value interface{}) Val
// If value is not a map, validate directly
for _, rule := range c.Rules {
if err := rule.Validate(ctx, value); err != nil {
if verr, ok := err.(*Error); ok {
errors = append(errors, verr)
var verr *Error
if errors.As(err, &verr) {
validationErrors = append(validationErrors, verr)
} else {
errors = append(errors, &Error{
validationErrors = append(validationErrors, &Error{
Message: err.Error(),
})
}
}
}
}

if len(errors) > 0 {
return Invalid(errors...)
if len(validationErrors) > 0 {
return Invalid(validationErrors...)
}
return Valid()
}

// ValidationError creates a new validation error
// NewValidationError ValidationError creates a new validation error
func NewValidationError(field, message string) *Error {
return &Error{
Field: field,
Expand Down
70 changes: 35 additions & 35 deletions asset/infrastructure/gcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ import (
"google.golang.org/api/iterator"
)

const (
errFailedToCreateClient = "failed to create client: %w"
errAssetAlreadyExists = "asset already exists: %s"
errAssetNotFound = "asset not found: %s"
errFailedToUpdateAsset = "failed to update asset: %w"
errFailedToDeleteAsset = "failed to delete asset: %w"
errFailedToListAssets = "failed to list assets: %w"
errFailedToUploadFile = "failed to upload file: %w"
errFailedToCloseWriter = "failed to close writer: %w"
errFailedToReadFile = "failed to read file: %w"
errFailedToGetAsset = "failed to get asset: %w"
errFailedToGenerateURL = "failed to generate upload URL: %w"
errFailedToMoveAsset = "failed to move asset: %w"
errInvalidURL = "invalid URL format: %s"
var (
ErrFailedToCreateClient = errors.New("failed to create client")
ErrAssetAlreadyExists = errors.New("asset already exists")
ErrAssetNotFound = errors.New("asset not found")
ErrFailedToUpdateAsset = errors.New("failed to update asset")
ErrFailedToDeleteAsset = errors.New("failed to delete asset")
ErrFailedToListAssets = errors.New("failed to list assets")
ErrFailedToUploadFile = errors.New("failed to upload file")
ErrFailedToCloseWriter = errors.New("failed to close writer")
ErrFailedToReadFile = errors.New("failed to read file")
ErrFailedToGetAsset = errors.New("failed to get asset")
ErrFailedToGenerateURL = errors.New("failed to generate upload URL")
ErrFailedToMoveAsset = errors.New("failed to move asset")
ErrInvalidURL = errors.New("invalid URL format")
)

type Client struct {
Expand All @@ -45,14 +45,14 @@ var _ repository.PersistenceRepository = (*Client)(nil)
func NewClient(ctx context.Context, bucketName string, basePath string, baseURL string) (*Client, error) {
client, err := storage.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf(errFailedToCreateClient, err)
return nil, fmt.Errorf("%w: %v", ErrFailedToCreateClient, err)
}

var u *url.URL
if baseURL != "" {
u, err = url.Parse(baseURL)
if err != nil {
return nil, fmt.Errorf(errInvalidURL, err)
return nil, fmt.Errorf("%w: %v", ErrInvalidURL, err)
}
}

Expand All @@ -74,7 +74,7 @@ func (c *Client) Create(ctx context.Context, asset *entity.Asset) error {
}

if _, err := obj.Attrs(ctx); err == nil {
return fmt.Errorf(errAssetAlreadyExists, asset.ID())
return fmt.Errorf("%w: %s", ErrAssetAlreadyExists, asset.ID())
}

writer := obj.NewWriter(ctx)
Expand Down Expand Up @@ -108,7 +108,7 @@ func (c *Client) Update(ctx context.Context, asset *entity.Asset) error {
}

if _, err := obj.Update(ctx, update); err != nil {
return fmt.Errorf(errFailedToUpdateAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToUpdateAsset, err)
}
return nil
}
Expand All @@ -119,7 +119,7 @@ func (c *Client) Delete(ctx context.Context, id id.ID) error {
if errors.Is(err, storage.ErrObjectNotExist) {
return nil
}
return fmt.Errorf(errFailedToDeleteAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToDeleteAsset, err)
}
return nil
}
Expand All @@ -134,7 +134,7 @@ func (c *Client) List(ctx context.Context) ([]*entity.Asset, error) {
break
}
if err != nil {
return nil, fmt.Errorf(errFailedToListAssets, err)
return nil, fmt.Errorf("%w: %v", ErrFailedToListAssets, err)
}

id, err := id.IDFrom(path.Base(attrs.Name))
Expand All @@ -160,11 +160,11 @@ func (c *Client) Upload(ctx context.Context, id id.ID, content io.Reader) error

if _, err := io.Copy(writer, content); err != nil {
_ = writer.Close()
return fmt.Errorf(errFailedToUploadFile, err)
return fmt.Errorf("%w: %v", ErrFailedToUploadFile, err)
}

if err := writer.Close(); err != nil {
return fmt.Errorf(errFailedToCloseWriter, err)
return fmt.Errorf("%w: %v", ErrFailedToCloseWriter, err)
}
return nil
}
Expand All @@ -174,9 +174,9 @@ func (c *Client) Download(ctx context.Context, id id.ID) (io.ReadCloser, error)
reader, err := obj.NewReader(ctx)
if err != nil {
if errors.Is(err, storage.ErrObjectNotExist) {
return nil, fmt.Errorf(errAssetNotFound, id)
return nil, fmt.Errorf("%w: %s", ErrAssetNotFound, id)
}
return nil, fmt.Errorf(errFailedToReadFile, err)
return nil, fmt.Errorf("%w: %v", ErrFailedToReadFile, err)
}
return reader, nil
}
Expand All @@ -189,7 +189,7 @@ func (c *Client) GetUploadURL(ctx context.Context, id id.ID) (string, error) {

signedURL, err := c.bucket.SignedURL(c.objectPath(id), opts)
if err != nil {
return "", fmt.Errorf(errFailedToGenerateURL, err)
return "", fmt.Errorf("%w: %v", ErrFailedToGenerateURL, err)
}
return signedURL, nil
}
Expand All @@ -199,11 +199,11 @@ func (c *Client) Move(ctx context.Context, fromID, toID id.ID) error {
dst := c.getObject(toID)

if _, err := dst.CopierFrom(src).Run(ctx); err != nil {
return fmt.Errorf(errFailedToMoveAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToMoveAsset, err)
}

if err := src.Delete(ctx); err != nil {
return fmt.Errorf(errFailedToMoveAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToMoveAsset, err)
}

return nil
Expand All @@ -220,12 +220,12 @@ func (c *Client) DeleteAll(ctx context.Context, prefix string) error {
break
}
if err != nil {
return fmt.Errorf(errFailedToDeleteAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToDeleteAsset, err)
}

if err := c.bucket.Object(attrs.Name).Delete(ctx); err != nil {
if !errors.Is(err, storage.ErrObjectNotExist) {
return fmt.Errorf(errFailedToDeleteAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToDeleteAsset, err)
}
}
}
Expand All @@ -245,16 +245,16 @@ func (c *Client) GetIDFromURL(urlStr string) (id.ID, error) {
emptyID := id.NewID()

if c.baseURL == nil {
return emptyID, fmt.Errorf(errInvalidURL, "base URL not set")
return emptyID, fmt.Errorf("%w: base URL not set", ErrInvalidURL)
}

u, err := url.Parse(urlStr)
if err != nil {
return emptyID, fmt.Errorf(errInvalidURL, err)
return emptyID, fmt.Errorf("%w: %v", ErrInvalidURL, err)
}

if u.Host != c.baseURL.Host {
return emptyID, fmt.Errorf(errInvalidURL, "host mismatch")
return emptyID, fmt.Errorf("%w: host mismatch", ErrInvalidURL)
}

urlPath := strings.TrimPrefix(u.Path, c.baseURL.Path)
Expand All @@ -275,9 +275,9 @@ func (c *Client) objectPath(id id.ID) string {

func (c *Client) handleNotFound(err error, id id.ID) error {
if errors.Is(err, storage.ErrObjectNotExist) {
return fmt.Errorf(errAssetNotFound, id)
return fmt.Errorf("%w: %s", ErrAssetNotFound, id)
}
return fmt.Errorf(errFailedToGetAsset, err)
return fmt.Errorf("%w: %v", ErrFailedToGetAsset, err)
}

func (c *Client) FindByGroup(ctx context.Context, groupID id.GroupID) ([]*entity.Asset, error) {
Expand All @@ -290,7 +290,7 @@ func (c *Client) FindByGroup(ctx context.Context, groupID id.GroupID) ([]*entity
break
}
if err != nil {
return nil, fmt.Errorf(errFailedToListAssets, err)
return nil, fmt.Errorf("%w: %v", ErrFailedToListAssets, err)
}

assetID, err := id.IDFrom(path.Base(attrs.Name))
Expand Down
Loading

0 comments on commit ae883ae

Please sign in to comment.