Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add description to request and response and version to the API. #27

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add description to request and response and version to the API.
rmsj committed Sep 4, 2024
commit f411ff1c3e67258961e27d9f52287adcf1a40392
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -21,10 +21,10 @@ See the [./examples](./examples) directory for complete examples.

```go
// Configure the models.
api := rest.NewAPI("messages")
api := rest.NewAPI("messages", "1.0.0")
api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"}

api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
status := s.Properties["statusCode"]
status.Value.WithMin(100).WithMax(600)
})
@@ -57,11 +57,11 @@ router := http.NewServeMux()
router.Handle("/topics", &get.Handler{})
router.Handle("/topic", &post.Handler{})

api := rest.NewAPI("messages")
api := rest.NewAPI("messages", "1.0.0")
api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"}

// Register the error type with customisations.
api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
status := s.Properties["statusCode"]
status.Value.WithMin(100).WithMax(600)
})
51 changes: 34 additions & 17 deletions api.go
Original file line number Diff line number Diff line change
@@ -20,9 +20,10 @@ func WithApplyCustomSchemaToType(f func(t reflect.Type, s *openapi3.Schema)) API
}

// NewAPI creates a new API from the router.
func NewAPI(name string, opts ...APIOpts) *API {
func NewAPI(name string, version string, opts ...APIOpts) *API {
api := &API{
Name: name,
Version: version,
KnownTypes: defaultKnownTypes,
Routes: make(map[Pattern]MethodToRoute),
// map of model name to schema.
@@ -121,6 +122,8 @@ type Pattern string
type API struct {
// Name of the API.
Name string
// Version of the API.
Version string
// Routes of the API.
// From patterns, to methods, to route.
Routes map[Pattern]MethodToRoute
@@ -161,7 +164,7 @@ func (api *API) Merge(r Route) {
toUpdate := api.Route(string(r.Method), string(r.Pattern))
mergeMap(toUpdate.Params.Path, r.Params.Path)
mergeMap(toUpdate.Params.Query, r.Params.Query)
if toUpdate.Models.Request.Type == nil {
if toUpdate.Models.Request.Content == nil {
toUpdate.Models.Request = r.Models.Request
}
mergeMap(toUpdate.Models.Responses, r.Models.Responses)
@@ -198,7 +201,7 @@ func (api *API) Route(method, pattern string) (r *Route) {
Method: Method(method),
Pattern: Pattern(pattern),
Models: Models{
Responses: make(map[int]Model),
Responses: make(map[int]Response),
},
Params: Params{
Path: make(map[string]PathParam),
@@ -255,21 +258,25 @@ func (api *API) Trace(pattern string) (r *Route) {
return api.Route(http.MethodTrace, pattern)
}

// HasResponseModel configures a response for the route.
// HasResponse configures a response for the route.
// Example:
//
// api.Get("/user").HasResponseModel(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasResponseModel(status int, response Model) *Route {
rm.Models.Responses[status] = response
// api.Get("/user").HasResponse(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasResponse(status int, resp *Model, desc string) *Route {
rm.Models.Responses[status] = Response{
Description: desc,
Content: resp,
}
return rm
}

// HasResponseModel configures the request model of the route.
// Example:
//
// api.Post("/user").HasRequestModel(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasRequestModel(request Model) *Route {
rm.Models.Request = request
// HasRequest configures the request model of the route.
// Example: api.Post("/user").HasRequest(http.StatusOK, rest.ModelOf[User]())
func (rm *Route) HasRequest(request *Model, desc string) *Route {
rm.Models.Request = Request{
Description: desc,
Content: request,
}
return rm
}

@@ -303,22 +310,32 @@ func (rm *Route) HasDescription(description string) *Route {
return rm
}

type Request struct {
Description string
Content *Model
}

type Response struct {
Description string
Content *Model
}

// Models defines the models used by a route.
type Models struct {
Request Model
Responses map[int]Model
Request Request
Responses map[int]Response
}

// ModelOf creates a model of type T.
func ModelOf[T any]() Model {
func ModelOf[T any]() *Model {
var t T
m := Model{
Type: reflect.TypeOf(t),
}
if sm, ok := any(t).(CustomSchemaApplier); ok {
m.s = sm.ApplyCustomSchema
}
return m
return &m
}

func modelFromType(t reflect.Type) Model {
7 changes: 4 additions & 3 deletions chiadapter/route_test.go
Original file line number Diff line number Diff line change
@@ -4,10 +4,11 @@ import (
"net/http"
"testing"

"github.com/a-h/rest"
"github.com/a-h/rest/chiadapter"
"github.com/go-chi/chi/v5"
"github.com/google/go-cmp/cmp"

"github.com/a-h/rest"
"github.com/a-h/rest/chiadapter"
)

func TestMerge(t *testing.T) {
@@ -16,7 +17,7 @@ func TestMerge(t *testing.T) {
router := chi.NewRouter()
router.Method(http.MethodGet, pattern,
http.RedirectHandler("/elsewhere", http.StatusMovedPermanently))
api := rest.NewAPI("test")
api := rest.NewAPI("test", "1.0.0")

// Act.
err := chiadapter.Merge(api, router)
4 changes: 2 additions & 2 deletions examples/chiexample/go.mod
Original file line number Diff line number Diff line change
@@ -16,12 +16,12 @@ require (
require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/invopop/yaml v0.3.1 // 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.20.0 // indirect
2 changes: 2 additions & 0 deletions examples/chiexample/go.sum
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -38,6 +39,7 @@ 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
31 changes: 19 additions & 12 deletions examples/chiexample/main.go
Original file line number Diff line number Diff line change
@@ -6,12 +6,13 @@ import (
"net/http"

"github.com/a-h/respond"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"

"github.com/a-h/rest"
"github.com/a-h/rest/chiadapter"
"github.com/a-h/rest/examples/chiexample/models"
"github.com/a-h/rest/swaggerui"
"github.com/getkin/kin-openapi/openapi3"
"github.com/go-chi/chi/v5"
)

func main() {
@@ -51,35 +52,41 @@ func main() {
})

// Create the API definition.
api := rest.NewAPI("Messaging API")
api := rest.NewAPI("Messaging API", "1.0.0")

// Create the routes and parameters of the Router in the REST API definition with an
// adapter, or do it manually.
chiadapter.Merge(api, router)
err := chiadapter.Merge(api, router)
if err != nil {
log.Fatalf("failed to create routes: %v", err)
}

// Because this example is all in the main package, we can strip the `main_` namespace from
// the types.
api.StripPkgPaths = []string{"main", "github.com/a-h"}

// It's possible to customise the OpenAPI schema for each type.
api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
_, _, err = api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
status := s.Properties["statusCode"]
status.Value.WithMin(100).WithMax(600)
})
if err != nil {
log.Fatalf("failed to register model: %v", err)
}

// Document the routes.
api.Get("/topic/{id}").
HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]())
HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response")

api.Get("/topics").
HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsGetResponse]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]())
HasResponse(http.StatusOK, rest.ModelOf[models.TopicsGetResponse](), "topic response").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response")

api.Post("/topics").
HasRequestModel(rest.ModelOf[models.TopicsPostRequest]()).
HasResponseModel(http.StatusOK, rest.ModelOf[models.TopicsPostResponse]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]())
HasRequest(rest.ModelOf[models.TopicsPostRequest](), "topic request").
HasResponse(http.StatusOK, rest.ModelOf[models.TopicsPostResponse](), "topic response").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "error response")

// Create the spec.
spec, err := api.Spec()
4 changes: 2 additions & 2 deletions examples/offline/go.mod
Original file line number Diff line number Diff line change
@@ -15,12 +15,12 @@ require (
require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/invopop/yaml v0.3.1 // 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.20.0 // indirect
2 changes: 2 additions & 0 deletions examples/offline/go.sum
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -36,6 +37,7 @@ 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
11 changes: 6 additions & 5 deletions examples/offline/main.go
Original file line number Diff line number Diff line change
@@ -7,17 +7,18 @@ import (
"os"

"github.com/a-h/respond"
"github.com/getkin/kin-openapi/openapi3"

"github.com/a-h/rest"
"github.com/a-h/rest/examples/offline/models"
"github.com/getkin/kin-openapi/openapi3"
)

func main() {
// Configure the models.
api := rest.NewAPI("messages")
api := rest.NewAPI("messages", "1.0.0")
api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"}

api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
status := s.Properties["statusCode"]
status.Value.WithMin(100).WithMax(600)
})
@@ -27,8 +28,8 @@ func main() {
Description: "id of the topic",
Regexp: `\d+`,
}).
HasResponseModel(http.StatusOK, rest.ModelOf[models.Topic]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]()).
HasResponse(http.StatusOK, rest.ModelOf[models.Topic](), "").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "").
HasTags([]string{"Topic"}).
HasDescription("Get one topic by id").
HasOperationID("getOneTopic")
4 changes: 2 additions & 2 deletions examples/stdlib/go.mod
Original file line number Diff line number Diff line change
@@ -15,12 +15,12 @@ require (
require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/invopop/yaml v0.3.1 // 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.20.0 // indirect
2 changes: 2 additions & 0 deletions examples/stdlib/go.sum
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -36,6 +37,7 @@ 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/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
17 changes: 9 additions & 8 deletions examples/stdlib/main.go
Original file line number Diff line number Diff line change
@@ -6,11 +6,12 @@ import (
"net/http"

"github.com/a-h/respond"
"github.com/getkin/kin-openapi/openapi3"

"github.com/a-h/rest"
"github.com/a-h/rest/examples/stdlib/handlers/topic/post"
"github.com/a-h/rest/examples/stdlib/handlers/topics/get"
"github.com/a-h/rest/swaggerui"
"github.com/getkin/kin-openapi/openapi3"
)

func main() {
@@ -19,25 +20,25 @@ func main() {
router.Handle("/topics", &get.Handler{})
router.Handle("/topic", &post.Handler{})

api := rest.NewAPI("messages")
api := rest.NewAPI("messages", "1.0.0")
api.StripPkgPaths = []string{"github.com/a-h/rest/example", "github.com/a-h/respond"}

// It's possible to customise the OpenAPI schema for each type.
// You can use helper functions, or write your own function that works
// directly on the openapi3.Schema type.
api.RegisterModel(rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
api.RegisterModel(*rest.ModelOf[respond.Error](), rest.WithDescription("Standard JSON error"), func(s *openapi3.Schema) {
status := s.Properties["statusCode"]
status.Value.WithMin(100).WithMax(600)
})

api.Get("/topics").
HasResponseModel(http.StatusOK, rest.ModelOf[get.TopicsGetResponse]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]())
HasResponse(http.StatusOK, rest.ModelOf[get.TopicsGetResponse](), "").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "")

api.Post("/topic").
HasRequestModel(rest.ModelOf[post.TopicPostRequest]()).
HasResponseModel(http.StatusOK, rest.ModelOf[post.TopicPostResponse]()).
HasResponseModel(http.StatusInternalServerError, rest.ModelOf[respond.Error]())
HasRequest(rest.ModelOf[post.TopicPostRequest](), "").
HasResponse(http.StatusOK, rest.ModelOf[post.TopicPostResponse](), "").
HasResponse(http.StatusInternalServerError, rest.ModelOf[respond.Error](), "")

// Create the spec.
spec, err := api.Spec()
40 changes: 24 additions & 16 deletions schema.go
Original file line number Diff line number Diff line change
@@ -7,10 +7,11 @@ import (
"sort"
"strings"

"github.com/a-h/rest/enums"
"github.com/a-h/rest/getcomments/parser"
"github.com/getkin/kin-openapi/openapi3"
"golang.org/x/exp/constraints"

"github.com/a-h/rest/enums"
"github.com/a-h/rest/getcomments/parser"
)

func newSpec(name string) *openapi3.T {
@@ -61,6 +62,7 @@ func newPrimitiveSchema(paramType PrimitiveType) *openapi3.Schema {

func (api *API) createOpenAPI() (spec *openapi3.T, err error) {
spec = newSpec(api.Name)
spec.Info.Version = api.Version
// Add all the routes.
for pattern, methodToRoute := range api.Routes {
path := &openapi3.PathItem{}
@@ -106,33 +108,39 @@ func (api *API) createOpenAPI() (spec *openapi3.T, err error) {
}

// Handle request types.
if route.Models.Request.Type != nil {
name, schema, err := api.RegisterModel(route.Models.Request)
if route.Models.Request.Content != nil {
name, schema, err := api.RegisterModel(*route.Models.Request.Content)
if err != nil {
return spec, err
}
op.RequestBody = &openapi3.RequestBodyRef{
Value: openapi3.NewRequestBody().WithContent(map[string]*openapi3.MediaType{
"application/json": {
Schema: getSchemaReferenceOrValue(name, schema),
},
}),
Value: openapi3.NewRequestBody().
WithContent(map[string]*openapi3.MediaType{
"application/json": {
Schema: getSchemaReferenceOrValue(name, schema),
},
}).
WithDescription(route.Models.Request.Description),
}
}

// Handle response types.
for status, model := range route.Models.Responses {
name, schema, err := api.RegisterModel(model)
if err != nil {
return spec, err
}
for status, response := range route.Models.Responses {
resp := openapi3.NewResponse().
WithDescription("").
WithContent(map[string]*openapi3.MediaType{
WithDescription(response.Description)

if response.Content != nil {
name, schema, err := api.RegisterModel(*response.Content)
if err != nil {
return spec, err
}
resp.WithContent(map[string]*openapi3.MediaType{
"application/json": {
Schema: getSchemaReferenceOrValue(name, schema),
},
})
}

op.AddResponse(status, resp)
}

85 changes: 42 additions & 43 deletions schema_test.go
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ package rest

import (
"embed"
_ "embed"
"encoding/json"
"fmt"
"net/http"
@@ -10,8 +11,6 @@ import (
"testing"
"time"

_ "embed"

"github.com/getkin/kin-openapi/openapi3"
"github.com/google/go-cmp/cmp"
"gopkg.in/yaml.v2"
@@ -193,8 +192,8 @@ func TestSchema(t *testing.T) {
name: "test001.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[TestRequestType]()).
HasResponseModel(http.StatusOK, ModelOf[TestResponseType]()).
HasRequest(ModelOf[TestRequestType](), "").
HasResponse(http.StatusOK, ModelOf[TestResponseType](), "").
HasDescription("Test request type description").
HasTags([]string{"TestRequest"})
return nil
@@ -204,8 +203,8 @@ func TestSchema(t *testing.T) {
name: "basic-data-types.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[AllBasicDataTypes]()).
HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypes]()).
HasRequest(ModelOf[AllBasicDataTypes](), "").
HasResponse(http.StatusOK, ModelOf[AllBasicDataTypes](), "").
HasOperationID("postAllBasicDataTypes").
HasTags([]string{"BasicData"}).
HasDescription("Post all basic data types description")
@@ -216,98 +215,98 @@ func TestSchema(t *testing.T) {
name: "basic-data-types-pointers.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[AllBasicDataTypesPointers]()).
HasResponseModel(http.StatusOK, ModelOf[AllBasicDataTypesPointers]())
HasRequest(ModelOf[AllBasicDataTypesPointers](), "").
HasResponse(http.StatusOK, ModelOf[AllBasicDataTypesPointers](), "")
return nil
},
},
{
name: "omit-empty-fields.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[OmitEmptyFields]()).
HasResponseModel(http.StatusOK, ModelOf[OmitEmptyFields]())
HasRequest(ModelOf[OmitEmptyFields](), "").
HasResponse(http.StatusOK, ModelOf[OmitEmptyFields](), "")
return nil
},
},
{
name: "anonymous-type.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[struct{ A string }]()).
HasResponseModel(http.StatusOK, ModelOf[struct{ B string }]())
HasRequest(ModelOf[struct{ A string }](), "").
HasResponse(http.StatusOK, ModelOf[struct{ B string }](), "")
return nil
},
},
{
name: "embedded-structs.yaml",
setup: func(api *API) error {
api.Get("/embedded").
HasResponseModel(http.StatusOK, ModelOf[EmbeddedStructA]())
HasResponse(http.StatusOK, ModelOf[EmbeddedStructA](), "")
api.Post("/test").
HasRequestModel(ModelOf[WithEmbeddedStructs]()).
HasResponseModel(http.StatusOK, ModelOf[WithEmbeddedStructs]())
HasRequest(ModelOf[WithEmbeddedStructs](), "").
HasResponse(http.StatusOK, ModelOf[WithEmbeddedStructs](), "")
return nil
},
},
{
name: "with-name-struct-tags.yaml",
setup: func(api *API) error {
api.Post("/test").
HasRequestModel(ModelOf[WithNameStructTags]()).
HasResponseModel(http.StatusOK, ModelOf[WithNameStructTags]())
HasRequest(ModelOf[WithNameStructTags](), "").
HasResponse(http.StatusOK, ModelOf[WithNameStructTags](), "")
return nil
},
},
{
name: "known-types.yaml",
setup: func(api *API) error {
api.Route(http.MethodGet, "/test").
HasResponseModel(http.StatusOK, ModelOf[KnownTypes]())
HasResponse(http.StatusOK, ModelOf[KnownTypes](), "")
return nil
},
},
{
name: "all-methods.yaml",
setup: func(api *API) (err error) {
api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Head("/head").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Post("/post").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Put("/put").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Patch("/patch").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Delete("/delete").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Connect("/connect").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Options("/options").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Trace("/trace").HasResponseModel(http.StatusOK, ModelOf[OK]())
api.Get("/get").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Head("/head").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Post("/post").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Put("/put").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Patch("/patch").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Delete("/delete").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Connect("/connect").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Options("/options").HasResponse(http.StatusOK, ModelOf[OK](), "")
api.Trace("/trace").HasResponse(http.StatusOK, ModelOf[OK](), "")
return
},
},
{
name: "enums.yaml",
setup: func(api *API) (err error) {
// Register the enums and values.
api.RegisterModel(ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC))
api.RegisterModel(ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3))
api.RegisterModel(*ModelOf[StringEnum](), WithEnumValues(StringEnumA, StringEnumB, StringEnumC))
api.RegisterModel(*ModelOf[IntEnum](), WithEnumValues(IntEnum1, IntEnum2, IntEnum3))

api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]())
api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "")
return
},
},
{
name: "enum-constants.yaml",
setup: func(api *API) (err error) {
// Register the enums and values.
api.RegisterModel(ModelOf[StringEnum](), WithEnumConstants[StringEnum]())
api.RegisterModel(ModelOf[IntEnum](), WithEnumConstants[IntEnum]())
api.RegisterModel(*ModelOf[StringEnum](), WithEnumConstants[StringEnum]())
api.RegisterModel(*ModelOf[IntEnum](), WithEnumConstants[IntEnum]())

api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithEnums]())
api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithEnums](), "")
return
},
},
{
name: "with-maps.yaml",
setup: func(api *API) (err error) {
api.Get("/get").HasResponseModel(http.StatusOK, ModelOf[WithMaps]())
api.Get("/get").HasResponse(http.StatusOK, ModelOf[WithMaps](), "")
return
},
},
@@ -322,7 +321,7 @@ func TestSchema(t *testing.T) {
HasPathParameter("userId", PathParam{
Description: "User ID",
}).
HasResponseModel(http.StatusOK, ModelOf[User]())
HasResponse(http.StatusOK, ModelOf[User](), "")
return
},
},
@@ -339,7 +338,7 @@ func TestSchema(t *testing.T) {
HasPathParameter("userId", PathParam{
Description: "User ID",
}).
HasResponseModel(http.StatusOK, ModelOf[User]())
HasResponse(http.StatusOK, ModelOf[User](), "")
return
},
},
@@ -358,7 +357,7 @@ func TestSchema(t *testing.T) {
Type: PrimitiveTypeString,
Regexp: `field|otherField`,
}).
HasResponseModel(http.StatusOK, ModelOf[User]())
HasResponse(http.StatusOK, ModelOf[User](), "")
return
},
},
@@ -381,25 +380,25 @@ func TestSchema(t *testing.T) {
s.Description = "The field to order the results by"
},
}).
HasResponseModel(http.StatusOK, ModelOf[User]())
HasResponse(http.StatusOK, ModelOf[User](), "")
return
},
},
{
name: "multiple-dates-with-comments.yaml",
setup: func(api *API) (err error) {
api.Get("/dates").
HasResponseModel(http.StatusOK, ModelOf[MultipleDateFieldsWithComments]())
HasResponse(http.StatusOK, ModelOf[MultipleDateFieldsWithComments](), "")
return
},
},
{
name: "custom-models.yaml",
setup: func(api *API) (err error) {
api.Get("/struct-with-customisation").
HasResponseModel(http.StatusOK, ModelOf[StructWithCustomisation]())
HasResponse(http.StatusOK, ModelOf[StructWithCustomisation](), "")
api.Get("/struct-ptr-with-customisation").
HasResponseModel(http.StatusOK, ModelOf[*StructWithCustomisation]())
HasResponse(http.StatusOK, ModelOf[*StructWithCustomisation](), "")
return
},
},
@@ -437,7 +436,7 @@ func TestSchema(t *testing.T) {
},
setup: func(api *API) error {
api.Get("/").
HasResponseModel(http.StatusOK, ModelOf[StructWithTags]())
HasResponse(http.StatusOK, ModelOf[StructWithTags](), "")
return nil
},
},
@@ -471,7 +470,7 @@ func TestSchema(t *testing.T) {
go func() {
defer wg.Done()
// Create the API.
api := NewAPI(test.name, test.opts...)
api := NewAPI(test.name, "1.0.0", test.opts...)
api.StripPkgPaths = []string{"github.com/a-h/rest"}
// Configure it.
test.setup(api)