113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
// Package service holds the business logic. It coordinates the store, the blob
|
|
// store, the analyzer and the event publisher, and it is the only layer that
|
|
// knows how the application's features fit together.
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"go-places-api/internal/analysis"
|
|
"go-places-api/internal/blobstore"
|
|
"go-places-api/internal/config"
|
|
"go-places-api/internal/events"
|
|
"go-places-api/internal/store"
|
|
)
|
|
|
|
// Errors surfaced to the transport layer. The HTTP and GraphQL layers map these
|
|
// onto status codes and GraphQL error extensions.
|
|
var (
|
|
// ErrNotFound means the addressed entity does not exist.
|
|
ErrNotFound = errors.New("not found")
|
|
// ErrInvalidInput means the request was malformed or violated a rule.
|
|
ErrInvalidInput = errors.New("invalid input")
|
|
// ErrTooLarge means the upload exceeded the configured limit.
|
|
ErrTooLarge = errors.New("upload too large")
|
|
// ErrUnsupportedType means the upload's content type is not allowed.
|
|
ErrUnsupportedType = errors.New("unsupported media type")
|
|
)
|
|
|
|
// Service is the application entry point used by the transport layers.
|
|
type Service struct {
|
|
store store.Store
|
|
blobs blobstore.BlobStore
|
|
analyze analysis.Analyzer
|
|
events events.Publisher
|
|
cfg config.Config
|
|
logger Logger
|
|
}
|
|
|
|
// Logger is the small logging surface the service needs. Keeping it narrow
|
|
// means tests can pass a no-op without pulling in slog.
|
|
type Logger interface {
|
|
Warn(msg string, args ...any)
|
|
Info(msg string, args ...any)
|
|
}
|
|
|
|
// Options configures a Service.
|
|
type Options struct {
|
|
Store store.Store
|
|
Blobs blobstore.BlobStore
|
|
Analyzer analysis.Analyzer
|
|
Events events.Publisher
|
|
Config config.Config
|
|
Logger Logger
|
|
}
|
|
|
|
// New builds a Service, filling in no-op collaborators where none are supplied
|
|
// so a partially wired service still works in tests.
|
|
func New(opts Options) *Service {
|
|
s := &Service{
|
|
store: opts.Store,
|
|
blobs: opts.Blobs,
|
|
analyze: opts.Analyzer,
|
|
events: opts.Events,
|
|
cfg: opts.Config,
|
|
logger: opts.Logger,
|
|
}
|
|
if s.analyze == nil {
|
|
s.analyze = analysis.NewLocal(analysis.DefaultMaxDimension)
|
|
}
|
|
if s.events == nil {
|
|
s.events = events.NewLogPublisher(nil)
|
|
}
|
|
if s.logger == nil {
|
|
s.logger = nopLogger{}
|
|
}
|
|
return s
|
|
}
|
|
|
|
// Pinger is implemented by stores that can report whether they are reachable.
|
|
// It is checked by the readiness probe, and is optional so an in-memory fake
|
|
// still satisfies the contract without implementing it.
|
|
type Pinger interface {
|
|
PingContext(ctx context.Context) error
|
|
}
|
|
|
|
// Ping reports whether the service's dependencies can serve traffic. A store
|
|
// that cannot report its health is treated as healthy.
|
|
func (s *Service) Ping(ctx context.Context) error {
|
|
p, ok := s.store.(Pinger)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return p.PingContext(ctx)
|
|
}
|
|
|
|
// mapStoreError translates store sentinels into service sentinels.
|
|
func mapStoreError(err error, entity string) error {
|
|
switch {
|
|
case errors.Is(err, store.ErrNotFound):
|
|
return fmt.Errorf("%w: no such %s", ErrNotFound, entity)
|
|
case errors.Is(err, store.ErrConflict):
|
|
return fmt.Errorf("%w: conflicting %s", ErrInvalidInput, entity)
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
type nopLogger struct{}
|
|
|
|
func (nopLogger) Warn(string, ...any) {}
|
|
func (nopLogger) Info(string, ...any) {}
|