Repository: swaggest/openapi-go
Pull request #3 (2020-04-20 15:49:02)

Fix missing request parameter schema, remove unwanted nullable in res…
…ponses


Patch:
diff --git a/README.md b/README.md
index c339293..92084f1 100644
--- a/README.md
+++ b/README.md
@@ -177,8 +177,7 @@ fmt.Println(string(schema))
 //          "type": "array",
 //          "items": {
 //           "$ref": "#/components/schemas/Openapi3TestResp"
-//          },
-//          "nullable": true
+//          }
 //         }
 //        }
 //       }
diff --git a/openapi3/helper.go b/openapi3/helper.go
index 0ab5f50..61ef09d 100644
--- a/openapi3/helper.go
+++ b/openapi3/helper.go
@@ -1,7 +1,91 @@
 package openapi3
 
-import "strings"
+import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+)
 
+// ToParameterOrRef exposes Parameter in general form.
+func (p Parameter) ToParameterOrRef() ParameterOrRef {
+	return ParameterOrRef{
+		Parameter: &p,
+	}
+}
+
+// WithOperation sets Operation to PathItem.
+//
+// Deprecated: use Spec.AddOperation.
 func (p *PathItem) WithOperation(method string, operation Operation) *PathItem {
 	return p.WithMapOfOperationValuesItem(strings.ToLower(method), operation)
 }
