Repository: swaggest/openapi-go
Pull request #56 (2023-01-18 10:26:56)

Add support for `form` field tags
Related to https://github.com/swaggest/rest/issues/108.

github-actions[bot]: ### Lines Of Code

| Language  | Files  |   Lines    |    Code    | Comments |  Blanks  | Complexity |     Bytes     |
|-----------|--------|------------|------------|----------|----------|------------|---------------|
| Go        | 7      | 1299 (+5)  | 972 (+4)   | 64       | 263 (+1) | 301 (+2)   | 29.5K (+195B) |
| Go (test) | 7      | 1799 (+51) | 1326 (+46) | 245 (+1) | 228 (+4) | 25         | 46.7K (+1.3K) |
| Markdown  | 4 (+3) | 228 (+42)  | 199 (+32)  | 0        | 29 (+10) | 0          | 6.4K (+975B)  |
| YAML      | 5      | 361 (-4)   | 296 (-4)   | 29       | 36       | 0          | 11.6K (+171B) |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# summary
Inferred base version: v0.2.26
Suggested version: v0.2.27
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:								(statements)				49.6%
changed lines: (statements) 100.0%
<details><summary>Coverage of changed lines</summary>

|          File           |     Function      | Coverage |
|-------------------------|-------------------|----------|
| Total                   |                   | 100.0%   |
| openapi3/reflect.go     |                   | 100.0%   |
| openapi3/reflect.go:92  | SetupRequest      | 100.0%   |
| openapi3/reflect.go:136 | parseRequestBody  | 100.0%   |
| openapi3/reflect.go:243 | parseParametersIn | 100.0%   |
| openapi3/reflect.go:468 | SetupResponse     | 100.0%   |

</details>

<details><summary>Coverage diff with base branch</summary>

|                        File                        |   Function    | Base Coverage | Current Coverage |
|----------------------------------------------------|---------------|---------------|------------------|
| Total                                              |               | 49.6%         | 49.6% (0.0%)     |
| github.com/swaggest/openapi-go/openapi3/reflect.go | SetupResponse | 88.9%         | 90.0% (+1.1%)    |

</details>
<!-- Sticky Pull Request Commentunit-test -->

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b3c2870
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,21 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+If feasible/relevant, please provide a code snippet (inline or with Go playground) to reproduce the issue.
+
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..ff47170
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,10 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+Please use discussions https://github.com/swaggest/openapi-go/discussions/categories/ideas to share feature ideas.
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
new file mode 100644
index 0000000..2057da9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -0,0 +1,10 @@
+---
+name: Question
+about: Any question about features or usage
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+Please use discussions https://github.com/swaggest/openapi-go/discussions/categories/q-a to make your question more discoverable by other folks.
diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml
index 619ca74..927e099 100644
--- a/.github/workflows/cloc.yml
+++ b/.github/workflows/cloc.yml
@@ -21,7 +21,7 @@ jobs:
         with:
           ref: ${{ github.event.pull_request.base.sha }}
           path: base
-      - name: Count Lines Of Code
+      - name: Count lines of code
         id: loc
         run: |
           curl -sLO https://github.com/vearutop/sccdiff/releases/download/v1.0.3/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz
@@ -29,10 +29,9 @@ jobs:
           [ "$sccdiff_hash" == "ae8a07b687bd3dba60861584efe724351aa7ff63" ] || (echo "::error::unexpected hash for sccdiff, possible tampering: $sccdiff_hash" && exit 1)
           OUTPUT=$(cd pr && ../sccdiff -basedir ../base)
           echo "${OUTPUT}"
-          OUTPUT="${OUTPUT//$'\n'/%0A}"
-          echo "::set-output name=diff::$OUTPUT"
+          echo "diff<<EOF" >> $GITHUB_OUTPUT && echo "$OUTPUT" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
 
-      - name: Comment Code Lines
+      - name: Comment lines of code
         continue-on-error: true
         uses: marocchino/sticky-pull-request-comment@v2
         with:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index 76ccec1..fbf8167 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -24,10 +24,10 @@ jobs:
           go-version: 1.19.x
       - uses: actions/checkout@v2
       - name: golangci-lint
-        uses: golangci/golangci-lint-action@v3.2.0
+        uses: golangci/golangci-lint-action@v3.3.1
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
-          version: v1.50.0
+          version: v1.50.1
 
           # Optional: working directory, useful for monorepos
           # working-directory: somedir
