diff --git a/internal/cli/main.go b/internal/cli/main.go index 24ee51e..5fbd156 100644 --- a/internal/cli/main.go +++ b/internal/cli/main.go @@ -1,6 +1,8 @@ package cli import ( + "context" + "github.com/alecthomas/kingpin" "github.com/rarimo/bio-data-svc/internal/config" "github.com/rarimo/bio-data-svc/internal/service" @@ -39,7 +41,7 @@ func Run(args []string) bool { switch cmd { case serviceCmd.FullCommand(): - service.Run(cfg) + service.Run(context.Background(), cfg) case migrateUpCmd.FullCommand(): err = MigrateUp(cfg) case migrateDownCmd.FullCommand(): diff --git a/internal/service/handlers/add_data.go b/internal/service/handlers/add_data.go new file mode 100644 index 0000000..cdc81b9 --- /dev/null +++ b/internal/service/handlers/add_data.go @@ -0,0 +1,58 @@ +package handlers + +import ( + "encoding/base64" + "net/http" + + "github.com/google/uuid" + "github.com/rarimo/bio-data-svc/internal/data" + "github.com/rarimo/bio-data-svc/internal/service/requests" + "github.com/rarimo/bio-data-svc/resources" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func AddData(w http.ResponseWriter, r *http.Request) { + req, err := requests.AddData(r) + if err != nil { + Log(r).WithError(err).Error("invalid request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + value, err := base64.StdEncoding.DecodeString(req.Data.Attributes.Value) + if err != nil { + Log(r).WithError(err).WithField("value", req.Data.Attributes.Value).Error("failed to decode base64 string") + ape.RenderErr(w, problems.InternalError()) + return + } + + kv := data.KV{ + Key: uuid.NewString(), + Value: value, + } + + if err = KVQ(r).Insert(kv); err != nil { + Log(r).WithError(err).WithField("kv", kv).Error("failed to insert new key-value") + ape.RenderErr(w, problems.InternalError()) + return + } + + w.WriteHeader(http.StatusCreated) + ape.Render(w, newKVResponse(kv)) +} + +func newKVResponse(kv data.KV) resources.ValueResponse { + return resources.ValueResponse{ + Data: resources.Value{ + Key: resources.Key{ + ID: kv.Key, + Type: resources.VALUE, + }, + Attributes: resources.ValueAttributes{ + Value: base64.StdEncoding.EncodeToString(kv.Value), + Key: kv.Key, + }, + }, + } +} diff --git a/internal/service/handlers/ctx.go b/internal/service/handlers/ctx.go index b4ed886..19021d4 100644 --- a/internal/service/handlers/ctx.go +++ b/internal/service/handlers/ctx.go @@ -4,6 +4,7 @@ import ( "context" "net/http" + "github.com/rarimo/bio-data-svc/internal/data" "gitlab.com/distributed_lab/logan/v3" ) @@ -11,6 +12,7 @@ type ctxKey int const ( logCtxKey ctxKey = iota + kvCtxKey ctxKey = iota ) func CtxLog(entry *logan.Entry) func(context.Context) context.Context { @@ -22,3 +24,13 @@ func CtxLog(entry *logan.Entry) func(context.Context) context.Context { func Log(r *http.Request) *logan.Entry { return r.Context().Value(logCtxKey).(*logan.Entry) } + +func CtxKVQ(stateQ data.KVQ) func(context.Context) context.Context { + return func(ctx context.Context) context.Context { + return context.WithValue(ctx, kvCtxKey, stateQ) + } +} + +func KVQ(r *http.Request) data.KVQ { + return r.Context().Value(kvCtxKey).(data.KVQ) +} diff --git a/internal/service/handlers/get_data.go b/internal/service/handlers/get_data.go new file mode 100644 index 0000000..60695b8 --- /dev/null +++ b/internal/service/handlers/get_data.go @@ -0,0 +1,51 @@ +package handlers + +import ( + "encoding/base64" + "net/http" + + "github.com/rarimo/bio-data-svc/internal/service/requests" + "gitlab.com/distributed_lab/ape" + "gitlab.com/distributed_lab/ape/problems" +) + +func GetData(w http.ResponseWriter, r *http.Request) { + req, err := requests.NewGetDataRequest(r) + if err != nil { + Log(r).WithError(err).Error("invalid request") + ape.RenderErr(w, problems.BadRequest(err)...) + return + } + + kvQuery := KVQ(r) + + if req.Key != nil { + kvQuery = kvQuery.FilterByKey(*req.Key) + } + + if req.Value != nil { + value, err := base64.StdEncoding.DecodeString(*req.Value) + if err != nil { + Log(r).WithError(err).WithField("value", *req.Value).Error("failed to decode base64 string") + ape.RenderErr(w, problems.InternalError()) + return + } + + kvQuery = kvQuery.FilterByValue(value) + } + + kv, err := kvQuery.Get() + if err != nil { + Log(r).WithError(err).Error("failed to get key-value") + ape.RenderErr(w, problems.InternalError()) + return + } + + if kv == nil { + Log(r).Error("no key-value row found") + ape.RenderErr(w, problems.NotFound()) + return + } + + ape.Render(w, newKVResponse(*kv)) +} diff --git a/internal/service/main.go b/internal/service/main.go index da7f567..35c64b5 100644 --- a/internal/service/main.go +++ b/internal/service/main.go @@ -1,42 +1,36 @@ package service import ( + "context" "net" - "net/http" "github.com/rarimo/bio-data-svc/internal/config" + "gitlab.com/distributed_lab/ape" "gitlab.com/distributed_lab/kit/copus/types" "gitlab.com/distributed_lab/logan/v3" - "gitlab.com/distributed_lab/logan/v3/errors" ) type service struct { log *logan.Entry copus types.Copus listener net.Listener + cfg config.Config } -func (s *service) run() error { +func (s *service) run(ctx context.Context) { s.log.Info("Service started") - r := s.router() - - if err := s.copus.RegisterChi(r); err != nil { - return errors.Wrap(err, "cop failed") - } - - return http.Serve(s.listener, r) + ape.Serve(ctx, s.router(), s.cfg, ape.ServeOpts{}) } func newService(cfg config.Config) *service { return &service{ - log: cfg.Log(), + log: cfg.Log().WithField("service", "api"), copus: cfg.Copus(), listener: cfg.Listener(), + cfg: cfg, } } -func Run(cfg config.Config) { - if err := newService(cfg).run(); err != nil { - panic(err) - } +func Run(ctx context.Context, cfg config.Config) { + newService(cfg).run(ctx) } diff --git a/internal/service/requests/add_data.go b/internal/service/requests/add_data.go new file mode 100644 index 0000000..388327b --- /dev/null +++ b/internal/service/requests/add_data.go @@ -0,0 +1,21 @@ +package requests + +import ( + "encoding/json" + "net/http" + + "github.com/go-ozzo/ozzo-validation/is" + val "github.com/go-ozzo/ozzo-validation/v4" + "github.com/rarimo/bio-data-svc/resources" +) + +func AddData(r *http.Request) (req resources.AddValueRequest, err error) { + if err = json.NewDecoder(r.Body).Decode(&req); err != nil { + return req, val.Errors{"body": err} + } + + return req, val.Errors{ + "data/type": val.Validate(req.Data.Type, val.Required, val.In(resources.VALUE)), + "data/attributes/value": val.Validate(req.Data.Attributes.Value, val.Required, is.Base64), + }.Filter() +} diff --git a/internal/service/requests/get_data.go b/internal/service/requests/get_data.go new file mode 100644 index 0000000..441d215 --- /dev/null +++ b/internal/service/requests/get_data.go @@ -0,0 +1,26 @@ +package requests + +import ( + "net/http" + + "github.com/go-ozzo/ozzo-validation/is" + val "github.com/go-ozzo/ozzo-validation/v4" + "gitlab.com/distributed_lab/urlval/v4" +) + +type GetDataRequest struct { + Key *string `filter:"key"` + Value *string `filter:"value"` +} + +func NewGetDataRequest(r *http.Request) (req GetDataRequest, err error) { + if err = urlval.Decode(r.URL.Query(), &req); err != nil { + return req, val.Errors{"query": err} + } + + err = val.Errors{ + "key": val.Validate(req.Key, val.When(!val.IsEmpty(req.Key), is.UUID)), + "value": val.Validate(req.Value, val.When(!val.IsEmpty(req.Value), is.Base64)), + }.Filter() + return +} diff --git a/internal/service/router.go b/internal/service/router.go index 36af9b9..8adfdd4 100644 --- a/internal/service/router.go +++ b/internal/service/router.go @@ -2,6 +2,7 @@ package service import ( "github.com/go-chi/chi" + "github.com/rarimo/bio-data-svc/internal/data/pg" "github.com/rarimo/bio-data-svc/internal/service/handlers" "gitlab.com/distributed_lab/ape" ) @@ -14,10 +15,14 @@ func (s *service) router() chi.Router { ape.LoganMiddleware(s.log), ape.CtxMiddleware( handlers.CtxLog(s.log), + handlers.CtxKVQ(pg.NewKVQ(s.cfg.DB().Clone())), ), ) r.Route("/integrations/bio-data-svc", func(r chi.Router) { - // configure endpoints here + r.Route("/value", func(r chi.Router) { + r.Post("/", handlers.AddData) + r.Get("/", handlers.GetData) + }) }) return r