Skip to content

Commit

Permalink
Implement mutable settings querying/updating REST APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
momeni committed Mar 7, 2024
1 parent 55ab459 commit dbda761
Show file tree
Hide file tree
Showing 20 changed files with 800 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ Remove this line after moving [Unreleased] to [1.2.0] - 2024-MM-DD.
- Reload instantiated use case objects whenever the mutable settings are updated
- Preserve comments in the configuration YAML files during the migration operation

### Fixed

- Return a bool ok flag from the DserXReq methods


## [1.1.0] - 2024-02-16

Expand Down
2 changes: 1 addition & 1 deletion cmd/caweb/command/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func startWebServer(_ *cobra.Command, _ []string) error {
}
defer p.Close()
var e *gin.Engine = c.Gin.NewEngine()
if err = routes.Register(e, p, c.Usecases); err != nil {
if err = routes.Register(ctx, e, p, c); err != nil {
return fmt.Errorf("registering routes: %w", err)
}
if err = e.Run(); err != nil {
Expand Down
25 changes: 25 additions & 0 deletions pkg/adapter/config/cfg2/cfg2.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/momeni/clean-arch/pkg/adapter/db/postgres/migration"
"github.com/momeni/clean-arch/pkg/core/model"
"github.com/momeni/clean-arch/pkg/core/repo"
"github.com/momeni/clean-arch/pkg/core/usecase/appuc"
"github.com/momeni/clean-arch/pkg/core/usecase/carsuc"
"gopkg.in/yaml.v3"
)
Expand Down Expand Up @@ -183,6 +184,30 @@ func (c *Config) SetSchemaVersion(sv model.SemVer) {
c.Vers.Versions.Database = sv
}

// NewAppUseCase instantiates a new application management use case.
// Instantiated use case needs a settings repository (and access to the
// connection pool) in order to query and update the mutable settings.
// It also needs to know about the configuration file contents which
// should be overridden by the database contents. However, the
// repository instance can manage this relationship with the
// configuration file contents (in the adapters layer), allowing the
// application use case to solely deal with the model layer settings.
// The settings repository must take the `c` Config instance during its
// instantiation.
func (c *Config) NewAppUseCase(
p repo.Pool, s appuc.SettingsRepo, carsRepo repo.Cars,
) (*appuc.UseCase, error) {
return appuc.New(p, s, carsRepo)
}

// NewCarsUseCase instantiates a new cars use case based on the settings
// in the c struct.
func (c *Config) NewCarsUseCase(
p repo.Pool, r repo.Cars,
) (*carsuc.UseCase, error) {
return c.Usecases.Cars.NewUseCase(p, r)
}

// Usecases contains the configuration settings for all use cases.
type Usecases struct {
Cars Cars // cars use cases related settings
Expand Down
105 changes: 105 additions & 0 deletions pkg/adapter/db/postgres/settingsrp/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2024 Behnam Momeni
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

package settingsrp

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/momeni/clean-arch/pkg/adapter/config/cfg2"
"github.com/momeni/clean-arch/pkg/adapter/config/settings"
"github.com/momeni/clean-arch/pkg/adapter/db/postgres"
"github.com/momeni/clean-arch/pkg/adapter/db/postgres/migration/sch1v1"
"github.com/momeni/clean-arch/pkg/adapter/db/postgres/migration/settle/stlmig1"
"github.com/momeni/clean-arch/pkg/core/model"
"github.com/momeni/clean-arch/pkg/core/usecase/appuc"
)

// Fetch queries the mutable settings from the settings repository,
// deserializes them, merges them into a clone of the baseConfs
// representing the configuration file and environment variables state,
// and returns the fresh configuration instance as an appuc.Builder
// interface in addition to its visible settings (as an instance of the
// version-independent model.VisibleSettings struct).
func Fetch(
ctx context.Context, c *postgres.Conn, baseConfs *cfg2.Config,
) (appuc.Builder, *model.VisibleSettings, error) {
b, err := sch1v1.LoadSettings(ctx, c)
if err != nil {
return nil, nil, fmt.Errorf("sch1v1.LoadSettings: %w", err)
}
var ser cfg2.Serializable
err = json.Unmarshal(b, &ser)
if err != nil {
return nil, nil, fmt.Errorf("deserializing json: %w", err)
}
confs := baseConfs.Clone()
err = confs.Mutate(ser)
if err != nil {
return nil, nil, fmt.Errorf("confs.Mutate(%#v): %w", ser, err)
}
v := confs.Visible()
vs := &model.VisibleSettings{
ImmutableSettings: &model.ImmutableSettings{
Logger: v.Immutable.Logger,
},
}
if doo := v.Cars.DelayOfOPM; doo != nil {
t := time.Duration(*doo)
vs.ParkingMethod.Delay = &t
}
return confs, vs, nil
}

// Update converts the version-independent mutable model.Settings
// instance into a version-dependent serializable settings instance
// for the last supported version, serializes them as JSON, and
// then stores them in the settings repository. Given mutable settings
// are also used in order to update a clone of the baseConfs instance.
// Updated configuration settings will be returned as an instance of
// the appuc.Builder interface in addition to its visible settings
// (which are provided as an instance of the version-independent
// model.VisibleSettings struct).
func Update(
ctx context.Context,
tx *postgres.Tx,
baseConfs *cfg2.Config,
s *model.Settings,
) (appuc.Builder, *model.VisibleSettings, error) {
ser := cfg2.Serializable{
Version: cfg2.Version,
}
if d := s.VisibleSettings.ParkingMethod.Delay; d != nil {
t := settings.Duration(*d)
ser.Settings.Visible.Cars.DelayOfOPM = &t
}
confs := baseConfs.Clone()
if err := confs.Mutate(ser); err != nil {
return nil, nil, fmt.Errorf("confs.Mutate(%#v): %w", ser, err)
}
b, err := json.Marshal(ser)
if err != nil {
return nil, nil, fmt.Errorf("serializing json: %w", err)
}
sm1 := stlmig1.New(tx)
err = sm1.PersistSettings(ctx, b)
if err != nil {
return nil, nil, fmt.Errorf("persisting settings: %w", err)
}
v := confs.Visible()
vs := &model.VisibleSettings{
ImmutableSettings: &model.ImmutableSettings{
Logger: v.Immutable.Logger,
},
}
if doo := v.Cars.DelayOfOPM; doo != nil {
t := time.Duration(*doo)
vs.ParkingMethod.Delay = &t
}
return confs, vs, nil
}
96 changes: 96 additions & 0 deletions pkg/adapter/db/postgres/settingsrp/repo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2024 Behnam Momeni
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

// Package settingsrp is the adapter for the settings repository.
// It exposes the settingsrp.Repo type in order to allow use cases
// to update mutable settings or query them from the database.
package settingsrp

import (
"context"

"github.com/momeni/clean-arch/pkg/adapter/config/cfg2"
"github.com/momeni/clean-arch/pkg/adapter/db/postgres"
"github.com/momeni/clean-arch/pkg/core/model"
"github.com/momeni/clean-arch/pkg/core/repo"
"github.com/momeni/clean-arch/pkg/core/usecase/appuc"
)

// Repo represents the settings repository instance.
type Repo struct {
baseConfs *cfg2.Config
}

// New instantiates a settings Repo struct. Created instance wraps
// the given configuration instance as its base configuration items, so
// whenever it needs to update the mutable settings or reload them from
// the database, it can apply them on a fresh clone of this base confs.
func New(c *cfg2.Config) *Repo {
return &Repo{
baseConfs: c,
}
}

type connQueryer struct {
*postgres.Conn
baseConfs *cfg2.Config
}

// Conn takes a Conn interface instance, unwraps it as required,
// and returns a SettingsConnQueryer interface which (with access to
// the implementation-dependent connection object) can run different
// permitted operations on settings.
// The connQueryer itself is not mentioned as the return value since
// it is not exported. Otherwise, the general rule is to take interfaces
// as arguments and return exported structs.
func (settings *Repo) Conn(c repo.Conn) appuc.SettingsConnQueryer {
cc := c.(*postgres.Conn)
return connQueryer{Conn: cc, baseConfs: settings.baseConfs}
}

// Fetch queries the mutable settings from the settings repository,
// deserializes them, merges them into a clone of the base settings
// (representing the configuration file and environment variables state
// when the settings repository instance was created), and returns the
// fresh configuration instance as an appuc.Builder interface in
// addition to its visible settings (as an instance of the
// version-independent model.VisibleSettings struct).
func (cq connQueryer) Fetch(ctx context.Context) (
appuc.Builder, *model.VisibleSettings, error,
) {
return Fetch(ctx, cq.Conn, cq.baseConfs)
}

type txQueryer struct {
*postgres.Tx
baseConfs *cfg2.Config
}

// Tx takes a Tx interface instance, unwraps it as required,
// and returns a SettingsTxQueryer interface which (with access to the
// implementation-dependent transaction object) can run different
// permitted operations on settings.
// The txQueryer itself is not mentioned as the return value since
// it is not exported. Otherwise, the general rule is to take interfaces
// as arguments and return exported structs.
func (settings *Repo) Tx(tx repo.Tx) appuc.SettingsTxQueryer {
tt := tx.(*postgres.Tx)
return txQueryer{Tx: tt, baseConfs: settings.baseConfs}
}

// Update converts the version-independent mutable model.Settings
// instance into a version-dependent serializable settings instance
// for the last supported version, serializes them as JSON, and
// then stores them in the settings repository. Given mutable settings
// are also used in order to update a clone of the base settings.
// Updated configuration settings will be returned as an instance of
// the appuc.Builder interface in addition to its visible settings
// (which are provided as an instance of the version-independent
// model.VisibleSettings struct).
func (tq txQueryer) Update(
ctx context.Context, s *model.Settings,
) (appuc.Builder, *model.VisibleSettings, error) {
return Update(ctx, tq.Tx, tq.baseConfs, s)
}
13 changes: 7 additions & 6 deletions pkg/adapter/restful/gin/carsrs/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,30 +18,31 @@ import (
)

type resource struct {
cars *carsuc.UseCase
cars func() *carsuc.UseCase
}

// Register instantiates a resource adapting the cars use case instance
// with the relevant REST APIs including:
// 1. PATCH request to /api/caweb/v1/cars/:cid
// in order to ride or park a car.
func Register(r *gin.RouterGroup, cars *carsuc.UseCase) {
func Register(r *gin.RouterGroup, cars func() *carsuc.UseCase) {
rs := &resource{cars: cars}
r.PATCH("cars/:cid", rs.UpdateCar)
}

func (rs *resource) UpdateCar(c *gin.Context) {
req := rs.DserUpdateCarReq(c)
if req == nil {
req, ok := rs.DserUpdateCarReq(c)
if !ok {
return
}
carsUseCase := rs.cars()
var car *model.Car
var err error
switch req.Op {
case "ride":
car, err = rs.cars.Ride(c, req.CarID, req.Dst)
car, err = carsUseCase.Ride(c, req.CarID, req.Dst)
case "park":
car, err = rs.cars.Park(c, req.CarID, req.Mode)
car, err = carsUseCase.Park(c, req.CarID, req.Mode)
default:
panic("unexpected op:" + req.Op)
}
Expand Down
12 changes: 7 additions & 5 deletions pkg/adapter/restful/gin/carsrs/serdser.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ func (sc StrCoordinate) ToModel() (c model.Coordinate, err error) {
return
}

func (rs *resource) DserUpdateCarReq(c *gin.Context) *carUpdateReq {
func (rs *resource) DserUpdateCarReq(
c *gin.Context,
) (*carUpdateReq, bool) {
req := &rawCarUpdateReq{}
val := &carUpdateReq{}
if ok := serdser.Bind(c, req, binding.Form); !ok {
return nil
return nil, false
}
var errs map[string][]string
defer func() {
Expand All @@ -66,7 +68,7 @@ func (rs *resource) DserUpdateCarReq(c *gin.Context) *carUpdateReq {
val.CarID, err = uuid.Parse(c.Param("cid"))
if err != nil {
serdser.AddErr(&errs, "cid", "Path param cid is not UUID.")
return nil
return nil, false
}
val.Op = req.Op
switch req.Op {
Expand Down Expand Up @@ -100,7 +102,7 @@ func (rs *resource) DserUpdateCarReq(c *gin.Context) *carUpdateReq {
panic("unknown op")
}
if errs == nil {
return val
return val, true
}
return nil
return nil, false
}
20 changes: 16 additions & 4 deletions pkg/adapter/restful/gin/gin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/momeni/clean-arch/internal/test/dbcontainer"
"github.com/momeni/clean-arch/pkg/adapter/config/cfg2"
"github.com/momeni/clean-arch/pkg/adapter/config/settings"
"github.com/momeni/clean-arch/pkg/adapter/config/vers"
"github.com/momeni/clean-arch/pkg/adapter/db/postgres"
"github.com/momeni/clean-arch/pkg/adapter/restful/gin"
"github.com/momeni/clean-arch/pkg/adapter/restful/gin/routes"
Expand Down Expand Up @@ -83,11 +84,22 @@ func (igts *IntegrationGinTestSuite) SetupSuite() {
igts.Gin = gin.New(gin.Logger(), gin.Recovery())
igts.Require().NotNil(igts.Gin, "cannot instantiate Gin engine")
delay := settings.Duration(2 * time.Second)
err = routes.Register(igts.Gin, igts.Pool, cfg2.Usecases{
Cars: cfg2.Cars{
DelayOfOPM: &delay,
c := &cfg2.Config{
Usecases: cfg2.Usecases{
Cars: cfg2.Cars{
DelayOfOPM: &delay,
},
},
})
Vers: vers.Config{
Versions: vers.Versions{
Database: postgres.Version,
Config: cfg2.Version,
},
},
}
err = c.ValidateAndNormalize()
igts.Require().NoError(err, "preparing configuration settings")
err = routes.Register(igts.Ctx, igts.Gin, igts.Pool, c)
igts.Require().NoError(err, "failed to register Gin routes")
}

Expand Down
Loading

0 comments on commit dbda761

Please sign in to comment.