Refactored the application structure to better separate concerns between files and packages
Some checks failed
ci/woodpecker/push/build-image Pipeline failed
Some checks failed
ci/woodpecker/push/build-image Pipeline failed
This commit is contained in:
7
internal/app/constants.go
Normal file
7
internal/app/constants.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// constants.go defines shared application-level constants.
|
||||
// Layer: internal/app service behavior defaults.
|
||||
package app
|
||||
|
||||
const (
|
||||
ObservationWindowMinutesDefault = 30
|
||||
)
|
||||
17
internal/app/current_conditions.go
Normal file
17
internal/app/current_conditions.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// current_conditions.go defines the current-conditions aggregate model.
|
||||
// Layer: internal/app domain-adjacent read model.
|
||||
package app
|
||||
|
||||
import "gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
|
||||
// CurrentConditions is an averaged current-conditions aggregate over a recent window.
|
||||
type CurrentConditions struct {
|
||||
TemperatureC *float64
|
||||
ApparentTemperatureC *float64
|
||||
DewpointC *float64
|
||||
RelativeHumidityPercent *float64
|
||||
WindSpeedKmh *float64
|
||||
WindDirectionDegrees *float64
|
||||
ConditionCode model.WMOCode
|
||||
IsDay *bool
|
||||
}
|
||||
42
internal/app/service.go
Normal file
42
internal/app/service.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// service.go defines application read ports and use-case orchestration.
|
||||
// Layer: internal/app core business-facing API.
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
// Repository defines outbound data access used by weatherapi use cases.
|
||||
type Repository interface {
|
||||
LatestObservation(ctx context.Context) (*model.WeatherObservation, error)
|
||||
LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error)
|
||||
LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error)
|
||||
CurrentConditions(ctx context.Context, observationWindowMinutes int) (*CurrentConditions, error)
|
||||
}
|
||||
|
||||
// Service provides weather read use-cases.
|
||||
type Service struct {
|
||||
repo Repository
|
||||
}
|
||||
|
||||
func NewService(repo Repository) *Service {
|
||||
return &Service{repo: repo}
|
||||
}
|
||||
|
||||
func (s *Service) LatestObservation(ctx context.Context) (*model.WeatherObservation, error) {
|
||||
return s.repo.LatestObservation(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error) {
|
||||
return s.repo.LatestHourlyForecast(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) LatestAlertRun(ctx context.Context) (*model.WeatherAlertRun, error) {
|
||||
return s.repo.LatestAlertRun(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) CurrentConditions(ctx context.Context) (*CurrentConditions, error) {
|
||||
return s.repo.CurrentConditions(ctx, ObservationWindowMinutesDefault)
|
||||
}
|
||||
100
internal/app/service_test.go
Normal file
100
internal/app/service_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// service_test.go validates application service delegation behavior.
|
||||
// Layer: internal/app tests for read use-case orchestration.
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
type fakeRepository struct {
|
||||
observation *model.WeatherObservation
|
||||
forecast *model.WeatherForecastRun
|
||||
alerts *model.WeatherAlertRun
|
||||
conditions *CurrentConditions
|
||||
err error
|
||||
|
||||
currentConditionsWindow int
|
||||
}
|
||||
|
||||
func (r *fakeRepository) LatestObservation(context.Context) (*model.WeatherObservation, error) {
|
||||
return r.observation, r.err
|
||||
}
|
||||
|
||||
func (r *fakeRepository) LatestHourlyForecast(context.Context) (*model.WeatherForecastRun, error) {
|
||||
return r.forecast, r.err
|
||||
}
|
||||
|
||||
func (r *fakeRepository) LatestAlertRun(context.Context) (*model.WeatherAlertRun, error) {
|
||||
return r.alerts, r.err
|
||||
}
|
||||
|
||||
func (r *fakeRepository) CurrentConditions(_ context.Context, observationWindowMinutes int) (*CurrentConditions, error) {
|
||||
r.currentConditionsWindow = observationWindowMinutes
|
||||
return r.conditions, r.err
|
||||
}
|
||||
|
||||
func TestServiceDelegatesObservation(t *testing.T) {
|
||||
repo := &fakeRepository{observation: &model.WeatherObservation{StationID: "KSTL"}}
|
||||
svc := NewService(repo)
|
||||
|
||||
obs, err := svc.LatestObservation(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if obs == nil || obs.StationID != "KSTL" {
|
||||
t.Fatalf("unexpected observation: %+v", obs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDelegatesForecast(t *testing.T) {
|
||||
repo := &fakeRepository{forecast: &model.WeatherForecastRun{LocationID: "stl"}}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestHourlyForecast(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if run == nil || run.LocationID != "stl" {
|
||||
t.Fatalf("unexpected forecast: %+v", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceDelegatesAlerts(t *testing.T) {
|
||||
repo := &fakeRepository{alerts: &model.WeatherAlertRun{LocationID: "stl"}}
|
||||
svc := NewService(repo)
|
||||
|
||||
run, err := svc.LatestAlertRun(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if run == nil || run.LocationID != "stl" {
|
||||
t.Fatalf("unexpected alert run: %+v", run)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceUsesDefaultCurrentConditionsWindow(t *testing.T) {
|
||||
repo := &fakeRepository{conditions: &CurrentConditions{ConditionCode: model.WMOUnknown}}
|
||||
svc := NewService(repo)
|
||||
|
||||
_, err := svc.CurrentConditions(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if repo.currentConditionsWindow != ObservationWindowMinutesDefault {
|
||||
t.Fatalf("expected observation window %d, got %d", ObservationWindowMinutesDefault, repo.currentConditionsWindow)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServicePropagatesErrors(t *testing.T) {
|
||||
want := errors.New("boom")
|
||||
repo := &fakeRepository{err: want}
|
||||
svc := NewService(repo)
|
||||
|
||||
if _, err := svc.LatestObservation(context.Background()); !errors.Is(err, want) {
|
||||
t.Fatalf("expected error %v, got %v", want, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user