From c94098afef98a24195636b0d080eacf1d1368040 Mon Sep 17 00:00:00 2001 From: Vojtech Mares Date: Thu, 10 Oct 2024 21:29:08 +0200 Subject: [PATCH] feat: add basic app --- cmd/api/main.go | 35 + go.mod | 29 + go.sum | 74 ++ internal/currency/currency.go | 13 + internal/faker/faker.go | 71 ++ internal/server/api.gen.go | 1009 ++++++++++++++++++++++ internal/server/server.go | 114 +++ internal/server/training.go | 167 ++++ internal/training/errors.go | 7 + internal/training/inmemory_repository.go | 105 +++ internal/training/model.go | 76 ++ internal/training/postgres_repository.go | 129 +++ internal/training/repository.go | 21 + 13 files changed, 1850 insertions(+) create mode 100644 cmd/api/main.go create mode 100644 internal/currency/currency.go create mode 100644 internal/faker/faker.go create mode 100644 internal/server/api.gen.go create mode 100644 internal/server/server.go create mode 100644 internal/server/training.go create mode 100644 internal/training/errors.go create mode 100644 internal/training/inmemory_repository.go create mode 100644 internal/training/model.go create mode 100644 internal/training/postgres_repository.go create mode 100644 internal/training/repository.go diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 0000000..719c7d4 --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "context" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/faker" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/server" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training" + "log" + "os" + "os/signal" + "syscall" +) + +func main() { + logger := log.New(os.Stdout, "backoffice-api: ", log.LstdFlags) + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer cancel() + + hostname, err := os.Hostname() + if err != nil { + logger.Fatalf("error: %v\n", err) + } + + trainingRepo := training.NewInMemoryRepository() + f := faker.NewFaker(trainingRepo) + err = f.GenerateFakeData() + if err != nil { + logger.Fatalf("error: %v\n", err) + } + + s := server.NewServer(hostname, logger, trainingRepo) + if err := s.Run(ctx); err != nil { + logger.Fatalf("error: %v\n", err) + } +} diff --git a/go.mod b/go.mod index 215b796..cfa02d3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,32 @@ module gitlab.mareshq.com/hq/backoffice/backoffice-api go 1.23.1 + +require ( + github.com/getkin/kin-openapi v0.127.0 + github.com/go-chi/chi/v5 v5.1.0 + github.com/jackc/pgx/v5 v5.7.1 + github.com/oapi-codegen/nethttp-middleware v1.0.2 + github.com/oapi-codegen/runtime v1.1.1 + github.com/shopspring/decimal v1.4.0 +) + +require ( + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/text v0.18.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index e69de29..5d4010e 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,74 @@ +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= +github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= +github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oapi-codegen/nethttp-middleware v1.0.2 h1:A5tfAcKJhWIbIPnlQH+l/DtfVE1i5TFgPlQAiW+l1vQ= +github.com/oapi-codegen/nethttp-middleware v1.0.2/go.mod h1:DfDalonSO+eRQ3RTb8kYoWZByCCPFRxm9WKq1UbY0E4= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/currency/currency.go b/internal/currency/currency.go new file mode 100644 index 0000000..4d29744 --- /dev/null +++ b/internal/currency/currency.go @@ -0,0 +1,13 @@ +package currency + +type Currency string + +const ( + USD Currency = "USD" + EUR Currency = "EUR" + CZK Currency = "CZK" +) + +var ( + SupportedCurrencies = []Currency{USD, EUR, CZK} +) diff --git a/internal/faker/faker.go b/internal/faker/faker.go new file mode 100644 index 0000000..cb43f97 --- /dev/null +++ b/internal/faker/faker.go @@ -0,0 +1,71 @@ +package faker + +import ( + "context" + "github.com/shopspring/decimal" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/currency" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training" +) + +type Faker struct { + trainingRepository training.Repository +} + +func NewFaker(trainingRepository training.Repository) *Faker { + return &Faker{ + trainingRepository: trainingRepository, + } +} + +func (f *Faker) GenerateFakeData() error { + var t *training.Training + + oneDayPricing := []training.Price{ + { + Currency: currency.CZK, + Amount: decimal.NewFromInt(5900), + Type: training.OpenTrainingPriceType, + }, + { + Currency: currency.CZK, + Amount: decimal.NewFromInt(24000), + Type: training.CorporateTrainingPriceType, + }, + } + + twoDayPricing := []training.Price{ + { + Currency: currency.CZK, + Amount: decimal.NewFromInt(9900), + Type: training.OpenTrainingPriceType, + }, + { + Currency: currency.CZK, + Amount: decimal.NewFromInt(44000), + Type: training.CorporateTrainingPriceType, + }, + } + + t = training.NewTraining("Kubernetes v1", 1, nil) + + err := f.trainingRepository.Create(context.Background(), t) + if err != nil { + return err + } + + t = training.NewTraining("Kubernetes", 2, twoDayPricing) + + err = f.trainingRepository.Create(context.Background(), t) + if err != nil { + return err + } + + t = training.NewTraining("Terraform", 1, oneDayPricing) + + err = f.trainingRepository.Create(context.Background(), t) + if err != nil { + return err + } + + return nil +} diff --git a/internal/server/api.gen.go b/internal/server/api.gen.go new file mode 100644 index 0000000..fd7317c --- /dev/null +++ b/internal/server/api.gen.go @@ -0,0 +1,1009 @@ +// Package server provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/oapi-codegen/oapi-codegen/v2 version v2.3.0 DO NOT EDIT. +package server + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/getkin/kin-openapi/openapi3" + "github.com/go-chi/chi/v5" + "github.com/oapi-codegen/runtime" + strictnethttp "github.com/oapi-codegen/runtime/strictmiddleware/nethttp" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/currency" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training" +) + +// Defines values for TrainingPriceType. +const ( + CORPORATE TrainingPriceType = "CORPORATE" + OPEN TrainingPriceType = "OPEN" +) + +// NewTraining defines model for NewTraining. +type NewTraining struct { + Days int8 `json:"days"` + Name string `json:"name"` + Pricing []TrainingPrice `json:"pricing"` +} + +// Price defines model for Price. +type Price struct { + Amount string `json:"amount"` + Currency currency.Currency `json:"currency"` +} + +// ProblemDetails Schema that carries the details of an error in an HTTP response. See https://datatracker.ietf.org/doc/html/rfc7807 for more information. +type ProblemDetails struct { + // Detail A human-readable explanation specific to this occurrence of the problem. + Detail string `json:"detail"` + + // Instance A URI reference that identifies the specific occurrence of the problem. + Instance string `json:"instance"` + + // Status The HTTP status code generated by the origin server for this occurrence of the problem. + Status int `json:"status"` + + // Title A human-readable summary of the problem type. + Title string `json:"title"` + + // Type A URI reference that identifies the problem type. + Type string `json:"type"` +} + +// Training defines model for Training. +type Training struct { + Days int8 `json:"days"` + Id TrainingID `json:"id"` + Name string `json:"name"` + Pricing []TrainingPrice `json:"pricing"` + Published bool `json:"published"` + Retired bool `json:"retired"` +} + +// TrainingID defines model for TrainingID. +type TrainingID = training.ID + +// TrainingPrice defines model for TrainingPrice. +type TrainingPrice struct { + Amount string `json:"amount"` + Currency currency.Currency `json:"currency"` + Type TrainingPriceType `json:"type"` +} + +// TrainingPriceType defines model for TrainingPrice.Type. +type TrainingPriceType string + +// CreateTrainingResponse defines model for CreateTrainingResponse. +type CreateTrainingResponse = Training + +// GetTrainingResponse defines model for GetTrainingResponse. +type GetTrainingResponse = Training + +// InternalError Schema that carries the details of an error in an HTTP response. See https://datatracker.ietf.org/doc/html/rfc7807 for more information. +type InternalError = ProblemDetails + +// InvalidInputError Schema that carries the details of an error in an HTTP response. See https://datatracker.ietf.org/doc/html/rfc7807 for more information. +type InvalidInputError = ProblemDetails + +// ListTrainingsResponse defines model for ListTrainingsResponse. +type ListTrainingsResponse struct { + // Trainings List of trainings + Trainings *[]Training `json:"trainings,omitempty"` +} + +// NotFoundError Schema that carries the details of an error in an HTTP response. See https://datatracker.ietf.org/doc/html/rfc7807 for more information. +type NotFoundError = ProblemDetails + +// CreateTrainingRequest defines model for CreateTrainingRequest. +type CreateTrainingRequest struct { + Training *NewTraining `json:"training,omitempty"` +} + +// CreateTrainingJSONBody defines parameters for CreateTraining. +type CreateTrainingJSONBody struct { + Training *NewTraining `json:"training,omitempty"` +} + +// CreateTrainingJSONRequestBody defines body for CreateTraining for application/json ContentType. +type CreateTrainingJSONRequestBody CreateTrainingJSONBody + +// ServerInterface represents all server handlers. +type ServerInterface interface { + // List all trainings + // (GET /v0/trainings) + ListTrainings(w http.ResponseWriter, r *http.Request) + // Create a new training + // (POST /v0/trainings) + CreateTraining(w http.ResponseWriter, r *http.Request) + // Get a training by ID + // (GET /v0/trainings/{trainingID}) + GetTrainingByID(w http.ResponseWriter, r *http.Request, trainingID TrainingID) + // Publish a training + // (PUT /v0/trainings/{trainingID}/publish) + PublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) + // Retire a training + // (PUT /v0/trainings/{trainingID}/retire) + RetireTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) + // Unpublish a training + // (PUT /v0/trainings/{trainingID}/unpublish) + UnpublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) +} + +// Unimplemented server implementation that returns http.StatusNotImplemented for each endpoint. + +type Unimplemented struct{} + +// List all trainings +// (GET /v0/trainings) +func (_ Unimplemented) ListTrainings(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Create a new training +// (POST /v0/trainings) +func (_ Unimplemented) CreateTraining(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Get a training by ID +// (GET /v0/trainings/{trainingID}) +func (_ Unimplemented) GetTrainingByID(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Publish a training +// (PUT /v0/trainings/{trainingID}/publish) +func (_ Unimplemented) PublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Retire a training +// (PUT /v0/trainings/{trainingID}/retire) +func (_ Unimplemented) RetireTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Unpublish a training +// (PUT /v0/trainings/{trainingID}/unpublish) +func (_ Unimplemented) UnpublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + w.WriteHeader(http.StatusNotImplemented) +} + +// ServerInterfaceWrapper converts contexts to parameters. +type ServerInterfaceWrapper struct { + Handler ServerInterface + HandlerMiddlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +type MiddlewareFunc func(http.Handler) http.Handler + +// ListTrainings operation middleware +func (siw *ServerInterfaceWrapper) ListTrainings(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListTrainings(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// CreateTraining operation middleware +func (siw *ServerInterfaceWrapper) CreateTraining(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateTraining(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetTrainingByID operation middleware +func (siw *ServerInterfaceWrapper) GetTrainingByID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "trainingID" ------------- + var trainingID TrainingID + + err = runtime.BindStyledParameterWithOptions("simple", "trainingID", chi.URLParam(r, "trainingID"), &trainingID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "trainingID", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetTrainingByID(w, r, trainingID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// PublishTraining operation middleware +func (siw *ServerInterfaceWrapper) PublishTraining(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "trainingID" ------------- + var trainingID TrainingID + + err = runtime.BindStyledParameterWithOptions("simple", "trainingID", chi.URLParam(r, "trainingID"), &trainingID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "trainingID", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PublishTraining(w, r, trainingID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// RetireTraining operation middleware +func (siw *ServerInterfaceWrapper) RetireTraining(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "trainingID" ------------- + var trainingID TrainingID + + err = runtime.BindStyledParameterWithOptions("simple", "trainingID", chi.URLParam(r, "trainingID"), &trainingID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "trainingID", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.RetireTraining(w, r, trainingID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// UnpublishTraining operation middleware +func (siw *ServerInterfaceWrapper) UnpublishTraining(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "trainingID" ------------- + var trainingID TrainingID + + err = runtime.BindStyledParameterWithOptions("simple", "trainingID", chi.URLParam(r, "trainingID"), &trainingID, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "trainingID", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UnpublishTraining(w, r, trainingID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +type UnescapedCookieParamError struct { + ParamName string + Err error +} + +func (e *UnescapedCookieParamError) Error() string { + return fmt.Sprintf("error unescaping cookie parameter '%s'", e.ParamName) +} + +func (e *UnescapedCookieParamError) Unwrap() error { + return e.Err +} + +type UnmarshalingParamError struct { + ParamName string + Err error +} + +func (e *UnmarshalingParamError) Error() string { + return fmt.Sprintf("Error unmarshaling parameter %s as JSON: %s", e.ParamName, e.Err.Error()) +} + +func (e *UnmarshalingParamError) Unwrap() error { + return e.Err +} + +type RequiredParamError struct { + ParamName string +} + +func (e *RequiredParamError) Error() string { + return fmt.Sprintf("Query argument %s is required, but not found", e.ParamName) +} + +type RequiredHeaderError struct { + ParamName string + Err error +} + +func (e *RequiredHeaderError) Error() string { + return fmt.Sprintf("Header parameter %s is required, but not found", e.ParamName) +} + +func (e *RequiredHeaderError) Unwrap() error { + return e.Err +} + +type InvalidParamFormatError struct { + ParamName string + Err error +} + +func (e *InvalidParamFormatError) Error() string { + return fmt.Sprintf("Invalid format for parameter %s: %s", e.ParamName, e.Err.Error()) +} + +func (e *InvalidParamFormatError) Unwrap() error { + return e.Err +} + +type TooManyValuesForParamError struct { + ParamName string + Count int +} + +func (e *TooManyValuesForParamError) Error() string { + return fmt.Sprintf("Expected one value for %s, got %d", e.ParamName, e.Count) +} + +// Handler creates http.Handler with routing matching OpenAPI spec. +func Handler(si ServerInterface) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{}) +} + +type ChiServerOptions struct { + BaseURL string + BaseRouter chi.Router + Middlewares []MiddlewareFunc + ErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +// HandlerFromMux creates http.Handler with routing matching OpenAPI spec based on the provided mux. +func HandlerFromMux(si ServerInterface, r chi.Router) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseRouter: r, + }) +} + +func HandlerFromMuxWithBaseURL(si ServerInterface, r chi.Router, baseURL string) http.Handler { + return HandlerWithOptions(si, ChiServerOptions{ + BaseURL: baseURL, + BaseRouter: r, + }) +} + +// HandlerWithOptions creates http.Handler with additional options +func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handler { + r := options.BaseRouter + + if r == nil { + r = chi.NewRouter() + } + if options.ErrorHandlerFunc == nil { + options.ErrorHandlerFunc = func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + } + } + wrapper := ServerInterfaceWrapper{ + Handler: si, + HandlerMiddlewares: options.Middlewares, + ErrorHandlerFunc: options.ErrorHandlerFunc, + } + + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/v0/trainings", wrapper.ListTrainings) + }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/v0/trainings", wrapper.CreateTraining) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/v0/trainings/{trainingID}", wrapper.GetTrainingByID) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/v0/trainings/{trainingID}/publish", wrapper.PublishTraining) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/v0/trainings/{trainingID}/retire", wrapper.RetireTraining) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/v0/trainings/{trainingID}/unpublish", wrapper.UnpublishTraining) + }) + + return r +} + +type CreateTrainingResponseJSONResponse Training + +type GetTrainingResponseJSONResponse Training + +type InternalErrorApplicationProblemPlusJSONResponse ProblemDetails + +type InvalidInputErrorApplicationProblemPlusJSONResponse ProblemDetails + +type ListTrainingsResponseJSONResponse struct { + // Trainings List of trainings + Trainings *[]Training `json:"trainings,omitempty"` +} + +type NotFoundErrorApplicationProblemPlusJSONResponse ProblemDetails + +type ListTrainingsRequestObject struct { +} + +type ListTrainingsResponseObject interface { + VisitListTrainingsResponse(w http.ResponseWriter) error +} + +type ListTrainings200JSONResponse struct { + ListTrainingsResponseJSONResponse +} + +func (response ListTrainings200JSONResponse) VisitListTrainingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type ListTrainings500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response ListTrainings500ApplicationProblemPlusJSONResponse) VisitListTrainingsResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type CreateTrainingRequestObject struct { + Body *CreateTrainingJSONRequestBody +} + +type CreateTrainingResponseObject interface { + VisitCreateTrainingResponse(w http.ResponseWriter) error +} + +type CreateTraining201JSONResponse struct { + CreateTrainingResponseJSONResponse +} + +func (response CreateTraining201JSONResponse) VisitCreateTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(201) + + return json.NewEncoder(w).Encode(response) +} + +type CreateTraining400ApplicationProblemPlusJSONResponse struct { + InvalidInputErrorApplicationProblemPlusJSONResponse +} + +func (response CreateTraining400ApplicationProblemPlusJSONResponse) VisitCreateTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(400) + + return json.NewEncoder(w).Encode(response) +} + +type CreateTraining500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response CreateTraining500ApplicationProblemPlusJSONResponse) VisitCreateTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrainingByIDRequestObject struct { + TrainingID TrainingID `json:"trainingID"` +} + +type GetTrainingByIDResponseObject interface { + VisitGetTrainingByIDResponse(w http.ResponseWriter) error +} + +type GetTrainingByID200JSONResponse struct { + GetTrainingResponseJSONResponse +} + +func (response GetTrainingByID200JSONResponse) VisitGetTrainingByIDResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrainingByID404ApplicationProblemPlusJSONResponse struct { + NotFoundErrorApplicationProblemPlusJSONResponse +} + +func (response GetTrainingByID404ApplicationProblemPlusJSONResponse) VisitGetTrainingByIDResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type GetTrainingByID500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response GetTrainingByID500ApplicationProblemPlusJSONResponse) VisitGetTrainingByIDResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type PublishTrainingRequestObject struct { + TrainingID TrainingID `json:"trainingID"` +} + +type PublishTrainingResponseObject interface { + VisitPublishTrainingResponse(w http.ResponseWriter) error +} + +type PublishTraining204Response struct { +} + +func (response PublishTraining204Response) VisitPublishTrainingResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type PublishTraining404ApplicationProblemPlusJSONResponse struct { + NotFoundErrorApplicationProblemPlusJSONResponse +} + +func (response PublishTraining404ApplicationProblemPlusJSONResponse) VisitPublishTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type PublishTraining500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response PublishTraining500ApplicationProblemPlusJSONResponse) VisitPublishTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type RetireTrainingRequestObject struct { + TrainingID TrainingID `json:"trainingID"` +} + +type RetireTrainingResponseObject interface { + VisitRetireTrainingResponse(w http.ResponseWriter) error +} + +type RetireTraining204Response struct { +} + +func (response RetireTraining204Response) VisitRetireTrainingResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type RetireTraining404ApplicationProblemPlusJSONResponse struct { + NotFoundErrorApplicationProblemPlusJSONResponse +} + +func (response RetireTraining404ApplicationProblemPlusJSONResponse) VisitRetireTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type RetireTraining500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response RetireTraining500ApplicationProblemPlusJSONResponse) VisitRetireTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +type UnpublishTrainingRequestObject struct { + TrainingID TrainingID `json:"trainingID"` +} + +type UnpublishTrainingResponseObject interface { + VisitUnpublishTrainingResponse(w http.ResponseWriter) error +} + +type UnpublishTraining204Response struct { +} + +func (response UnpublishTraining204Response) VisitUnpublishTrainingResponse(w http.ResponseWriter) error { + w.WriteHeader(204) + return nil +} + +type UnpublishTraining404ApplicationProblemPlusJSONResponse struct { + NotFoundErrorApplicationProblemPlusJSONResponse +} + +func (response UnpublishTraining404ApplicationProblemPlusJSONResponse) VisitUnpublishTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(404) + + return json.NewEncoder(w).Encode(response) +} + +type UnpublishTraining500ApplicationProblemPlusJSONResponse struct { + InternalErrorApplicationProblemPlusJSONResponse +} + +func (response UnpublishTraining500ApplicationProblemPlusJSONResponse) VisitUnpublishTrainingResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(500) + + return json.NewEncoder(w).Encode(response) +} + +// StrictServerInterface represents all server handlers. +type StrictServerInterface interface { + // List all trainings + // (GET /v0/trainings) + ListTrainings(ctx context.Context, request ListTrainingsRequestObject) (ListTrainingsResponseObject, error) + // Create a new training + // (POST /v0/trainings) + CreateTraining(ctx context.Context, request CreateTrainingRequestObject) (CreateTrainingResponseObject, error) + // Get a training by ID + // (GET /v0/trainings/{trainingID}) + GetTrainingByID(ctx context.Context, request GetTrainingByIDRequestObject) (GetTrainingByIDResponseObject, error) + // Publish a training + // (PUT /v0/trainings/{trainingID}/publish) + PublishTraining(ctx context.Context, request PublishTrainingRequestObject) (PublishTrainingResponseObject, error) + // Retire a training + // (PUT /v0/trainings/{trainingID}/retire) + RetireTraining(ctx context.Context, request RetireTrainingRequestObject) (RetireTrainingResponseObject, error) + // Unpublish a training + // (PUT /v0/trainings/{trainingID}/unpublish) + UnpublishTraining(ctx context.Context, request UnpublishTrainingRequestObject) (UnpublishTrainingResponseObject, error) +} + +type StrictHandlerFunc = strictnethttp.StrictHTTPHandlerFunc +type StrictMiddlewareFunc = strictnethttp.StrictHTTPMiddlewareFunc + +type StrictHTTPServerOptions struct { + RequestErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) + ResponseErrorHandlerFunc func(w http.ResponseWriter, r *http.Request, err error) +} + +func NewStrictHandler(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: StrictHTTPServerOptions{ + RequestErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + }, + ResponseErrorHandlerFunc: func(w http.ResponseWriter, r *http.Request, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) + }, + }} +} + +func NewStrictHandlerWithOptions(ssi StrictServerInterface, middlewares []StrictMiddlewareFunc, options StrictHTTPServerOptions) ServerInterface { + return &strictHandler{ssi: ssi, middlewares: middlewares, options: options} +} + +type strictHandler struct { + ssi StrictServerInterface + middlewares []StrictMiddlewareFunc + options StrictHTTPServerOptions +} + +// ListTrainings operation middleware +func (sh *strictHandler) ListTrainings(w http.ResponseWriter, r *http.Request) { + var request ListTrainingsRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.ListTrainings(ctx, request.(ListTrainingsRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "ListTrainings") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(ListTrainingsResponseObject); ok { + if err := validResponse.VisitListTrainingsResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// CreateTraining operation middleware +func (sh *strictHandler) CreateTraining(w http.ResponseWriter, r *http.Request) { + var request CreateTrainingRequestObject + + var body CreateTrainingJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err)) + return + } + request.Body = &body + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.CreateTraining(ctx, request.(CreateTrainingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "CreateTraining") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(CreateTrainingResponseObject); ok { + if err := validResponse.VisitCreateTrainingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// GetTrainingByID operation middleware +func (sh *strictHandler) GetTrainingByID(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + var request GetTrainingByIDRequestObject + + request.TrainingID = trainingID + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetTrainingByID(ctx, request.(GetTrainingByIDRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetTrainingByID") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetTrainingByIDResponseObject); ok { + if err := validResponse.VisitGetTrainingByIDResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// PublishTraining operation middleware +func (sh *strictHandler) PublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + var request PublishTrainingRequestObject + + request.TrainingID = trainingID + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.PublishTraining(ctx, request.(PublishTrainingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "PublishTraining") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(PublishTrainingResponseObject); ok { + if err := validResponse.VisitPublishTrainingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// RetireTraining operation middleware +func (sh *strictHandler) RetireTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + var request RetireTrainingRequestObject + + request.TrainingID = trainingID + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.RetireTraining(ctx, request.(RetireTrainingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "RetireTraining") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(RetireTrainingResponseObject); ok { + if err := validResponse.VisitRetireTrainingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// UnpublishTraining operation middleware +func (sh *strictHandler) UnpublishTraining(w http.ResponseWriter, r *http.Request, trainingID TrainingID) { + var request UnpublishTrainingRequestObject + + request.TrainingID = trainingID + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.UnpublishTraining(ctx, request.(UnpublishTrainingRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "UnpublishTraining") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(UnpublishTrainingResponseObject); ok { + if err := validResponse.VisitUnpublishTrainingResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + +// Base64 encoded, gzipped, json marshaled Swagger object +var swaggerSpec = []string{ + + "H4sIAAAAAAAC/8xY3VLkNhN9FVV/312MPftXS81VdoFsqCQwxUIuQrjQyO2xWFvySjLsLOUnyVXeJXmv", + "lCT/js0yEKomdwZJ3adPt1qn5w6YzAspUBgN8zsoqKI5GlTur3NFueBidXxo/4pRM8ULw6WAORwfEpkQ", + "kyIx9S4IgNuVgpoUAhA0R5iD6WwEoPBzyRXGMDeqxAA0SzGn1vj/FSYwh/9FHZ7Ir+qoB6OqKm8FtXkv", + "Y44O54FCarDZduaX7QKTwqBwn7QoMs6oRR9daxvCXc99oWSBytT22ogeAHaCt41TsMDMurAhy+U1MmOx", + "erS6kEJPI/VLj4K6DVOep2HCGmfEevLbCCUCb7M1YQ5W3KWyCuADmv8QTs3FKsMBwGNhUAmaHSkl1Teg", + "FUouM8y/exzEhT91iIbyTE8BbfwTdAAcohua8fhYFKXZHSqHgXALooP2M9dtOvWT8jl9RfS4M1hXrje0", + "WwLgBnO9fVW0V4kqRdfTd2u7uslGYKoATqT5QZYi3lWOTqQhiQXQ5KdqWqHjqN9WRsTHdF1zntAyMzB/", + "EUAiVU4NzIELsw8B5PQLz8sc5m8CyLnw3y9aDrkwuEJXFr5J3zUr2qia/0JxVvt/VO4WijOcTGDX+y+9", + "28DH0vm6GmU5AG9vxALNZenzNULOSqVQsLVdRGEjv4SD336CAI4uziCAi4+HPU/1uQC+7K3kXv3PxkR4", + "0Njqre8VlH2iKw/KPnVzWHGT0WWYU4U6/RwymUfp52hJ2SeZJJxh73OPFjzideeIWqzVJkOs81zHOs3O", + "oNRGV/Gjyw4xKTWEUaU4avdkx/6AvRhU+CIkXNjvH8/PF6R5skLyEZGkxhR6HkUxNdQoyj6hCjmaJJRq", + "FcWSRanJs0gl7O3+7C1JpCK5VEi48GXJpQh/Fy7Ngzp2EMaQ35G0zKnYU0hjusyQ4Jcio8LZIbpAxhPO", + "iJHEpFwTyWqisFEj9Z0NIRiXBhfaUOHradPrxdkxUZigN+YY4zEKw5OGtNb545xqQ005kZvzFD3bfgNh", + "MkayQoHKPcXLtbMsFV9xQTSqG1SO3K3j7l1zw02GW3Ctyzynar1hk1iDk7H5fzyFzAdMb9wGt9qE0VIa", + "NEXUy+zULek3U5plpwnMLx8h7ILN5sPjxyjWAIpymXGdYtzrV0spM6QCXKDGxzle3KCBx9C31h0dR33V", + "i9vL9288BMPu1zyVoZPsXd/jeSGVeY62Z/qiefhybJ2i5qEZCZO6JJvWf7o4OoEADk7PFqdn786Pxr1/", + "stimGK1cC0lkoxgoc2xg7hoZ3Mhrgyz93vERsq/dDPSrvDZ//cFS8gtV+PefEECp7Imms97e3obtqSqA", + "jDOs5VltYaFkoTgaqta9+wzvW4LJu8UxBHCDSvtLOAtfhjO7VxYoaMFhDq/CWfjKVhA1qeMquplFAyG3", + "QheR5dM13OO41nPnPS03GGhezmb3XYZ2XzQtPqsA3mxzeqjznVbybarRmjTLBmLTUBvNJXSg7W0opJ4I", + "bjiQQX+4XN8PrTd/RtPDZzXi6cXDkd4zHVYBvN6OqM3x4zko9qD8qNif9adYroJhTUV33fhf3VtgvUnz", + "/do1nf7PEPf0gW7LoNtePaU+p0ZdR/rrh88OZ4nnIPwDGkJbpq0QcJw8mu+ofilcxy4neF/49V7xPyfv", + "ryckTxNT94btjuY6+h7VTyLZP8H3cnzmlndAcSMNdkewD/3f8luKh8r4otmxA5ZbdDtlumVgC7LtQTdM", + "eGY2frqRjGYkxhvMZJGjMPXgMVAs8yjK7L5UajPfn+3PIifDhpZ6ykQburJcTZiy4qdeDjuxGN7kDkLI", + "vkZWuXzTeqFkXDI/HU476BnuS1Vn+ar6JwAA//8BndtsAhcAAA==", +} + +// GetSwagger returns the content of the embedded swagger specification file +// or error if failed to decode +func decodeSpec() ([]byte, error) { + zipped, err := base64.StdEncoding.DecodeString(strings.Join(swaggerSpec, "")) + if err != nil { + return nil, fmt.Errorf("error base64 decoding spec: %w", err) + } + zr, err := gzip.NewReader(bytes.NewReader(zipped)) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + var buf bytes.Buffer + _, err = buf.ReadFrom(zr) + if err != nil { + return nil, fmt.Errorf("error decompressing spec: %w", err) + } + + return buf.Bytes(), nil +} + +var rawSpec = decodeSpecCached() + +// a naive cached of a decoded swagger spec +func decodeSpecCached() func() ([]byte, error) { + data, err := decodeSpec() + return func() ([]byte, error) { + return data, err + } +} + +// Constructs a synthetic filesystem for resolving external references when loading openapi specifications. +func PathToRawSpec(pathToFile string) map[string]func() ([]byte, error) { + res := make(map[string]func() ([]byte, error)) + if len(pathToFile) > 0 { + res[pathToFile] = rawSpec + } + + return res +} + +// GetSwagger returns the Swagger specification corresponding to the generated code +// in this file. The external references of Swagger specification are resolved. +// The logic of resolving external references is tightly connected to "import-mapping" feature. +// Externally referenced files must be embedded in the corresponding golang packages. +// Urls can be supported but this task was out of the scope. +func GetSwagger() (swagger *openapi3.T, err error) { + resolvePath := PathToRawSpec("") + + loader := openapi3.NewLoader() + loader.IsExternalRefsAllowed = true + loader.ReadFromURIFunc = func(loader *openapi3.Loader, url *url.URL) ([]byte, error) { + pathToFile := url.String() + pathToFile = path.Clean(pathToFile) + getSpec, ok := resolvePath[pathToFile] + if !ok { + err1 := fmt.Errorf("path not found: %s", pathToFile) + return nil, err1 + } + return getSpec() + } + var specData []byte + specData, err = rawSpec() + if err != nil { + return + } + swagger, err = loader.LoadFromData(specData) + if err != nil { + return + } + return +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..c37c514 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,114 @@ +package server + +import ( + "context" + "errors" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + oapimiddleware "github.com/oapi-codegen/nethttp-middleware" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training" + "log" + "net/http" + "time" +) + +type Server struct { + logger *log.Logger + hostname string + + trainingRepository training.Repository + + router *chi.Mux + srv *http.Server + tls *TLS +} + +type TLS struct { + CertFile string + KeyFile string +} + +func NewServer(hostname string, logger *log.Logger, trainingRepository training.Repository) *Server { + return &Server{ + logger: logger, + hostname: hostname, + trainingRepository: trainingRepository, + router: chi.NewRouter(), + tls: nil, + } +} + +func NewServerWithTLS(hostname string, tls *TLS, logger *log.Logger, trainingRepository training.Repository) *Server { + s := NewServer(hostname, logger, trainingRepository) + s.tls = tls + return s +} + +func (s *Server) Run(ctx context.Context) error { + swagger, err := GetSwagger() + if err != nil { + return err + } + + // Clear out the servers array in the swagger spec, that skips validating + // that server names match. We don't know how this thing will be run. + swagger.Servers = nil + + s.router.Use(middleware.Logger) + // middleware.Recoverer recovers from panics, logs the panic (and a stack trace), + s.router.Use(middleware.Recoverer) + // we trust headers, since we are running on Kubernetes with Ingress Controller (Ingress-NGINX) + // and behind an L4 load balancer (on Hetzner Cloud) + s.router.Use(middleware.RealIP) + // middleware.RequestID generates a request ID and adds it to request context + // if the request has an `X-Request-ID` header, it will use that as the request ID + s.router.Use(middleware.RequestID) + s.router.Use(middleware.Timeout(10 * time.Second)) + s.router.Use(oapimiddleware.OapiRequestValidator(swagger)) + + // create handler + h := NewStrictHandler(s, nil) + + // register endpoints + HandlerFromMux(h, s.router) + + s.srv = &http.Server{ + Addr: ":8080", // TODO: make port configurable + Handler: s.router, + } + + go func() { + var err error + if s.tls != nil { + s.logger.Printf("Starting HTTPS server on: %s\n", s.srv.Addr) + err = s.srv.ListenAndServeTLS(s.tls.CertFile, s.tls.KeyFile) + } else { + s.logger.Printf("Starting HTTP server on: %s\n", s.srv.Addr) + err = s.srv.ListenAndServe() + } + // suppress the error if the server was closed gracefully + if err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Printf("error: %v\n", err) + } + }() + + // wait for the context to be done + // context done means the server is shutting down + <-ctx.Done() + + // TODO: make graceful shutdown period configurable + timeout := 10 * time.Second + timeoutCtx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + s.logger.Printf("Shutting down server in %s seconds\n", timeout.String()) + err = s.srv.Shutdown(timeoutCtx) + // suppress the error if the server was closed gracefully + if err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + + s.logger.Println("Server shutdown successfully") + + return nil +} diff --git a/internal/server/training.go b/internal/server/training.go new file mode 100644 index 0000000..9273af3 --- /dev/null +++ b/internal/server/training.go @@ -0,0 +1,167 @@ +package server + +import ( + "context" + "github.com/shopspring/decimal" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/training" +) + +func (s *Server) CreateTraining(ctx context.Context, req CreateTrainingRequestObject) (CreateTrainingResponseObject, error) { + + pricing := make([]training.Price, len(req.Body.Training.Pricing)) + for i, p := range req.Body.Training.Pricing { + amount, err := decimal.NewFromString(p.Amount) + if err != nil { + return CreateTraining400ApplicationProblemPlusJSONResponse{ + InvalidInputErrorApplicationProblemPlusJSONResponse{ + Title: "Invalid amount", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + pricing[i] = training.Price{ + Currency: p.Currency, + Amount: amount, + Type: training.PriceType(p.Type), + } + } + + newTr := training.NewTraining(req.Body.Training.Name, req.Body.Training.Days, pricing) + + err := s.trainingRepository.Create(ctx, newTr) + if err != nil { + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + return CreateTraining500ApplicationProblemPlusJSONResponse{ + InternalErrorApplicationProblemPlusJSONResponse{ + Title: "Internal Server Error", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + tr := trainingToAPITraining(newTr) + return CreateTraining201JSONResponse{ + CreateTrainingResponseJSONResponse(tr), + }, nil +} + +func (s *Server) GetTrainingByID(ctx context.Context, req GetTrainingByIDRequestObject) (GetTrainingByIDResponseObject, error) { + tr, err := s.trainingRepository.FindByID(ctx, req.TrainingID) + if err != nil { + return GetTrainingByID404ApplicationProblemPlusJSONResponse{ + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + NotFoundErrorApplicationProblemPlusJSONResponse{ + Title: "Not Found Training by ID", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + trAPI := trainingToAPITraining(tr) + return GetTrainingByID200JSONResponse{ + GetTrainingResponseJSONResponse(trAPI), + }, nil +} + +func (s *Server) ListTrainings(ctx context.Context, _ ListTrainingsRequestObject) (ListTrainingsResponseObject, error) { + trs, err := s.trainingRepository.FindAll(ctx) + if err != nil { + return ListTrainings500ApplicationProblemPlusJSONResponse{ + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + InternalErrorApplicationProblemPlusJSONResponse{ + Title: "Internal Server Error", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + var trAPIs []Training + for _, tr := range trs { + trAPIs = append(trAPIs, trainingToAPITraining(&tr)) + } + + return ListTrainings200JSONResponse{ + ListTrainingsResponseJSONResponse{ + Trainings: &trAPIs, + }, + }, nil +} + +func (s *Server) PublishTraining(ctx context.Context, req PublishTrainingRequestObject) (PublishTrainingResponseObject, error) { + err := s.trainingRepository.Publish(ctx, req.TrainingID) + if err != nil { + return PublishTraining500ApplicationProblemPlusJSONResponse{ + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + InternalErrorApplicationProblemPlusJSONResponse{ + Title: "Internal Server Error", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + return PublishTraining204Response{}, nil +} + +func (s *Server) UnpublishTraining(ctx context.Context, req UnpublishTrainingRequestObject) (UnpublishTrainingResponseObject, error) { + err := s.trainingRepository.Unpublish(ctx, req.TrainingID) + if err != nil { + return UnpublishTraining500ApplicationProblemPlusJSONResponse{ + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + InternalErrorApplicationProblemPlusJSONResponse{ + Title: "Internal Server Error", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + return UnpublishTraining204Response{}, nil +} + +func (s *Server) RetireTraining(ctx context.Context, req RetireTrainingRequestObject) (RetireTrainingResponseObject, error) { + err := s.trainingRepository.Retire(ctx, req.TrainingID) + if err != nil { + return RetireTraining500ApplicationProblemPlusJSONResponse{ + // returning response and nil as error + // since we suppress the error in code here and return an error response instead + InternalErrorApplicationProblemPlusJSONResponse{ + Title: "Internal Server Error", + Detail: err.Error(), + Instance: s.hostname, + }, + }, nil + } + + return RetireTraining204Response{}, nil +} + +func trainingToAPITraining(t *training.Training) Training { + tPricing := t.Pricing() + pricing := make([]TrainingPrice, len(tPricing)) + for i, p := range tPricing { + pricing[i] = TrainingPrice{ + Currency: p.Currency, + Amount: p.Amount.String(), + Type: TrainingPriceType(p.Type), + } + } + return Training{ + Id: t.ID(), + Name: t.Name(), + Days: t.Days(), + Published: t.Published(), + Retired: t.Retired(), + Pricing: pricing, + } +} diff --git a/internal/training/errors.go b/internal/training/errors.go new file mode 100644 index 0000000..e5040f6 --- /dev/null +++ b/internal/training/errors.go @@ -0,0 +1,7 @@ +package training + +type ErrNotFound struct{} + +func (ErrNotFound) Error() string { + return "not found" +} diff --git a/internal/training/inmemory_repository.go b/internal/training/inmemory_repository.go new file mode 100644 index 0000000..e3236ee --- /dev/null +++ b/internal/training/inmemory_repository.go @@ -0,0 +1,105 @@ +package training + +import ( + "context" + "sync" +) + +type InMemoryRepository struct { + m sync.Mutex + nextID ID + + trainings map[ID]*Training +} + +type InMemoryPricingRepository struct{} + +func NewInMemoryRepository() *InMemoryRepository { + return &InMemoryRepository{ + m: sync.Mutex{}, + nextID: 1, + trainings: make(map[ID]*Training), + } +} + +func (r *InMemoryRepository) FindAll(_ context.Context) ([]Training, error) { + r.m.Lock() + defer r.m.Unlock() + + var trainings []Training + for _, t := range r.trainings { + trainings = append(trainings, *t) + } + + return trainings, nil +} + +func (r *InMemoryRepository) FindByID(_ context.Context, id ID) (*Training, error) { + r.m.Lock() + defer r.m.Unlock() + + t, ok := r.trainings[id] + if !ok { + return nil, &ErrNotFound{} + } + return t, nil +} + +func (r *InMemoryRepository) Create(_ context.Context, training *Training) error { + r.m.Lock() + defer r.m.Unlock() + + training.id = r.nextID + r.nextID++ + r.trainings[training.id] = training + + return nil +} + +func (r *InMemoryRepository) Update(_ context.Context, training *Training) error { + r.m.Lock() + defer r.m.Unlock() + + if _, ok := r.trainings[training.id]; !ok { + return &ErrNotFound{} + } + + r.trainings[training.id] = training + return nil +} + +func (r *InMemoryRepository) Publish(_ context.Context, id ID) error { + r.m.Lock() + defer r.m.Unlock() + + t, ok := r.trainings[id] + if !ok { + return &ErrNotFound{} + } + t.published = true + return nil +} + +func (r *InMemoryRepository) Unpublish(_ context.Context, id ID) error { + r.m.Lock() + defer r.m.Unlock() + + t, ok := r.trainings[id] + if !ok { + return &ErrNotFound{} + } + t.published = false + return nil +} + +func (r *InMemoryRepository) Retire(_ context.Context, id ID) error { + r.m.Lock() + defer r.m.Unlock() + + t, ok := r.trainings[id] + if !ok { + return &ErrNotFound{} + } + t.retired = true + return nil +} diff --git a/internal/training/model.go b/internal/training/model.go new file mode 100644 index 0000000..1fc6f50 --- /dev/null +++ b/internal/training/model.go @@ -0,0 +1,76 @@ +package training + +import ( + "github.com/shopspring/decimal" + "gitlab.mareshq.com/hq/backoffice/backoffice-api/internal/currency" +) + +type ID = int + +type Training struct { + id ID // unique and auto-incrementing + published bool + retired bool + + days int8 + + // In the future, this should be localized + name string + pricing []Price `db:"-"` +} + +type Price struct { + Amount decimal.Decimal + Currency currency.Currency + Type PriceType +} + +type PriceType string + +const ( + OpenTrainingPriceType PriceType = "OPEN" + CorporateTrainingPriceType PriceType = "CORPORATE" +) + +func NewTraining(name string, days int8, pricing []Price) *Training { + t := &Training{ + // metadata + published: false, + retired: false, + + // data + name: name, + days: days, + pricing: make([]Price, 0), + } + + if pricing != nil { + t.pricing = pricing + } + + return t +} + +func (t *Training) ID() ID { + return t.id +} + +func (t *Training) Days() int8 { + return t.days +} + +func (t *Training) Published() bool { + return t.published +} + +func (t *Training) Retired() bool { + return t.retired +} + +func (t *Training) Name() string { + return t.name +} + +func (t *Training) Pricing() []Price { + return t.pricing +} diff --git a/internal/training/postgres_repository.go b/internal/training/postgres_repository.go new file mode 100644 index 0000000..fd99641 --- /dev/null +++ b/internal/training/postgres_repository.go @@ -0,0 +1,129 @@ +package training + +import ( + "context" + "github.com/jackc/pgx/v5/pgxpool" + "log" +) + +type PostgresRepository struct { + pg *pgxpool.Pool + logger *log.Logger +} + +func NewPostgresRepository(logger *log.Logger, pg *pgxpool.Pool) *PostgresRepository { + return &PostgresRepository{ + pg: pg, + logger: logger, + } +} + +func (r *PostgresRepository) Create(ctx context.Context, training *Training) error { + tx, txErr := r.pg.Begin(ctx) + if txErr != nil { + return txErr + } + + queryErr := tx.QueryRow(ctx, ` + INSERT INTO training.trainings (name, days, published) + VALUES ($1, $2, $3, $4, $5) + RETURNING id + `, training.name, training.days, training.published).Scan(&training.id) + if queryErr != nil { + return queryErr + } + + // TODO: insert pricing using the transaction (tx) + + return nil +} + +func (r *PostgresRepository) Update(ctx context.Context, training *Training) error { + _, err := r.pg.Exec(ctx, ` + UPDATE training.trainings + SET title = $2 + WHERE id = $1 + `, training.ID, training.name) + if err != nil { + r.logger.Printf("error: %v\n", err) + return err + } + return nil +} + +func (r *PostgresRepository) FindByID(ctx context.Context, id ID) (*Training, error) { + var t Training + err := r.pg.QueryRow(ctx, ` + SELECT id, title, published, retired + FROM training.trainings + WHERE id = $1 + `, id).Scan(&t.id, &t.name, &t.published, &t.retired) + if err != nil { + r.logger.Printf("error: %v\n", err) + return nil, err + } + return &t, nil +} + +func (r *PostgresRepository) FindAll(ctx context.Context) ([]Training, error) { + rows, err := r.pg.Query(ctx, ` + SELECT id, title, published, retired + FROM training.trainings + `) + if err != nil { + r.logger.Printf("error: %v\n", err) + return nil, err + } + defer rows.Close() + + var trainings []Training + for rows.Next() { + var t Training + err := rows.Scan(&t.id, &t.name, &t.published, &t.retired) + if err != nil { + r.logger.Printf("error: %v\n", err) + return nil, err + } + trainings = append(trainings, t) + } + return trainings, nil +} + +func (r *PostgresRepository) Publish(ctx context.Context, id ID) error { + _, err := r.pg.Exec(ctx, ` + UPDATE training.trainings + SET published = true + WHERE id = $1 + `, id) + if err != nil { + r.logger.Printf("error: %v\n", err) + return err + } + return nil +} + +func (r *PostgresRepository) Unpublish(ctx context.Context, id ID) error { + _, err := r.pg.Exec(ctx, ` + UPDATE training.trainings + SET published = false + WHERE id = $1 + `, id) + if err != nil { + r.logger.Printf("error: %v\n", err) + return err + } + return nil +} + +func (r *PostgresRepository) Retire(ctx context.Context, id ID) error { + _, err := r.pg.Exec(ctx, ` + UPDATE training.trainings + SET retired = true + WHERE id = $1 + `, id) + if err != nil { + r.logger.Printf("error: %v\n", err) + return err + } + return nil +} diff --git a/internal/training/repository.go b/internal/training/repository.go new file mode 100644 index 0000000..c9b2200 --- /dev/null +++ b/internal/training/repository.go @@ -0,0 +1,21 @@ +package training + +import ( + "context" +) + +type Repository interface { + FindAll(ctx context.Context) ([]Training, error) + FindByID(ctx context.Context, id ID) (*Training, error) + Create(ctx context.Context, training *Training) error + Update(ctx context.Context, training *Training) error + Publish(ctx context.Context, id ID) error + Unpublish(ctx context.Context, id ID) error + Retire(ctx context.Context, id ID) error +} + +type PricingRepository interface { + UpdateForCurrency(ctx context.Context, trainingID ID, currency string, price float64) error + AddCurrency(ctx context.Context, trainingID ID, currency string, price float64) error + RemoveCurrency(ctx context.Context, trainingID ID, currency string) error +}