185 lines
5.5 KiB
Go
185 lines
5.5 KiB
Go
// Package app is the composition root: it builds and wires every dependency
|
|
// (config, logger, database, object store, messaging, repositories, services,
|
|
// and HTTP handlers) and exposes them to the CLI commands.
|
|
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
stdhttp "net/http"
|
|
|
|
"gis/api"
|
|
"gis/internal/config"
|
|
"gis/internal/domain"
|
|
"gis/internal/messaging/rabbitmq"
|
|
"gis/internal/parser"
|
|
"gis/internal/platform/logger"
|
|
"gis/internal/raster"
|
|
"gis/internal/repository/postgres"
|
|
"gis/internal/service"
|
|
"gis/internal/storage/s3"
|
|
transporthttp "gis/internal/transport/http"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// App holds the wired application dependencies.
|
|
type App struct {
|
|
Cfg *config.Config
|
|
Log *slog.Logger
|
|
|
|
pool *pgxpool.Pool
|
|
store *s3.Client
|
|
rabbit *rabbitmq.Connection
|
|
|
|
publisher *rabbitmq.Publisher
|
|
categories *service.CategoryService
|
|
datasets *service.DatasetService
|
|
eventRepo *postgres.EventRepository
|
|
}
|
|
|
|
// New builds the application from configuration. The caller must call Close.
|
|
func New(ctx context.Context) (*App, error) {
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
log := logger.New("json", "info")
|
|
|
|
pool, err := postgres.Connect(ctx, cfg.DB.URL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("connect postgres: %w", err)
|
|
}
|
|
|
|
store, err := s3.New(ctx, cfg.S3)
|
|
if err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("connect s3: %w", err)
|
|
}
|
|
|
|
rabbit, err := rabbitmq.Connect(cfg.RabbitMQ)
|
|
if err != nil {
|
|
pool.Close()
|
|
return nil, fmt.Errorf("connect rabbitmq: %w", err)
|
|
}
|
|
|
|
categoryRepo := postgres.NewCategoryRepository(pool)
|
|
datasetRepo := postgres.NewDatasetRepository(pool)
|
|
eventRepo := postgres.NewEventRepository(pool)
|
|
|
|
publisher := rabbitmq.NewPublisher(rabbit)
|
|
jobPublisher := rabbitmq.NewDatasetJobPublisher(publisher)
|
|
|
|
return &App{
|
|
Cfg: cfg,
|
|
Log: log,
|
|
pool: pool,
|
|
store: store,
|
|
rabbit: rabbit,
|
|
publisher: publisher,
|
|
categories: service.NewCategoryService(categoryRepo),
|
|
datasets: service.NewDatasetService(datasetRepo, store, categoryRepo, jobPublisher, parser.Columns, parser.Rows, raster.NewGDALConverter()),
|
|
eventRepo: eventRepo,
|
|
}, nil
|
|
}
|
|
|
|
// Handler builds the HTTP handler with all routes and readiness checks wired.
|
|
func (a *App) Handler() stdhttp.Handler {
|
|
validate := validator.New(validator.WithRequiredStructEnabled())
|
|
validate.RegisterValidation("slug", func(fl validator.FieldLevel) bool {
|
|
return domain.ValidSlug(fl.Field().String())
|
|
})
|
|
|
|
health := transporthttp.NewHealthHandler(map[string]transporthttp.ReadinessCheck{
|
|
"postgres": func(ctx context.Context) error { return a.pool.Ping(ctx) },
|
|
"s3": func(ctx context.Context) error { return a.store.Ping(ctx) },
|
|
"rabbitmq": func(_ context.Context) error { return a.rabbit.Ping() },
|
|
})
|
|
|
|
return transporthttp.NewRouter(transporthttp.RouterDeps{
|
|
Logger: a.Log,
|
|
Health: health,
|
|
Categories: transporthttp.NewCategoryHandler(a.categories, validate),
|
|
Datasets: transporthttp.NewDatasetHandler(a.datasets, validate),
|
|
OpenAPISpec: api.Spec,
|
|
})
|
|
}
|
|
|
|
// Server builds the HTTP server.
|
|
func (a *App) Server() *transporthttp.Server {
|
|
return transporthttp.NewServer(a.Cfg.HTTP, a.Handler(), a.Log)
|
|
}
|
|
|
|
// Consumers returns all RabbitMQ consumers the worker should run.
|
|
func (a *App) Consumers() []*rabbitmq.Consumer {
|
|
return []*rabbitmq.Consumer{
|
|
a.ParseConsumer(),
|
|
a.PropertiesConsumer(),
|
|
a.ExtractConsumer(),
|
|
a.ConvertConsumer(),
|
|
a.ExampleConsumer(),
|
|
}
|
|
}
|
|
|
|
// PropertiesConsumer builds the plain-vector properties-extraction consumer.
|
|
func (a *App) PropertiesConsumer() *rabbitmq.Consumer {
|
|
handler := rabbitmq.NewPropertiesHandler(a.datasets, a.Log)
|
|
return rabbitmq.NewConsumer(
|
|
a.rabbit, rabbitmq.DatasetPropertiesQueue, rabbitmq.DatasetPropertiesRoutingKey,
|
|
"gis-dataset-properties", handler, a.Log,
|
|
)
|
|
}
|
|
|
|
// ParseConsumer builds the dataset attribute-table parse consumer.
|
|
func (a *App) ParseConsumer() *rabbitmq.Consumer {
|
|
handler := rabbitmq.NewParseHandler(a.datasets, a.Log)
|
|
return rabbitmq.NewConsumer(
|
|
a.rabbit, rabbitmq.DatasetParseQueue, rabbitmq.DatasetParseRoutingKey,
|
|
"gis-dataset-parser", handler, a.Log,
|
|
)
|
|
}
|
|
|
|
// ExtractConsumer builds the dataset extraction (unpivot) consumer.
|
|
func (a *App) ExtractConsumer() *rabbitmq.Consumer {
|
|
handler := rabbitmq.NewExtractHandler(a.datasets, a.Log)
|
|
return rabbitmq.NewConsumer(
|
|
a.rabbit, rabbitmq.DatasetExtractQueue, rabbitmq.DatasetExtractRoutingKey,
|
|
"gis-dataset-extractor", handler, a.Log,
|
|
)
|
|
}
|
|
|
|
// ConvertConsumer builds the raster COG-conversion consumer.
|
|
func (a *App) ConvertConsumer() *rabbitmq.Consumer {
|
|
handler := rabbitmq.NewConvertHandler(a.datasets, a.Log)
|
|
return rabbitmq.NewConsumer(
|
|
a.rabbit, rabbitmq.DatasetConvertQueue, rabbitmq.DatasetConvertRoutingKey,
|
|
"gis-dataset-converter", handler, a.Log,
|
|
)
|
|
}
|
|
|
|
// ExampleConsumer builds the generic example RabbitMQ consumer.
|
|
func (a *App) ExampleConsumer() *rabbitmq.Consumer {
|
|
handler := rabbitmq.NewExampleHandler(a.eventRepo, a.Log)
|
|
return rabbitmq.NewConsumer(
|
|
a.rabbit, a.Cfg.RabbitMQ.Queue, rabbitmq.ExampleBindingKey,
|
|
"gis-example-consumer", handler, a.Log,
|
|
)
|
|
}
|
|
|
|
// Publisher returns the RabbitMQ publisher.
|
|
func (a *App) Publisher() *rabbitmq.Publisher { return a.publisher }
|
|
|
|
// Close releases all resources in reverse order of acquisition.
|
|
func (a *App) Close() {
|
|
if a.rabbit != nil {
|
|
if err := a.rabbit.Close(); err != nil {
|
|
a.Log.Warn("close rabbitmq", "error", err)
|
|
}
|
|
}
|
|
if a.pool != nil {
|
|
a.pool.Close()
|
|
}
|
|
}
|