diff --git a/internal/apischema/apischema.go b/internal/apischema/apischema.go index 3b387f0..4d5d13c 100644 --- a/internal/apischema/apischema.go +++ b/internal/apischema/apischema.go @@ -7,9 +7,15 @@ type API struct { type Operation struct { Method string Path string + Body map[string]FieldType Responses []Response } +type FieldType struct { + Required bool + Type string +} + type Response struct { StatusCode int MediaType string diff --git a/internal/apischema/find.go b/internal/apischema/find.go index 9bb5d37..c301cc8 100644 --- a/internal/apischema/find.go +++ b/internal/apischema/find.go @@ -1,6 +1,10 @@ package apischema import ( + "encoding/json" + "errors" + "io" + "net/http" "strings" ) @@ -16,9 +20,12 @@ func (e *FindResponseError) Error() string { type FindResponseParams struct { Path string Method string + Body io.ReadCloser MediaType string } +var ErrEmptyRequireField = errors.New("empty require field") + func (a API) FindResponse(params FindResponseParams) (Response, error) { operation, ok := a.findOperation(params) if !ok { @@ -28,6 +35,23 @@ func (a API) FindResponse(params FindResponseParams) (Response, error) { } } + switch params.Method { + case http.MethodPost, http.MethodPut, http.MethodPatch: + var body map[string]interface{} + + err := json.NewDecoder(params.Body).Decode(&body) + if err != nil { + return Response{}, err + } + + for k, v := range operation.Body { + _, ok := body[k] + if !ok && v.Required { + return Response{}, ErrEmptyRequireField + } + } + } + response, ok := operation.findResponse(params) if !ok { return operation.Responses[0], nil diff --git a/internal/openapi3/parse.go b/internal/openapi3/parse.go index 044990d..22576b0 100644 --- a/internal/openapi3/parse.go +++ b/internal/openapi3/parse.go @@ -73,7 +73,7 @@ func (e *SchemaTypeError) Error() string { return "unknown type " + e.schemaType } -var EmptyItemsErr = errors.New("empty items in array") +var ErrEmptyItems = errors.New("empty items in array") type builder struct { openapi OpenAPI @@ -126,6 +126,36 @@ func (b *builder) Set(path, method string, o *Operation) (apischema.Operation, e operation := apischema.Operation{ Method: method, Path: path, + Body: make(map[string]apischema.FieldType), + } + + body, ok := o.RequestBody.Content["application/json"] + if ok { + var s Schema + + if body.Schema.Reference != "" { + schema, err := b.openapi.LookupByReference(body.Schema.Reference) + if err != nil { + return apischema.Operation{}, fmt.Errorf("resolve reference: %w", err) + } + + s = schema + } else { + s = body.Schema + } + + for _, v := range s.Required { + operation.Body[v] = apischema.FieldType{ + Required: true, + } + } + + for k, v := range s.Properties { + operation.Body[k] = apischema.FieldType{ + Required: operation.Body[k].Required, + Type: v.Type, + } + } } for code, resp := range o.Responses { @@ -201,7 +231,7 @@ func (b *builder) convertSchema(s Schema) (apischema.Schema, error) { return apischema.StringSchema{Example: val}, nil case "array": if nil == s.Items { - return nil, EmptyItemsErr + return nil, ErrEmptyItems } itemsSchema, err := b.convertSchema(*s.Items) diff --git a/internal/openapi3/parse_test.go b/internal/openapi3/parse_test.go index b59a39b..f144f0a 100644 --- a/internal/openapi3/parse_test.go +++ b/internal/openapi3/parse_test.go @@ -16,6 +16,20 @@ func TestParse_YAML(t *testing.T) { { Method: "POST", Path: "/users", + Body: map[string]apischema.FieldType{ + "id": { + Required: true, + Type: "string", + }, + "firstName": { + Required: true, + Type: "string", + }, + "lastName": { + Required: true, + Type: "string", + }, + }, Responses: []apischema.Response{ { StatusCode: 201, @@ -35,6 +49,7 @@ func TestParse_YAML(t *testing.T) { { Method: "GET", Path: "/users", + Body: map[string]apischema.FieldType{}, Responses: []apischema.Response{ { StatusCode: 200, @@ -69,6 +84,7 @@ func TestParse_YAML(t *testing.T) { { Method: "GET", Path: "/users/{userId}", + Body: map[string]apischema.FieldType{}, Responses: []apischema.Response{ { StatusCode: 200, diff --git a/internal/openapi3/schema.go b/internal/openapi3/schema.go index 979fd52..9fab922 100644 --- a/internal/openapi3/schema.go +++ b/internal/openapi3/schema.go @@ -9,6 +9,7 @@ type Schema struct { Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` Example interface{} `json:"example,omitempty" yaml:"example,omitempty"` Faker string `json:"x-faker,omitempty" yaml:"x-faker,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` Items *Schema `json:"items,omitempty" yaml:"items,omitempty"` diff --git a/internal/server/handler.go b/internal/server/handler.go index 242a6ff..8009c61 100644 --- a/internal/server/handler.go +++ b/internal/server/handler.go @@ -2,6 +2,8 @@ package server import ( "encoding/json" + "errors" + "io" "net/http" "strings" @@ -33,8 +35,14 @@ func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { path := RemoveFragment(r.URL.Path) - response, ok := s.Handlers.Get(path, r.Method) + response, ok, err := s.Handlers.Get(path, r.Method, r.Body) if ok { + if _, ok := err.(*json.SyntaxError); ok || errors.Is(err, apischema.ErrEmptyRequireField) { + w.WriteHeader(http.StatusBadRequest) + + return + } + w.WriteHeader(response.StatusCode) resp := response.ExampleValue(r.Header.Get("X-Example")) @@ -59,16 +67,25 @@ func (s *Server) Handler(w http.ResponseWriter, r *http.Request) { } // Get -. -func (h Handlers) Get(path, method string) (apischema.Response, bool) { +func (h Handlers) Get(path, method string, body io.ReadCloser) (apischema.Response, bool, error) { response, err := h.API.FindResponse(apischema.FindResponseParams{ Path: path, Method: method, + Body: body, }) if err != nil { - return apischema.Response{}, false + if errors.Is(err, apischema.ErrEmptyRequireField) { + return apischema.Response{}, true, err + } + + if _, ok := err.(*json.SyntaxError); ok { + return apischema.Response{}, true, err + } + + return apischema.Response{}, false, err } - return response, true + return response, true, nil } func setStatusCode(w http.ResponseWriter, statusCode string) bool { diff --git a/test/testdata/badrequest/openapi3.yml b/test/testdata/badrequest/openapi3.yml new file mode 100644 index 0000000..de78bdb --- /dev/null +++ b/test/testdata/badrequest/openapi3.yml @@ -0,0 +1,43 @@ +openapi: 3.0.3 + +info: + title: Users dummy API + version: 0.1.0 + +paths: + /users: + post: + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/User" + responses: + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/User' + example: + id: e1afccea-5168-4735-84d4-cb96f6fb5d25 + firstName: Elon + lastName: Musk + +components: + schemas: + User: + type: object + required: + - id + - firstName + - lastName + properties: + id: + type: string + format: uuid + firstName: + type: string + lastName: + type: string diff --git a/test/testdata/badrequest/pact.json b/test/testdata/badrequest/pact.json new file mode 100644 index 0000000..aa7549c --- /dev/null +++ b/test/testdata/badrequest/pact.json @@ -0,0 +1,23 @@ +{ + "consumer": { + "name": "consumer" + }, + "provider": { + "name": "dummy" + }, + "interactions": [ + { + "description": "", + "request": { + "method": "POST", + "path": "/users", + "body": { + "id": "e1afccea-5168-4735-84d4-cb96f6fb5d25" + } + }, + "response": { + "status": 400 + } + } + ] +}