diff --git a/Makefile b/Makefile index b506048a..85754f7e 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,14 @@ default: ci ci: fmt lint cover -ci-full: ci dependencies-analyze bench +ci-full: ci dependencies-analyze openapi-check test-all-modules lint-markdown bench test: go test ./... +test-all-modules: + ./test.sh + cover: go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out @@ -30,14 +33,26 @@ lint: go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run lint-markdown: - markdownlint --dot . + markdownlint --ignore documentation/node_modules --dot . + +# Update golden files +golden-update: + (cd examples/petstore && go test -update) +# Check OpenAPI spec generated for the Petstore example. Uses https://github.com/daveshanley/vacuum +openapi-check: + vacuum lint -d examples/petstore/testdata/doc/openapi.json + +# Examples example: ( cd examples/full-app-gourmet && go run . -debug ) example-watch: ( cd examples/full-app-gourmet && air -- -debug ) +petstore: + ( cd examples/petstore && go run . -debug ) + # Documentation website docs: go run golang.org/x/pkgsite/cmd/pkgsite@latest -http localhost:8084 @@ -46,4 +61,5 @@ docs-open: go run golang.org/x/pkgsite/cmd/pkgsite@latest -http localhost:8084 -open .PHONY: docs-open docs example-watch example lint lint-markdown fmt ci ci-full -.PHONY: dependencies-analyze build bench cover-web cover test +.PHONY: dependencies-analyze build bench cover-web cover test petstore test-all-modules +.PHONY: golden-update openapi-check diff --git a/examples/petstore/controllers/pets.go b/examples/petstore/controllers/pets.go new file mode 100644 index 00000000..a7e13dac --- /dev/null +++ b/examples/petstore/controllers/pets.go @@ -0,0 +1,89 @@ +//coverage:ignore +package controller + +import ( + "github.com/go-fuego/fuego" +) + +type PetsRessources struct { + PetsService PetsService +} + +type Pets struct { + ID string `json:"id"` + Name string `json:"name" example:"Napoleon"` + Age int `json:"age" example:"18"` +} + +type PetsCreate struct { + Name string `json:"name" validate:"required,min=1,max=100" example:"Napoleon"` + Age int `json:"age" validate:"max=100" example:"18"` +} + +type PetsUpdate struct { + Name string `json:"name" validate:"min=1,max=100" example:"Napoleon"` + Age int `json:"age" validate:"max=100" example:"18"` +} + +func (rs PetsRessources) Routes(s *fuego.Server) { + petsGroup := fuego.Group(s, "/pets") + + fuego.Get(petsGroup, "/", rs.getAllPets) + fuego.Post(petsGroup, "/", rs.postPets) + + fuego.Get(petsGroup, "/{id}", rs.getPets) + fuego.Put(petsGroup, "/{id}", rs.putPets) + fuego.Delete(petsGroup, "/{id}", rs.deletePets) +} + +func (rs PetsRessources) getAllPets(c fuego.ContextNoBody) ([]Pets, error) { + return rs.PetsService.GetAllPets() +} + +func (rs PetsRessources) postPets(c *fuego.ContextWithBody[PetsCreate]) (Pets, error) { + body, err := c.Body() + if err != nil { + return Pets{}, err + } + + new, err := rs.PetsService.CreatePets(body) + if err != nil { + return Pets{}, err + } + + return new, nil +} + +func (rs PetsRessources) getPets(c fuego.ContextNoBody) (Pets, error) { + id := c.PathParam("id") + + return rs.PetsService.GetPets(id) +} + +func (rs PetsRessources) putPets(c *fuego.ContextWithBody[PetsUpdate]) (Pets, error) { + id := c.PathParam("id") + + body, err := c.Body() + if err != nil { + return Pets{}, err + } + + new, err := rs.PetsService.UpdatePets(id, body) + if err != nil { + return Pets{}, err + } + + return new, nil +} + +func (rs PetsRessources) deletePets(c *fuego.ContextNoBody) (any, error) { + return rs.PetsService.DeletePets(c.PathParam("id")) +} + +type PetsService interface { + GetPets(id string) (Pets, error) + CreatePets(PetsCreate) (Pets, error) + GetAllPets() ([]Pets, error) + UpdatePets(id string, input PetsUpdate) (Pets, error) + DeletePets(id string) (any, error) +} diff --git a/examples/petstore/main.go b/examples/petstore/main.go new file mode 100644 index 00000000..a22f8b4a --- /dev/null +++ b/examples/petstore/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/go-fuego/fuego" + controller "github.com/go-fuego/fuego/examples/petstore/controllers" +) + +func newPetStoreServer(options ...func(*fuego.Server)) *fuego.Server { + s := fuego.NewServer(options...) + + petsRessources := controller.PetsRessources{ + PetsService: nil, // Dependency injection: we can pass a service here (for example a database service) + } + petsRessources.Routes(s) + + return s +} + +func main() { + newPetStoreServer().Run() +} diff --git a/examples/petstore/main_test.go b/examples/petstore/main_test.go new file mode 100644 index 00000000..05fbb7d3 --- /dev/null +++ b/examples/petstore/main_test.go @@ -0,0 +1,30 @@ +package main + +import ( + "context" + "os" + "testing" + + "github.com/go-fuego/fuego" + "github.com/stretchr/testify/require" + "gotest.tools/v3/golden" +) + +func TestPetstoreOpenAPIGeneration(t *testing.T) { + server := newPetStoreServer( + fuego.WithoutStartupMessages(), + fuego.WithOpenAPIConfig(fuego.OpenAPIConfig{ + JsonFilePath: "testdata/doc/openapi.json", + PrettyFormatJson: true, + }), + ) + + server.OutputOpenAPISpec() + err := server.OpenApiSpec.Validate(context.Background()) + require.NoError(t, err) + + generatedSpec, err := os.ReadFile("testdata/doc/openapi.json") + require.NoError(t, err) + + golden.Assert(t, string(generatedSpec), "doc/openapi.golden.json") +} diff --git a/examples/petstore/testdata/doc/openapi.golden.json b/examples/petstore/testdata/doc/openapi.golden.json new file mode 100644 index 00000000..2c9e6872 --- /dev/null +++ b/examples/petstore/testdata/doc/openapi.golden.json @@ -0,0 +1,271 @@ +{ + "openapi": "3.0.3", + "components": { + "requestBodies": { + "PetsCreate": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PetsCreate" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/PetsCreate" + } + } + }, + "description": "Request body for controller.PetsCreate", + "required": true + }, + "PetsUpdate": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PetsUpdate" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/PetsUpdate" + } + } + }, + "description": "Request body for controller.PetsUpdate", + "required": true + } + }, + "schemas": { + "Pets": { + "properties": { + "age": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "PetsCreate": { + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "PetsUpdate": { + "properties": { + "age": { + "type": "integer" + }, + "name": { + "type": "string" + } + }, + "type": "object" + }, + "unknown-interface": {} + } + }, + "info": { + "description": "OpenAPI", + "title": "OpenAPI", + "version": "0.0.1" + }, + "paths": { + "/pets/": { + "get": { + "description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.getAllPets`\n\n---\n\n", + "operationId": "GET_/pets/", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Pets" + }, + "type": "array" + } + }, + "application/xml": { + "schema": { + "items": { + "$ref": "#/components/schemas/Pets" + }, + "type": "array" + } + } + }, + "description": "OK" + }, + "default": { + "description": "" + } + }, + "summary": "get all pets", + "tags": [ + "pets" + ] + }, + "post": { + "description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.postPets`\n\n---\n\n", + "operationId": "POST_/pets/", + "requestBody": { + "$ref": "#/components/requestBodies/PetsCreate" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + }, + "description": "OK" + }, + "default": { + "description": "" + } + }, + "summary": "post pets", + "tags": [ + "pets" + ] + } + }, + "/pets/{id}": { + "delete": { + "description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.deletePets`\n\n---\n\n", + "operationId": "DELETE_/pets/:id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/unknown-interface" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/unknown-interface" + } + } + }, + "description": "OK" + }, + "default": { + "description": "" + } + }, + "summary": "delete pets", + "tags": [ + "pets" + ] + }, + "get": { + "description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.getPets`\n\n---\n\n", + "operationId": "GET_/pets/:id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + }, + "description": "OK" + }, + "default": { + "description": "" + } + }, + "summary": "get pets", + "tags": [ + "pets" + ] + }, + "put": { + "description": "controller: `github.com/go-fuego/fuego/examples/petstore/controllers.PetsRessources.putPets`\n\n---\n\n", + "operationId": "PUT_/pets/:id", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/PetsUpdate" + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pets" + } + } + }, + "description": "OK" + }, + "default": { + "description": "" + } + }, + "summary": "put pets", + "tags": [ + "pets" + ] + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 891773b1..aaf235f8 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/stretchr/testify v1.9.0 gopkg.in/yaml.v3 v3.0.1 + gotest.tools/v3 v3.5.1 ) require ( @@ -20,6 +21,7 @@ require ( github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/invopop/yaml v0.3.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index d3c7ff62..10a8da5b 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,7 @@ 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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= -github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= -github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8= -github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM= github.com/getkin/kin-openapi v0.126.0 h1:c2cSgLnAsS0xYfKsgt5oBV6MYRM/giU8/RtwUY4wyfY= github.com/getkin/kin-openapi v0.126.0/go.mod h1:7mONz8IwmSRg6RttPu6v8U/OJ+gr+J99qSFNjPGSQqw= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -18,24 +14,20 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47 h1:k4Tw0nt6lwro3Uin8eqoET7MDA4JnT8YgbCjc/g5E3k= -github.com/gomarkdown/markdown v0.0.0-20231222211730-1d6d20845b47/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024 h1:saBP362Qm7zDdDXqv61kI4rzhmLFq3Z1gx34xpl6cWE= github.com/gomarkdown/markdown v0.0.0-20240626202925-2eda941fd024/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 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/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -60,25 +52,18 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= -golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= -golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 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/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= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= diff --git a/mux.go b/mux.go index 0a32bc23..b6c3b2ad 100644 --- a/mux.go +++ b/mux.go @@ -125,7 +125,7 @@ func Register[T, B any](s *Server, route Route[T, B], controller http.Handler, m route.Operation.Summary = route.NameFromNamespace(camelToHuman) route.Operation.Description = "controller: `" + route.FullName + "`\n\n---\n\n" - route.Operation.OperationID = route.Method + " " + s.basePath + route.Path + ":" + route.NameFromNamespace() + route.Operation.OperationID = route.Method + "_" + s.basePath + strings.ReplaceAll(strings.ReplaceAll(route.Path, "{", ":"), "}", "") return route } diff --git a/openapi.go b/openapi.go index 39fa922f..6d2174fd 100644 --- a/openapi.go +++ b/openapi.go @@ -11,6 +11,7 @@ import ( "path/filepath" "reflect" "regexp" + "strings" "github.com/getkin/kin-openapi/openapi3" "github.com/getkin/kin-openapi/openapi3gen" @@ -162,8 +163,8 @@ func RegisterOpenAPIOperation[T, B any](s *Server, method, path string) (*openap // Request body bodyTag := schemaTagFromType(s, *new(B)) - if (method == http.MethodPost || method == http.MethodPut || method == http.MethodPatch) && bodyTag.name != "unknown-interface" && bodyTag.name != "string" { - content := openapi3.NewContentWithSchemaRef(&bodyTag.SchemaRef, []string{"application/json"}) + if bodyTag.name != "unknown-interface" { + content := openapi3.NewContentWithSchemaRef(&bodyTag.SchemaRef, []string{"application/json", "application/xml"}) requestBody := openapi3.NewRequestBody(). WithRequired(true). WithDescription("Request body for " + reflect.TypeOf(*new(B)).String()). @@ -181,7 +182,7 @@ func RegisterOpenAPIOperation[T, B any](s *Server, method, path string) (*openap } responseSchema := schemaTagFromType(s, *new(T)) - content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json"}) + content := openapi3.NewContentWithSchemaRef(&responseSchema.SchemaRef, []string{"application/json", "application/xml"}) response := openapi3.NewResponse(). WithDescription("OK"). WithContent(content) @@ -192,6 +193,10 @@ func RegisterOpenAPIOperation[T, B any](s *Server, method, path string) (*openap for _, pathParam := range parsePathParams(path) { parameter := openapi3.NewPathParameter(pathParam) parameter.Schema = openapi3.NewStringSchema().NewRef() + if strings.HasSuffix(pathParam, "...") { + parameter.Description += " (might contain slashes)" + } + operation.AddParameter(parameter) } @@ -209,11 +214,12 @@ type schemaTag struct { func schemaTagFromType(s *Server, v any) schemaTag { if v == nil { // ensure we add unknown-interface to our schemas - s.getOrCreateSchema("unknown-interface", struct{}{}) + schema := s.getOrCreateSchema("unknown-interface", struct{}{}) return schemaTag{ name: "unknown-interface", SchemaRef: openapi3.SchemaRef{ - Ref: "#/components/schemas/unknown-interface", + Ref: "#/components/schemas/unknown-interface", + Value: schema, }, } } diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..0669a9f7 --- /dev/null +++ b/test.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +mods=$(go list -f '{{.Dir}}' -m) +for mod in $mods; do + go test -C "$mod" ./... +done