+
+var regexFindPathParameter = regexp.MustCompile(`{([^}:]+)(:[^/]+)?(?:})`)
+
+// AddOperation validates and sets operation by path and method.
+func (s *Spec) AddOperation(method, path string, operation Operation) error {
+	method = strings.ToLower(method)
+	pathParametersSubmatches := regexFindPathParameter.FindAllStringSubmatch(path, -1)
+
+	switch method {
+	case "get", "put", "post", "delete", "options", "head", "patch", "trace":
+		break
+	default:
+		return fmt.Errorf("unexpected http method: %s", method)
+	}
+
+	pathItem := s.Paths.MapOfPathItemValues[path]
+	if _, found := pathItem.MapOfOperationValues[method]; found {
+		return errors.New("operation with method and path already exists")
+	}
+
+	pathParams := map[string]bool{}
+
+	if len(pathParametersSubmatches) > 0 {
+		for _, submatch := range pathParametersSubmatches {
+			pathParams[submatch[1]] = true
+
+			if submatch[2] != "" { // Remove gorilla.Mux-style regexp in path
+				path = strings.Replace(path, submatch[0], "{"+submatch[1]+"}", 1)
+			}
+		}
+	}
+
+	var errs []string
+
+	paramIndex := make(map[string]bool, len(operation.Parameters))
+	for _, p := range operation.Parameters {
+		if p.Parameter == nil {
+			continue
+		}
+
+		if found := paramIndex[p.Parameter.Name+string(p.Parameter.In)]; found {
+			errs = append(errs, "duplicate parameter in "+string(p.Parameter.In)+": "+p.Parameter.Name)
+			continue
+		}
+
+		if found := pathParams[p.Parameter.Name]; !found && p.Parameter.In == ParameterInPath {
+			errs = append(errs, "missing path parameter placeholder in url: "+p.Parameter.Name)
+			continue
+		}
+
+		paramIndex[p.Parameter.Name+string(p.Parameter.In)] = true
+	}
+
+	for pathParam := range pathParams {
+		if !paramIndex[pathParam+string(ParameterInPath)] {
+			errs = append(errs, "undefined path parameter: "+pathParam)
+		}
+	}
+
+	if len(errs) > 0 {
+		return errors.New(strings.Join(errs, ", "))
+	}
+
+	pathItem.WithMapOfOperationValuesItem(method, operation)
+
+	s.Paths.WithMapOfPathItemValuesItem(path, pathItem)
+
+	return nil
+}
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index f3bed6f..709ed54 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -147,7 +147,7 @@ func (r *Reflector) parseRequestBody(o *Operation, input interface{}, tag, mime
 }
 
 func (r *Reflector) parseParametersIn(o *Operation, input interface{}, in ParameterIn) error {
-	_, err := r.Reflect(input,
+	schema, err := r.Reflect(input,
 		jsonschema.DefinitionsPrefix("#/components/schemas/"),
 		jsonschema.PropertyNameTag(string(in)),
 		jsonschema.InterceptProperty(func(name string, field reflect.StructField, propertySchema *jsonschema.Schema) error {
@@ -196,6 +196,19 @@ func (r *Reflector) parseParametersIn(o *Operation, input interface{}, in Parame
 		return err
 	}
 
+	for name, def := range schema.Definitions {
+		s := SchemaOrRef{}
+
+		s.FromJSONSchema(def)
+
+		// Disable nullability of request bodies.
+		if s.Schema != nil && s.Schema.Nullable != nil {
+			s.Schema.Nullable = nil
+		}
+
+		r.SpecEns().ComponentsEns().SchemasEns().WithMapOfSchemaOrRefValuesItem(name, s)
+	}
+
 	return nil
 }
 
@@ -248,6 +261,10 @@ func (r *Reflector) SetJSONResponse(o *Operation, output interface{}, httpStatus
 	oaiSchema := SchemaOrRef{}
 	oaiSchema.FromJSONSchema(schema.ToSchemaOrBool())
 
+	if oaiSchema.Schema != nil {
+		oaiSchema.Schema.Nullable = nil
+	}
+
 	resp := Response{
 		Content: map[string]MediaType{
 			"application/json": {


Test patch:
diff --git a/openapi3/_testdata/openapi.json b/openapi3/_testdata/openapi.json
index c1b0e47..a5cccbf 100644
--- a/openapi3/_testdata/openapi.json
+++ b/openapi3/_testdata/openapi.json
@@ -26,8 +26,7 @@
       "description": "Query parameter.",
       "required": true,
       "schema": {
-       "type": "integer",
-       "description": "Query parameter."
+       "$ref": "#/components/schemas/Openapi3TestISOWeek"
       }
      },
      {
@@ -250,8 +249,7 @@
          "type": "array",
          "items": {
           "$ref": "#/components/schemas/Openapi3TestWeirdResp"
-         },
-         "nullable": true
+         }
         }
        }
       }
diff --git a/openapi3/_testdata/openapi_req_only.json b/openapi3/_testdata/openapi_req_only.json
new file mode 100644
index 0000000..18b65b7
--- /dev/null
+++ b/openapi3/_testdata/openapi_req_only.json
@@ -0,0 +1,80 @@
+{
+ "openapi": "3.0.2",
+ "info": {
+  "title": "SampleAPI",
+  "version": "1.2.3"
+ },
+ "paths": {
+  "/somewhere/{in_path}": {
+   "summary": "Path Summary",
+   "description": "Path Description",
+   "post": {
+    "parameters": [
+     {
+      "name": "in_query1",
+      "in": "query",
+      "description": "Query parameter.",
+      "required": true,
+      "schema": {
+       "type": "integer",
+       "description": "Query parameter."
+      }
+     },
+     {
+      "name": "in_query2",
+      "in": "query",
+      "description": "Query parameter.",
+      "required": true,
+      "schema": {
+       "$ref": "#/components/schemas/Openapi3TestISOWeek"
+      }
+     },
+     {
+      "name": "in_query3",
+      "in": "query",
+      "description": "Query parameter.",
+      "required": true,
+      "schema": {
+       "type": "integer",
+       "description": "Query parameter."
+      }
+     },
+     {
+      "name": "in_path",
+      "in": "path",
+      "required": true,
+      "schema": {
+       "type": "integer"
+      }
+     },
+     {
+      "name": "in_cookie",
+      "in": "cookie",
+      "deprecated": true,
+      "schema": {
+       "type": "string"
+      }
+     },
+     {
+      "name": "in_header",
+      "in": "header",
+      "schema": {
+       "type": "number"
+      }
+     }
+    ],
+    "responses": {}
+   }
+  }
+ },
+ "components": {
+  "schemas": {
+   "Openapi3TestISOWeek": {
+    "pattern": "^[0-9]{4}-W(0[1-9]|[1-4][0-9]|5[0-2])$",
+    "type": "string",
+    "description": "ISO Week",
+    "example": "2006-W43"
+   }
+  }
+ }
+}
\ No newline at end of file
diff --git a/openapi3/example_test.go b/openapi3/example_test.go
index 274a94e..97308ec 100644
--- a/openapi3/example_test.go
+++ b/openapi3/example_test.go
@@ -177,8 +177,7 @@ func ExampleReflector_SetJSONResponse() {
 	//          "type": "array",
 	//          "items": {
 	//           "$ref": "#/components/schemas/Openapi3TestResp"
-	//          },
-	//          "nullable": true
+	//          }
 	//         }
 	//        }
 	//       }
diff --git a/openapi3/helper_test.go b/openapi3/helper_test.go
new file mode 100644
index 0000000..3cba48c
--- /dev/null
+++ b/openapi3/helper_test.go
@@ -0,0 +1,42 @@
+package openapi3_test
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/swaggest/openapi-go/openapi3"
+)
+
+func TestSpec_SetOperation(t *testing.T) {
+	s := openapi3.Spec{}
+	op := openapi3.Operation{}
+
+	op.WithParameters(
+		openapi3.Parameter{In: openapi3.ParameterInPath, Name: "foo"}.ToParameterOrRef(),
+	)
+
+	assert.EqualError(t, s.AddOperation("bar", "/", op),
+		"unexpected http method: bar")
+
+	assert.EqualError(t, s.AddOperation(http.MethodGet, "/", op),
+		"missing path parameter placeholder in url: foo")
+
+	assert.EqualError(t, s.AddOperation(http.MethodGet, "/{bar}", op),
+		"missing path parameter placeholder in url: foo, undefined path parameter: bar")
+
+	assert.NoError(t, s.AddOperation(http.MethodGet, "/{foo}", op))
+
+	assert.EqualError(t, s.AddOperation(http.MethodGet, "/{foo}", op),
+		"operation with method and path already exists")
+
+	op.WithParameters(
+		openapi3.Parameter{In: openapi3.ParameterInPath, Name: "foo"}.ToParameterOrRef(),
+		openapi3.Parameter{In: openapi3.ParameterInPath, Name: "foo"}.ToParameterOrRef(),
+		openapi3.Parameter{In: openapi3.ParameterInQuery, Name: "bar"}.ToParameterOrRef(),
+		openapi3.Parameter{In: openapi3.ParameterInQuery, Name: "bar"}.ToParameterOrRef(),
+	)
+
+	assert.EqualError(t, s.AddOperation(http.MethodGet, "/another/{foo}", op),
+		"duplicate parameter in path: foo, duplicate parameter in query: bar")
+}
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 583c440..27ea846 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -93,14 +93,50 @@ type Req struct {
 
 type GetReq struct {
 	InQuery1 int     `query:"in_query1" required:"true" description:"Query parameter."`
-	InQuery2 int     `query:"in_query2" required:"true" description:"Query parameter."`
+	InQuery2 ISOWeek `query:"in_query2" required:"true" description:"Query parameter."`
 	InQuery3 int     `query:"in_query3" required:"true" description:"Query parameter."`
 	InPath   int     `path:"in_path"`
 	InCookie string  `cookie:"in_cookie" deprecated:"true"`
 	InHeader float64 `header:"in_header"`
 }
 
-func TestGenerator_SetResponse(t *testing.T) {
+func TestReflector_SetRequest(t *testing.T) {
+	reflector := openapi3.Reflector{}
+	reflector.DefaultOptions = append(reflector.DefaultOptions, jsonschema.InterceptType(swjschema.InterceptType))
+
+	s := reflector.SpecEns()
+	s.Info.Title = "SampleAPI"
+	s.Info.Version = "1.2.3"
+
+	op := openapi3.Operation{}
+
+	err := reflector.SetRequest(&op, new(GetReq), http.MethodGet)
+	assert.NoError(t, err)
+
+	pathItem := s.Paths.MapOfPathItemValues["/somewhere/{in_path}"]
+	pathItem.
+		WithSummary("Path Summary").
+		WithDescription("Path Description")
+
+	pathItem.WithOperation(http.MethodPost, op)
+
+	s.Paths.WithMapOfPathItemValuesItem(
+		"/somewhere/{in_path}",
+		pathItem,
+	)
+
+	b, err := json.MarshalIndent(s, "", " ")
+	assert.NoError(t, err)
+
+	require.NoError(t, ioutil.WriteFile("_testdata/openapi_req_only_last_run.json", b, 0640))
+
+	expected, err := ioutil.ReadFile("_testdata/openapi_req_only.json")
+	require.NoError(t, err)
+
+	assertjson.Equal(t, expected, b)
+}
+
+func TestReflector_SetJSONResponse(t *testing.T) {
 	reflector := openapi3.Reflector{}
 	reflector.DefaultOptions = append(reflector.DefaultOptions, jsonschema.InterceptType(swjschema.InterceptType))