@@ -45,4 +45,4 @@ jobs:
           # skip-pkg-cache: true
 
           # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
-          # skip-build-cache: true
+          # skip-build-cache: true
\ No newline at end of file
diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml
index 6267500..5d9fae0 100644
--- a/.github/workflows/gorelease.yml
+++ b/.github/workflows/gorelease.yml
@@ -42,9 +42,8 @@ jobs:
           test -e ~/go/bin/gorelease || go install golang.org/x/exp/cmd/gorelease@latest
           OUTPUT=$(gorelease 2>&1 || exit 0)
           echo "${OUTPUT}"
-          OUTPUT="${OUTPUT//$'\n'/%0A}"
-          echo "::set-output name=report::$OUTPUT"
-      - name: Comment Report
+          echo "report<<EOF" >> $GITHUB_OUTPUT && echo "$OUTPUT" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
+      - name: Comment report
         continue-on-error: true
         uses: marocchino/sticky-pull-request-comment@v2
         with:
diff --git a/Makefile b/Makefile
index b98c23e..aaa3592 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-#GOLANGCI_LINT_VERSION := "v1.50.0" # Optional configuration to pinpoint golangci-lint version.
+#GOLANGCI_LINT_VERSION := "v1.50.1" # Optional configuration to pinpoint golangci-lint version.
 
 # The head of Makefile determines location of dev-go to include standard targets.
 GO ?= go
