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
This commit is contained in:
Victor Lacasse-Beaudoin 2023-11-20 15:13:42 -05:00
parent 0c7009b16b
commit 7bf489315e
15 changed files with 152 additions and 25 deletions

View file

@ -16,6 +16,8 @@ ADD api/ api/
ADD apihandler/ apihandler/ ADD apihandler/ apihandler/
ADD apirequest/ apirequest/
ADD apiresponse/ apiresponse/ ADD apiresponse/ apiresponse/
ADD config/ config/ ADD config/ config/

View file

@ -10,6 +10,7 @@ import (
"net/url" "net/url"
"codeberg.org/vlbeaudoin/voki" "codeberg.org/vlbeaudoin/voki"
"git.agecem.com/agecem/agecem-org/apirequest"
"git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/apiresponse"
"git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/config"
"github.com/spf13/viper" "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 return &API{Voki: voki.New(client, host, key, port, protocol)}, nil
} }
func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentCreateResponse, error) { func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (apiresponse.V1DocumentCreate, error) {
var response apiresponse.V1DocumentCreateResponse var response apiresponse.V1DocumentCreate
endpoint := fmt.Sprintf("%s://%s:%d", endpoint := fmt.Sprintf("%s://%s:%d",
a.Voki.Protocol, a.Voki.Protocol,
a.Voki.Host, a.Voki.Host,
@ -100,6 +101,15 @@ func (a *API) UploadDocument(bucket string, file_header *multipart.FileHeader) (
return response, err 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) 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)
}

View file

@ -5,6 +5,7 @@ import (
"net/http" "net/http"
"sort" "sort"
"codeberg.org/vlbeaudoin/pave"
"git.agecem.com/agecem/agecem-org/apiresponse" "git.agecem.com/agecem/agecem-org/apiresponse"
"git.agecem.com/agecem/agecem-org/config" "git.agecem.com/agecem/agecem-org/config"
"git.agecem.com/agecem/agecem-org/media" "git.agecem.com/agecem/agecem-org/media"
@ -15,6 +16,7 @@ import (
type V1Handler struct { type V1Handler struct {
Config config.Config Config config.Config
MediaClient *media.MediaClient MediaClient *media.MediaClient
Pave *pave.Pave
} }
// API Handlers // 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 // 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. // 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 { func (h *V1Handler) HandleV1Seed(c echo.Context) error {
var response apiresponse.V1SeedResponse var response apiresponse.V1SeedPOST
new_buckets, err := h.MediaClient.Seed() new_buckets, err := h.MediaClient.Seed()
response.Data.Buckets = new_buckets 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. // HandleV1BucketList affiche les buckets permis par server.documents.buckets, qui existent.
func (h *V1Handler) HandleV1BucketList(c echo.Context) error { func (h *V1Handler) HandleV1BucketList(c echo.Context) error {
var response apiresponse.V1BucketListResponse var response apiresponse.V1BucketList
var buckets = make(map[string]string) 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 { func (h *V1Handler) HandleV1BucketRead(c echo.Context) error {
var response apiresponse.V1BucketReadResponse var response apiresponse.V1BucketRead
bucket := c.Param("bucket") 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 // HandleV1DocumentCreate permet d'ajouter un object dans un bucket, par multipart/form-data
func (h *V1Handler) HandleV1DocumentCreate(c echo.Context) error { func (h *V1Handler) HandleV1DocumentCreate(c echo.Context) error {
var response apiresponse.V1DocumentCreateResponse var response apiresponse.V1DocumentCreate
bucket := c.Param("bucket") bucket := c.Param("bucket")

36
apihandler/spec.go Normal file
View file

@ -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)
}

29
apirequest/seed.go Normal file
View file

@ -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)
}

29
apirequest/spec.go Normal file
View file

@ -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)
}

View file

@ -2,27 +2,19 @@ package apiresponse
import ( import (
"net/http" "net/http"
"codeberg.org/vlbeaudoin/voki/response"
) )
type Responder interface {
Respond() Responder
}
type Response struct { type Response struct {
StatusCode int `json:"status_code"` response.ResponseWithError
Message string
Error string
}
func (r Response) Respond() Responder {
return r
} }
type SimpleResponse struct { type SimpleResponse struct {
Message string Message string
} }
func (r SimpleResponse) Respond() Responder { func (r SimpleResponse) Respond() response.Responder {
return r return r
} }

View file

@ -1,13 +1,13 @@
package apiresponse package apiresponse
type V1BucketListResponse struct { type V1BucketList struct {
Response Response
Data struct { Data struct {
Buckets map[string]string Buckets map[string]string
} }
} }
type V1BucketReadResponse struct { type V1BucketRead struct {
Response Response
Data struct { Data struct {
Keys []string Keys []string

View file

@ -1,6 +1,6 @@
package apiresponse package apiresponse
type V1DocumentCreateResponse struct { type V1DocumentCreate struct {
Response Response
Data struct { Data struct {
Bucket string Bucket string

View file

@ -1,6 +1,6 @@
package apiresponse package apiresponse
type V1SeedResponse struct { type V1SeedPOST struct {
Response Response
Data struct { Data struct {
Buckets []string Buckets []string

8
apiresponse/spec.go Normal file
View file

@ -0,0 +1,8 @@
package apiresponse
type V1SpecGET struct {
Response
Data struct {
Spec string
}
}

View file

@ -13,12 +13,15 @@ import (
"io" "io"
"net/http" "net/http"
"codeberg.org/vlbeaudoin/pave"
"codeberg.org/vlbeaudoin/serpents" "codeberg.org/vlbeaudoin/serpents"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"git.agecem.com/agecem/agecem-org/api" "git.agecem.com/agecem/agecem-org/api"
"git.agecem.com/agecem/agecem-org/apihandler" "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/config"
"git.agecem.com/agecem/agecem-org/media" "git.agecem.com/agecem/agecem-org/media"
"git.agecem.com/agecem/agecem-org/public" "git.agecem.com/agecem/agecem-org/public"
@ -205,14 +208,27 @@ func RunServer() {
log.Fatal("Error during NewMediaClientFromViper for API handlers") log.Fatal("Error during NewMediaClientFromViper for API handlers")
} }
p := pave.New()
v1Handler := apihandler.V1Handler{ v1Handler := apihandler.V1Handler{
Config: cfg, Config: cfg,
MediaClient: mediaClient, MediaClient: mediaClient,
Pave: &p,
} }
groupV1.GET("", v1Handler.HandleV1) 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) groupV1.GET("/bucket", v1Handler.HandleV1BucketList)

1
go.mod
View file

@ -3,6 +3,7 @@ module git.agecem.com/agecem/agecem-org
go 1.21.1 go 1.21.1
require ( require (
codeberg.org/vlbeaudoin/pave v1.0.1
codeberg.org/vlbeaudoin/serpents v1.1.0 codeberg.org/vlbeaudoin/serpents v1.1.0
codeberg.org/vlbeaudoin/voki v1.7.1 codeberg.org/vlbeaudoin/voki v1.7.1
github.com/labstack/echo/v4 v4.11.3 github.com/labstack/echo/v4 v4.11.3

2
go.sum
View file

@ -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.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.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 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 h1:U9f2+2D1yUVHx90yePi2ZOLRLG/Wkoob4JXDIVyoBwA=
codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ= codeberg.org/vlbeaudoin/serpents v1.1.0/go.mod h1:3bE/R0ToABwcUJtS1VcGEBa86K5FYhrZGAbFl2qL8kQ=
codeberg.org/vlbeaudoin/voki v1.7.1 h1:Eywgk2A8NQmg4vucJjtheUpB0S2RYlDS8A7VwP+wFHU= codeberg.org/vlbeaudoin/voki v1.7.1 h1:Eywgk2A8NQmg4vucJjtheUpB0S2RYlDS8A7VwP+wFHU=

View file

@ -66,7 +66,7 @@ func (h *WebHandler) HandleDocumentation(c echo.Context) error {
for bucket, displayName := range v1BucketListResponse.Data.Buckets { for bucket, displayName := range v1BucketListResponse.Data.Buckets {
// TODO move call to dedicated API client method // 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 { if err = h.ApiClient.Voki.Unmarshal(http.MethodGet, fmt.Sprintf("/v1/bucket/%s", bucket), nil, true, &v1BucketReadResponse); err != nil {
response.Error = err.Error() response.Error = err.Error()