From 7bf489315e5433577877afddbcc83ba39566b381 Mon Sep 17 00:00:00 2001 From: Victor Lacasse-Beaudoin Date: Mon, 20 Nov 2023 15:13:42 -0500 Subject: [PATCH] feat(api): add pave spec to route /v1/spec and add seed to it Exposes the API spec in pave format, which intends to show information about all API routes. Also pave V1SeedPOST and V1SpecGET --- Dockerfile | 2 ++ api/api.go | 16 +++++++++++++--- apihandler/apihandler.go | 10 ++++++---- apihandler/spec.go | 36 ++++++++++++++++++++++++++++++++++++ apirequest/seed.go | 29 +++++++++++++++++++++++++++++ apirequest/spec.go | 29 +++++++++++++++++++++++++++++ apiresponse/apiresponse.go | 16 ++++------------ apiresponse/bucket.go | 4 ++-- apiresponse/document.go | 2 +- apiresponse/seed.go | 2 +- apiresponse/spec.go | 8 ++++++++ cmd/server.go | 18 +++++++++++++++++- go.mod | 1 + go.sum | 2 ++ webhandler/webhandler.go | 2 +- 15 files changed, 152 insertions(+), 25 deletions(-) create mode 100644 apihandler/spec.go create mode 100644 apirequest/seed.go create mode 100644 apirequest/spec.go create mode 100644 apiresponse/spec.go diff --git a/Dockerfile b/Dockerfile index 3b53c4a..6210ca7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,6 +16,8 @@ ADD api/ api/ ADD apihandler/ apihandler/ +ADD apirequest/ apirequest/ + ADD apiresponse/ apiresponse/ ADD config/ config/ diff --git a/api/api.go b/api/api.go index 9e8fbf7..15eb710 100644 --- a/api/api.go +++ b/api/api.go @@ -10,6 +10,7 @@ import ( "net/url" "codeberg.org/vlbeaudoin/voki" + "git.agecem.com/agecem/agecem-org/apirequest" "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/config" "github.com/spf13/viper" @@ -35,8 +36,8 @@ func New(client *http.Client, host, key string, port int, protocol string) (*API return &API{Voki: voki.New(client, host, key, port, protocol)}, nil } -func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentCreateResponse, error) { - var response apiresponse.V1DocumentCreateResponse +func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentCreate, error) { + var response apiresponse.V1DocumentCreate endpoint := fmt.Sprintf("%s://%s:%d", a.Voki.Protocol, a.Voki.Host, @@ -100,6 +101,15 @@ func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) ( return response, err } -func (a *API) ListBuckets() (response apiresponse.V1BucketListResponse, err error) { +func (a *API) ListBuckets() (response apiresponse.V1BucketList, err error) { return response, a.Voki.Unmarshal(http.MethodGet, "/v1/bucket", nil, true, &response) } + +func (a *API) Seed() (response apiresponse.V1SeedPOST, err error) { + request, err := apirequest.NewV1SeedPOST() + if err != nil { + return + } + + return request.Request(a.Voki) +} diff --git a/apihandler/apihandler.go b/apihandler/apihandler.go index d3d170f..2e4fc37 100644 --- a/apihandler/apihandler.go +++ b/apihandler/apihandler.go @@ -5,6 +5,7 @@ import ( "net/http" "sort" + "codeberg.org/vlbeaudoin/pave" "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/media" @@ -15,6 +16,7 @@ import ( type V1Handler struct { Config config.Config MediaClient *media.MediaClient + Pave *pave.Pave } // API Handlers @@ -30,7 +32,7 @@ func (h *V1Handler) HandleV1(c echo.Context) error { // HandleV1Seed créé des buckets dans minio selon la liste de buckets dans server.documents.buckets // Les buckets sont créés avec paramètres par défaut, et sont ensuite visible dans /v1/bucket. func (h *V1Handler) HandleV1Seed(c echo.Context) error { - var response apiresponse.V1SeedResponse + var response apiresponse.V1SeedPOST new_buckets, err := h.MediaClient.Seed() response.Data.Buckets = new_buckets @@ -56,7 +58,7 @@ func (h *V1Handler) HandleV1Seed(c echo.Context) error { // HandleV1BucketList affiche les buckets permis par server.documents.buckets, qui existent. func (h *V1Handler) HandleV1BucketList(c echo.Context) error { - var response apiresponse.V1BucketListResponse + var response apiresponse.V1BucketList var buckets = make(map[string]string) @@ -83,7 +85,7 @@ func (h *V1Handler) HandleV1BucketList(c echo.Context) error { } func (h *V1Handler) HandleV1BucketRead(c echo.Context) error { - var response apiresponse.V1BucketReadResponse + var response apiresponse.V1BucketRead bucket := c.Param("bucket") @@ -137,7 +139,7 @@ func (h *V1Handler) HandleV1BucketRead(c echo.Context) error { // HandleV1DocumentCreate permet d'ajouter un object dans un bucket, par multipart/form-data func (h *V1Handler) HandleV1DocumentCreate(c echo.Context) error { - var response apiresponse.V1DocumentCreateResponse + var response apiresponse.V1DocumentCreate bucket := c.Param("bucket") diff --git a/apihandler/spec.go b/apihandler/spec.go new file mode 100644 index 0000000..de394eb --- /dev/null +++ b/apihandler/spec.go @@ -0,0 +1,36 @@ +package apihandler + +import ( + "fmt" + "net/http" + + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" + "git.agecem.com/agecem/agecem-org/version" + "github.com/labstack/echo/v4" +) + +const DescriptionV1SpecGET string = "Afficher le API spec en format pave" + +func (h *V1Handler) HandleV1Spec(c echo.Context) error { + var request apirequest.V1SpecGET + var response apiresponse.V1SpecGET + + if !request.Complete() { + response.Message = "Incomplete V1SpecGET request received" + response.StatusCode = http.StatusBadRequest + + return c.JSON(response.StatusCode, response) + } + + response.Data.Spec = fmt.Sprintf("# pave spec for agecem-org %s", version.Version()) + + for _, route := range h.Pave.SortedRouteStrings() { + response.Data.Spec = fmt.Sprintf("%s%s", response.Data.Spec, route) + } + + response.Message = "ok" + response.StatusCode = http.StatusOK + + return c.JSON(response.StatusCode, response) +} diff --git a/apirequest/seed.go b/apirequest/seed.go new file mode 100644 index 0000000..89187d7 --- /dev/null +++ b/apirequest/seed.go @@ -0,0 +1,29 @@ +package apirequest + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1SeedPOST] = V1SeedPOST{} + +type V1SeedPOST struct{} + +func NewV1SeedPOST() (request V1SeedPOST, err error) { + return +} + +func (r V1SeedPOST) Complete() bool { return true } + +func (r V1SeedPOST) Request(v *voki.Voki) (response apiresponse.V1SeedPOST, err error) { + if !r.Complete() { + err = fmt.Errorf("Incomplete V1SeedPOST") + return + } + + return response, v.UnmarshalIfComplete(http.MethodPost, "/v1/seed", nil, true, &response) +} diff --git a/apirequest/spec.go b/apirequest/spec.go new file mode 100644 index 0000000..b2aaa4e --- /dev/null +++ b/apirequest/spec.go @@ -0,0 +1,29 @@ +package apirequest + +import ( + "fmt" + "net/http" + + "codeberg.org/vlbeaudoin/voki" + "codeberg.org/vlbeaudoin/voki/request" + "git.agecem.com/agecem/agecem-org/apiresponse" +) + +var _ request.Requester[apiresponse.V1SpecGET] = V1SpecGET{} + +type V1SpecGET struct{} + +func NewV1SpecGET() (request V1SpecGET, err error) { + return +} + +func (request V1SpecGET) Complete() bool { return true } + +func (request V1SpecGET) Request(v *voki.Voki) (response apiresponse.V1SpecGET, err error) { + if !request.Complete() { + err = fmt.Errorf("Incomplete V1SpecGET") + return + } + + return response, v.UnmarshalIfComplete(http.MethodGet, "/v1/spec", nil, true, &response) +} diff --git a/apiresponse/apiresponse.go b/apiresponse/apiresponse.go index d4e98da..89adb8b 100644 --- a/apiresponse/apiresponse.go +++ b/apiresponse/apiresponse.go @@ -2,27 +2,19 @@ package apiresponse import ( "net/http" + + "codeberg.org/vlbeaudoin/voki/response" ) -type Responder interface { - Respond() Responder -} - type Response struct { - StatusCode int `json:"status_code"` - Message string - Error string -} - -func (r Response) Respond() Responder { - return r + response.ResponseWithError } type SimpleResponse struct { Message string } -func (r SimpleResponse) Respond() Responder { +func (r SimpleResponse) Respond() response.Responder { return r } diff --git a/apiresponse/bucket.go b/apiresponse/bucket.go index 02c26a3..476fcf1 100644 --- a/apiresponse/bucket.go +++ b/apiresponse/bucket.go @@ -1,13 +1,13 @@ package apiresponse -type V1BucketListResponse struct { +type V1BucketList struct { Response Data struct { Buckets map[string]string } } -type V1BucketReadResponse struct { +type V1BucketRead struct { Response Data struct { Keys []string diff --git a/apiresponse/document.go b/apiresponse/document.go index 9e1677b..3e99d70 100644 --- a/apiresponse/document.go +++ b/apiresponse/document.go @@ -1,6 +1,6 @@ package apiresponse -type V1DocumentCreateResponse struct { +type V1DocumentCreate struct { Response Data struct { Bucket string diff --git a/apiresponse/seed.go b/apiresponse/seed.go index 858beab..b266c5a 100644 --- a/apiresponse/seed.go +++ b/apiresponse/seed.go @@ -1,6 +1,6 @@ package apiresponse -type V1SeedResponse struct { +type V1SeedPOST struct { Response Data struct { Buckets []string diff --git a/apiresponse/spec.go b/apiresponse/spec.go new file mode 100644 index 0000000..d9e87c7 --- /dev/null +++ b/apiresponse/spec.go @@ -0,0 +1,8 @@ +package apiresponse + +type V1SpecGET struct { + Response + Data struct { + Spec string + } +} diff --git a/cmd/server.go b/cmd/server.go index 1380154..073c447 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -13,12 +13,15 @@ import ( "io" "net/http" + "codeberg.org/vlbeaudoin/pave" "codeberg.org/vlbeaudoin/serpents" "github.com/spf13/cobra" "github.com/spf13/viper" "git.agecem.com/agecem/agecem-org/api" "git.agecem.com/agecem/agecem-org/apihandler" + "git.agecem.com/agecem/agecem-org/apirequest" + "git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/media" "git.agecem.com/agecem/agecem-org/public" @@ -205,14 +208,27 @@ func RunServer() { log.Fatal("Error during NewMediaClientFromViper for API handlers") } + p := pave.New() + v1Handler := apihandler.V1Handler{ Config: cfg, MediaClient: mediaClient, + Pave: &p, } groupV1.GET("", v1Handler.HandleV1) - groupV1.POST("/seed", v1Handler.HandleV1Seed) + if err := pave.EchoRegister[ + apirequest.V1SeedPOST, + apiresponse.V1SeedPOST](groupV1, &p, "/v1", http.MethodPost, "/seed", "Créer buckets manquants définis dans `server.documents.buckets`", "V1SeedPOST", v1Handler.HandleV1Seed); err != nil { + log.Fatal(err) + } + + if err := pave.EchoRegister[ + apirequest.V1SpecGET, + apiresponse.V1SpecGET](groupV1, &p, "/v1", http.MethodGet, "/spec", apihandler.DescriptionV1SpecGET, "V1SpecGET", v1Handler.HandleV1Spec); err != nil { + log.Fatal(err) + } groupV1.GET("/bucket", v1Handler.HandleV1BucketList) diff --git a/go.mod b/go.mod index 999b68e..40534c0 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.agecem.com/agecem/agecem-org go 1.21.1 require ( + codeberg.org/vlbeaudoin/pave v1.0.1 codeberg.org/vlbeaudoin/serpents v1.1.0 codeberg.org/vlbeaudoin/voki v1.7.1 github.com/labstack/echo/v4 v4.11.3 diff --git a/go.sum b/go.sum index 27815d3..6826586 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= +codeberg.org/vlbeaudoin/pave v1.0.1 h1:N7/TIb615By1nds5h+iKSZtPAFeuT3ceuUH/VG6t7Rw= +codeberg.org/vlbeaudoin/pave v1.0.1/go.mod h1:D/Lb/EmfJzl066A+2g4wc42e1Pb/l4nmXjIGouYBviM= codeberg.org/vlbeaudoin/serpents v1.1.0 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA= codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ= codeberg.org/vlbeaudoin/voki v1.7.1 h1:Eywgk2A8NQmg4vucJjtheUpB0S2RYlDS8A7VwP+wFHU= diff --git a/webhandler/webhandler.go b/webhandler/webhandler.go index cec2667..83bcb0b 100644 --- a/webhandler/webhandler.go +++ b/webhandler/webhandler.go @@ -66,7 +66,7 @@ func (h *WebHandler) HandleDocumentation(c echo.Context) error { for bucket, displayName := range v1BucketListResponse.Data.Buckets { // TODO move call to dedicated API client method - var v1BucketReadResponse apiresponse.V1BucketReadResponse + var v1BucketReadResponse apiresponse.V1BucketRead if err = h.ApiClient.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", bucket), nil, true, &v1BucketReadResponse); err != nil { response.Error = err.Error()