diff --git a/README.md b/README.md
index 70eddc6..4eba99d 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ For automated HTTP REST service framework built with this library please check [
     * `json` for request bodies and responses in JSON
     * `query`, `path` for parameters in URL
     * `header`, `cookie`, `formData`, `file` for other parameters
+    * `form` acts as `query` and `formData`
     * [field tags](https://github.com/swaggest/jsonschema-go#field-tags) named after JSON Schema/OpenAPI 3 Schema constraints
 * Flexible schema control with [`jsonschema-go`](https://github.com/swaggest/jsonschema-go#implementing-interfaces-on-a-type)
 
diff --git a/go.mod b/go.mod
index 1d21488..f4dc96d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,10 +3,10 @@ module github.com/swaggest/openapi-go
 go 1.17
 
 require (
-	github.com/bool64/dev v0.2.22
+	github.com/bool64/dev v0.2.24
 	github.com/stretchr/testify v1.8.1
 	github.com/swaggest/assertjson v1.7.0
-	github.com/swaggest/jsonschema-go v0.3.42
+	github.com/swaggest/jsonschema-go v0.3.45
 	github.com/swaggest/refl v1.1.0
 	gopkg.in/yaml.v2 v2.4.0
 )
diff --git a/go.sum b/go.sum
index 4d6d7b4..334f251 100644
--- a/go.sum
+++ b/go.sum
@@ -2,9 +2,8 @@ github.com/bool64/dev v0.2.5/go.mod h1:cTHiTDNc8EewrQPy3p1obNilpMpdmlUesDkFTF2zR
 github.com/bool64/dev v0.2.10/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
 github.com/bool64/dev v0.2.16/go.mod h1:/csLrm+4oDSsKJRIVS0mrywAonLnYKFG8RvGT7Jh9b8=
 github.com/bool64/dev v0.2.17/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/dev v0.2.21/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
-github.com/bool64/dev v0.2.22 h1:YJFKBRKplkt+0Emq/5Xk1Z5QRmMNzc1UOJkR3rxJksA=
-github.com/bool64/dev v0.2.22/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
+github.com/bool64/dev v0.2.24 h1:xptlKivPh870W3Xc9szPcM7wkFmTMuHT8rc0nu7dITk=
+github.com/bool64/dev v0.2.24/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg=
 github.com/bool64/shared v0.1.4/go.mod h1:ryGjsnQFh6BnEXClfVlEJrzjwzat7CmA8PNS5E+jPp0=
 github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
 github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs=
@@ -64,8 +63,8 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/swaggest/assertjson v1.7.0 h1:SKw5Rn0LQs6UvmGrIdaKQbMR1R3ncXm5KNon+QJ7jtw=
 github.com/swaggest/assertjson v1.7.0/go.mod h1:vxMJMehbSVJd+dDWFCKv3QRZKNTpy/ktZKTz9LOEDng=
-github.com/swaggest/jsonschema-go v0.3.42 h1:Eepch6hlDtEuSVgfBH4NsP9F2Yf/KGj+7MwgBk618ac=
-github.com/swaggest/jsonschema-go v0.3.42/go.mod h1:yt5lcjdqywYtNnzkFgYnyfvMwlH2XHEDitKw749AV1w=
+github.com/swaggest/jsonschema-go v0.3.45 h1:h2RhU+K3HbI8c+giX4PWI8R96EW6/wtPHfagpAvdPfc=
+github.com/swaggest/jsonschema-go v0.3.45/go.mod h1:pW2jvwFFD2cQ0jv51bAo8+RLmma7yaUOIcH8s54EfJw=
 github.com/swaggest/refl v1.1.0 h1:a+9a75Kv6ciMozPjVbOfcVTEQe81t2R3emvaD9oGQGc=
 github.com/swaggest/refl v1.1.0/go.mod h1:g3Qa6ki0A/L2yxiuUpT+cuBURuRaltF5SDQpg1kMZSY=
 github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU=
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index 439fbb0..761855d 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -91,12 +91,12 @@ type OperationContext struct {
 // SetupRequest sets up operation parameters.
 func (r *Reflector) SetupRequest(oc OperationContext) error {
 	return joinErrors(
-		r.parseParametersIn(oc, ParameterInQuery, oc.ReqQueryMapping),
-		r.parseParametersIn(oc, ParameterInPath, oc.ReqPathMapping),
-		r.parseParametersIn(oc, ParameterInCookie, oc.ReqCookieMapping),
-		r.parseParametersIn(oc, ParameterInHeader, oc.ReqHeaderMapping),
-		r.parseRequestBody(oc, tagJSON, mimeJSON, oc.HTTPMethod, nil),
-		r.parseRequestBody(oc, tagFormData, mimeFormUrlencoded, oc.HTTPMethod, oc.ReqFormDataMapping),
+		r.parseParametersIn(oc, oc.ReqQueryMapping, ParameterInQuery, tagForm),
+		r.parseParametersIn(oc, oc.ReqPathMapping, ParameterInPath),
+		r.parseParametersIn(oc, oc.ReqCookieMapping, ParameterInCookie),
+		r.parseParametersIn(oc, oc.ReqHeaderMapping, ParameterInHeader),
+		r.parseRequestBody(oc, mimeJSON, oc.HTTPMethod, nil, tagJSON),
+		r.parseRequestBody(oc, mimeFormUrlencoded, oc.HTTPMethod, oc.ReqFormDataMapping, tagFormData, tagForm),
 	)
 }
 
@@ -112,6 +112,7 @@ func (r *Reflector) SetRequest(o *Operation, input interface{}, httpMethod strin
 const (
 	tagJSON            = "json"
 	tagFormData        = "formData"
+	tagForm            = "form"
 	mimeJSON           = "application/json"
 	mimeFormUrlencoded = "application/x-www-form-urlencoded"
 	mimeMultipart      = "multipart/form-data"
@@ -133,7 +134,7 @@ type RequestJSONBodyEnforcer interface {
 }
 
 func (r *Reflector) parseRequestBody(
-	oc OperationContext, tag, mime string, httpMethod string, mapping map[string]string,
+	oc OperationContext, mime string, httpMethod string, mapping map[string]string, tag string, additionalTags ...string,
 ) error {
 	o := oc.Operation
 	input := oc.Input
@@ -179,7 +180,7 @@ func (r *Reflector) parseRequestBody(
 		jsonschema.DefinitionsPrefix("#/components/schemas/"+definitionPrefix),
 		jsonschema.RootRef,
 		jsonschema.PropertyNameMapping(mapping),
-		jsonschema.PropertyNameTag(tag),
+		jsonschema.PropertyNameTag(tag, additionalTags...),
 		jsonschema.InterceptType(func(v reflect.Value, s *jsonschema.Schema) (bool, error) {
 			vv := v.Interface()
 
@@ -240,7 +241,7 @@ const (
 )
 
 func (r *Reflector) parseParametersIn(
-	oc OperationContext, in ParameterIn, propertyMapping map[string]string,
+	oc OperationContext, propertyMapping map[string]string, in ParameterIn, additionalTags ...string,
 ) error {
 	o := oc.Operation
 	input := oc.Input
@@ -254,7 +255,7 @@ func (r *Reflector) parseParametersIn(
 		jsonschema.DefinitionsPrefix("#/components/schemas/"),
 		jsonschema.CollectDefinitions(r.collectDefinition),
 		jsonschema.PropertyNameMapping(propertyMapping),
-		jsonschema.PropertyNameTag(string(in)),
+		jsonschema.PropertyNameTag(string(in), additionalTags...),
 		func(rc *jsonschema.ReflectContext) {
 			rc.UnnamedFieldWithTag = true
 		},
@@ -465,6 +466,10 @@ func (r *Reflector) hasJSONBody(output interface{}) (bool, error) {
 
 // SetupResponse sets up operation response.
 func (r *Reflector) SetupResponse(oc OperationContext) error {
+	if oc.HTTPStatus == 0 {
+		oc.HTTPStatus = 200
+	}
+
 	httpStatus := strconv.Itoa(oc.HTTPStatus)
 	resp := oc.Operation.Responses.MapOfResponseOrRefValues[httpStatus].Response
 


Test patch:
diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml
index 94441bd..17cca25 100644
--- a/.github/workflows/test-unit.yml
+++ b/.github/workflows/test-unit.yml
@@ -82,7 +82,7 @@ jobs:
           go tool cover -func=./unit.coverprofile > unit.txt
           TOTAL=$(grep 'total:' unit.txt)
           echo "${TOTAL}"
-          echo "::set-output name=total::$TOTAL"
+          echo "total=$TOTAL" >> $GITHUB_OUTPUT
 
       - name: Annotate missing test coverage
         id: annotate
@@ -94,16 +94,14 @@ jobs:
           git fetch origin master ${{ github.event.pull_request.base.sha }}
           REP=$(./gocovdiff -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV})
           echo "${REP}"
-          REP="${REP//$'\n'/%0A}"
           cat gha-unit.txt
           DIFF=$(test -e unit-base.txt && ./gocovdiff -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file")
-          DIFF="${DIFF//$'\n'/%0A}"
           TOTAL=$(cat delta-cov-unit.txt)
-          echo "::set-output name=rep::$REP"
-          echo "::set-output name=diff::$DIFF"
-          echo "::set-output name=total::$TOTAL"
+          echo "rep<<EOF" >> $GITHUB_OUTPUT && echo "$REP" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
+          echo "diff<<EOF" >> $GITHUB_OUTPUT && echo "$DIFF" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
+          echo "total<<EOF" >> $GITHUB_OUTPUT && echo "$TOTAL" >> $GITHUB_OUTPUT && echo "EOF" >> $GITHUB_OUTPUT
 
-      - name: Comment Test Coverage
+      - name: Comment test coverage
         continue-on-error: true
         if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != ''
         uses: marocchino/sticky-pull-request-comment@v2
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 7889aa7..d04ad22 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -804,3 +804,54 @@ func TestReflector_SetRequest_formData_with_json(t *testing.T) {
 	  }
 	}`), r.SpecEns())
 }
+
+func TestReflector_SetupRequest_form(t *testing.T) {
+	// Fields with `form` will be present as both `query` parameter and `formData` property.
+	type req struct {
+		Foo  int    `formData:"foo"`
+		Bar  int    `form:"bar"`
+		Baz  string `query:"baz"`
+		Quux string `query:"quux" form:"ignored"`
+	}
+
+	r := openapi3.Reflector{}
+	oc := openapi3.OperationContext{
+		Operation: &openapi3.Operation{},
+		Input:     req{},
+	}
+
+	require.NoError(t, r.SetupRequest(oc))
+	require.NoError(t, r.SpecEns().AddOperation(http.MethodPost, "/foo", *oc.Operation))
+
+	assertjson.EqualMarshal(t, []byte(`{
+	  "openapi":"3.0.3","info":{"title":"","version":""},
+	  "paths":{
+		"/foo":{
+		  "post":{
+			"parameters":[
+			  {"name":"bar","in":"query","schema":{"type":"integer"}},
+			  {"name":"baz","in":"query","schema":{"type":"string"}},
+			  {"name":"quux","in":"query","schema":{"type":"string"}}
+			],
+			"requestBody":{
+			  "content":{
+				"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/FormDataOpenapi3TestReq"}}
+			  }
+			},
+			"responses":{"204":{"description":"No Content"}}
+		  }
+		}
+	  },
+	  "components":{
+		"schemas":{
+		  "FormDataOpenapi3TestReq":{
+			"type":"object",
+			"properties":{
+			  "bar":{"type":"integer"},"foo":{"type":"integer"},
+			  "ignored":{"type":"string"}
+			}
+		  }
+		}
+	  }
+	}`), r.SpecEns())
+}