Repository: swaggest/openapi-go
Pull request #73 (2023-07-30 07:50:41)

Abstract OpenAPI schema with an interface
In order to prepare for OpenAPI 3.1 addition, current OpenAPI 3.0 implementation has to be generalized and decoupled from useful public API.

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

| Language  |  Files  |   Lines    |    Code    | Comments |  Blanks   | Complexity |     Bytes     |
|-----------|---------|------------|------------|----------|-----------|------------|---------------|
| Go        | 14 (+1) | 2118 (+34) | 1548 (+18) | 134 (+6) | 436 (+10) | 406 (-2)   | 49.7K (+805B) |
| Go (test) | 10      | 3348 (+2)  | 2591 (+2)  | 333      | 424       | 32         | 87.8K (+95B)  |
| JSON      | 6       | 2206       | 2206       | 0        | 0         | 0          | 50.1K (+47B)  |
<!-- Sticky Pull Request CommentLOC -->
github-actions[bot]: ### Go API Changes

<pre>
# github.com/swaggest/openapi-go
## incompatible changes
Reflector.JSONSchemaReflector: added
Reflector.SpecSchema: added
## compatible changes
SpecSchema: added

# github.com/swaggest/openapi-go/openapi3
## compatible changes
(*Reflector).JSONSchemaReflector: added
(*Reflector).SpecSchema: added
(*Spec).SetDescription: added
(*Spec).SetTitle: added
(*Spec).SetVersion: added

# summary
Inferred base version: v0.2.35
Suggested version: v0.3.0
</pre>
<!-- Sticky Pull Request Commentgorelease -->
github-actions[bot]: ### Unit Test Coverage
total:									(statements)				50.8%
changed lines: (statements) 80.0%, coverage is less than 90.0%, consider testing the changes more thoroughly
<details><summary>Coverage of changed lines</summary>

|          File           |      Function       | Coverage |
|-------------------------|---------------------|----------|
| Total                   |                     | 80.0%    |
| openapi3/helper.go      |                     | 100.0%   |
| openapi3/helper.go:134  | SetTitle            | 100.0%   |
| openapi3/helper.go:139  | SetDescription      | 100.0%   |
| openapi3/helper.go:144  | SetVersion          | 100.0%   |
| openapi3/reflect.go     |                     | 50.0%    |
| openapi3/reflect.go:795 | SpecSchema          | 100.0%   |
| openapi3/reflect.go:800 | JSONSchemaReflector | 0.0%     |

</details>

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

|                        File                        |      Function       | Base Coverage | Current Coverage |
|----------------------------------------------------|---------------------|---------------|------------------|
| Total                                              |                     | 50.8%         | 50.8% (0.0%)     |
| github.com/swaggest/openapi-go/openapi3/helper.go  | SetDescription      | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/helper.go  | SetTitle            | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/helper.go  | SetVersion          | no function   | 100.0%           |
| github.com/swaggest/openapi-go/openapi3/reflect.go | JSONSchemaReflector | no function   | 0.0%             |
| github.com/swaggest/openapi-go/openapi3/reflect.go | SpecSchema          | no function   | 100.0%           |

</details>
<!-- Sticky Pull Request Commentunit-test -->
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/swaggest/openapi-go/pull/73?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) Report
> Merging [#73](https://app.codecov.io/gh/swaggest/openapi-go/pull/73?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (ea0fc2c) into [master](https://app.codecov.io/gh/swaggest/openapi-go/commit/709f0d7884b82087980143fe420f709d71cbba79?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) (709f0d7) will **increase** coverage by `0.04%`.
> The diff coverage is `80.00%`.

```diff
@@            Coverage Diff             @@
##           master      #73      +/-   ##
==========================================
+ Coverage   43.12%   43.17%   +0.04%     
==========================================
  Files           8        8              
  Lines        3810     3815       +5     
==========================================
+ Hits         1643     1647       +4     
- Misses       1859     1860       +1     
  Partials      308      308              
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `43.17% <80.00%> (+0.04%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Files Changed](https://app.codecov.io/gh/swaggest/openapi-go/pull/73?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest) | Coverage Δ | |
|---|---|---|
| [openapi3/reflect.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/73?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvcmVmbGVjdC5nbw==) | `80.96% <50.00%> (-0.16%)` | :arrow_down: |
| [openapi3/helper.go](https://app.codecov.io/gh/swaggest/openapi-go/pull/73?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest#diff-b3BlbmFwaTMvaGVscGVyLmdv) | `90.38% <100.00%> (+0.58%)` | :arrow_up: |

:mega: We’re building smart automated test selection to slash your CI/CD build times. [Learn more](https://about.codecov.io/iterative-testing/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=swaggest)


Patch:
diff --git a/go.mod b/go.mod
index b5be700..ced757c 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ require (
 	github.com/bool64/dev v0.2.29
 	github.com/stretchr/testify v1.8.2
 	github.com/swaggest/assertjson v1.9.0
-	github.com/swaggest/jsonschema-go v0.3.54
+	github.com/swaggest/jsonschema-go v0.3.55
 	github.com/swaggest/refl v1.2.0
 	gopkg.in/yaml.v2 v2.4.0
 )
diff --git a/go.sum b/go.sum
index 0adb2a1..a9ef3cb 100644
--- a/go.sum
+++ b/go.sum
@@ -33,8 +33,8 @@ github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
 github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
 github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU=
-github.com/swaggest/jsonschema-go v0.3.54 h1:kRwXd7tqrub8vmtAVV5IElGTE1hiEuo/n9gHrsX7ySY=
-github.com/swaggest/jsonschema-go v0.3.54/go.mod h1:iQdEa2VW62As5W+884vA13TC2pEwnKjlQGaQe2pj+fc=
+github.com/swaggest/jsonschema-go v0.3.55 h1:xbDQaLw9NxxkL3meYnUnX5f6Hhav2wNAEfb/We53CkM=
+github.com/swaggest/jsonschema-go v0.3.55/go.mod h1:5WFFGBBte5JAWAV8gDpNRJ/tlQnb1AHDdf/ghgsVUik=
 github.com/swaggest/refl v1.2.0 h1:Qqqhfwi7REXF6/4cwJmj3gQMzl0Q0cYquxTYdD0kvi0=
 github.com/swaggest/refl v1.2.0/go.mod h1:CkC6g7h1PW33KprTuYRSw8UUOslRUt4lF3oe7tTIgNU=
 github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
diff --git a/openapi3/helper.go b/openapi3/helper.go
index 37a37db..d4571f8 100644
--- a/openapi3/helper.go
+++ b/openapi3/helper.go
@@ -127,3 +127,20 @@ func (o Operation) UnknownParamIsForbidden(in ParameterIn) bool {
 
 	return f && ok
 }
+
+var _ openapi.SpecSchema = &Spec{}
+
+// SetTitle describes the service.
+func (s *Spec) SetTitle(t string) {
+	s.Info.Title = t
+}
+
+// SetDescription describes the service.
+func (s *Spec) SetDescription(d string) {
+	s.Info.WithDescription(d)
+}
+
+// SetVersion describes the service.
+func (s *Spec) SetVersion(v string) {
+	s.Info.Version = v
+}
diff --git a/openapi3/reflect.go b/openapi3/reflect.go
index bab795c..48fe3f2 100644
--- a/openapi3/reflect.go
+++ b/openapi3/reflect.go
@@ -243,21 +243,27 @@ func (r *Reflector) setupRequest(o *Operation, oc openapi.OperationContext) erro
 	for _, cu := range oc.Request() {
 		switch cu.ContentType {
 		case "":
-			return joinErrors(
+			if err := joinErrors(
 				r.parseParameters(o, oc, cu),
 				r.parseRequestBody(o, oc, cu, mimeJSON, oc.Method(), nil, tagJSON),
 				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
-			)
+			); err != nil {
+				return err
+			}
 		case mimeJSON:
-			return joinErrors(
+			if err := joinErrors(
 				r.parseParameters(o, oc, cu),
 				r.parseRequestBody(o, oc, cu, mimeJSON, oc.Method(), nil, tagJSON),
-			)
+			); err != nil {
+				return err
+			}
 		case mimeFormUrlencoded, mimeMultipart:
-			return joinErrors(
+			if err := joinErrors(
 				r.parseParameters(o, oc, cu),
 				r.parseRequestBody(o, oc, cu, mimeFormUrlencoded, oc.Method(), cu.FieldMapping(openapi.InFormData), tagFormData, tagForm),
-			)
+			); err != nil {
+				return err
+			}
 		default:
 			r.stringRequestBody(o, cu.ContentType, cu.Format)
 		}
@@ -573,9 +579,9 @@ func (r *Reflector) parseParametersIn(
 var defNameSanitizer = regexp.MustCompile(`[^a-zA-Z0-9.\-_]+`)
 
 func sanitizeDefName(rc *jsonschema.ReflectContext) {
-	rc.DefName = func(t reflect.Type, defaultDefName string) string {
+	jsonschema.InterceptDefName(func(t reflect.Type, defaultDefName string) string {
 		return defNameSanitizer.ReplaceAllString(defaultDefName, "")
-	}
+	})(rc)
 }
 
 func (r *Reflector) collectDefinition(namePrefix string) func(name string, schema jsonschema.Schema) {
@@ -790,3 +796,13 @@ func (r *Reflector) parseJSONResponse(resp *Response, oc openapi.OperationContex
 
 	return nil
 }
+
+// SpecSchema returns OpenAPI spec schema.
+func (r *Reflector) SpecSchema() openapi.SpecSchema {
+	return r.SpecEns()
+}
+
+// JSONSchemaReflector provides access to a low-level struct reflector.
+func (r *Reflector) JSONSchemaReflector() *jsonschema.Reflector {
+	return &r.Reflector
+}
diff --git a/reflector.go b/reflector.go
index fa1790f..9f406c9 100644
--- a/reflector.go
+++ b/reflector.go
@@ -8,6 +8,9 @@ type Reflector interface {
 
 	NewOperationContext(method, pathPattern string) (OperationContext, error)
 	AddOperation(oc OperationContext) error
+
+	SpecSchema() SpecSchema
+	JSONSchemaReflector() *jsonschema.Reflector
 }
 
 // JSONSchemaCallback is a user function called by JSONSchemaWalker.
diff --git a/spec.go b/spec.go
new file mode 100644
index 0000000..45960fb
--- /dev/null
+++ b/spec.go
@@ -0,0 +1,8 @@
+package openapi
+
+// SpecSchema abstracts OpenAPI schema implementation to generalize multiple revisions.
+type SpecSchema interface {
+	SetTitle(t string)
+	SetDescription(d string)
+	SetVersion(v string)
+}


Test patch:
diff --git a/openapi3/_testdata/openapi_req.json b/openapi3/_testdata/openapi_req.json
index 9b5ba10..d106808 100644
--- a/openapi3/_testdata/openapi_req.json
+++ b/openapi3/_testdata/openapi_req.json
@@ -1,5 +1,5 @@
 {
- "openapi":"3.0.3","info":{"title":"SampleAPI","version":"1.2.3"},
+ "openapi":"3.0.3","info":{"title":"SampleAPI","description":"This a sample API description.","version":"1.2.3"},
  "paths":{
   "/somewhere/{in_path}":{
    "get":{
@@ -16,6 +16,7 @@
      {"name":"in_cookie","in":"cookie","deprecated":true,"schema":{"type":"string","deprecated":true}},
      {"name":"in_header","in":"header","schema":{"type":"number"}}
     ],
+    "requestBody":{"content":{"text/csv":{"schema":{"type":"string"}}}},
     "responses":{"204":{"description":"No Content"}}
    }
   }
diff --git a/openapi3/_testdata/openapi_req2.json b/openapi3/_testdata/openapi_req2.json
new file mode 100644
index 0000000..395f49e
--- /dev/null
+++ b/openapi3/_testdata/openapi_req2.json
@@ -0,0 +1,23 @@
+{
+ "openapi":"3.0.3","info":{"title":"SampleAPI","description":"This a sample API description.","version":"1.2.3"},
+ "paths":{
+  "/somewhere/{in_path}":{
+   "get":{
+    "parameters":[
+     {
+      "name":"in_query1","in":"query","description":"Query parameter.","required":true,
+      "schema":{"type":"integer","description":"Query parameter."}
+     },
+     {
+      "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","deprecated":true}},
+     {"name":"in_header","in":"header","schema":{"type":"number"}}
+    ],
+    "responses":{"204":{"description":"No Content"}}
+   }
+  }
+ }
+}
\ No newline at end of file
diff --git a/openapi3/_testdata/swgui/swgui.go b/openapi3/_testdata/swgui/swgui.go
index a5d9193..577320d 100644
--- a/openapi3/_testdata/swgui/swgui.go
+++ b/openapi3/_testdata/swgui/swgui.go
@@ -3,27 +3,23 @@ package main
 import (
 	"log"
 	"net/http"
-	"os"
 
 	swgui "github.com/swaggest/swgui/v5"
 )
 
 func main() {
-	h := swgui.NewHandler("Foo", "/openapi.json", "/")
+	urlToSchema := "/openapi.json"
+	filePathToSchema := "../openapi.json"
+
+	swh := swgui.NewHandler("Foo", urlToSchema, "/")
 	hh := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
-		if r.URL.Path == "/openapi.json" {
-			o, err := os.ReadFile("../openapi.json")
-			if err != nil {
-				http.Error(rw, err.Error(), 500)
-				return
-			}
-			rw.Header().Set("Content-Type", "application/json")
-			rw.Write(o)
-			return
+		if r.URL.Path == urlToSchema {
+			http.ServeFile(rw, r, filePathToSchema)
 		}
 
-		h.ServeHTTP(rw, r)
+		swh.ServeHTTP(rw, r)
 	})
+
 	log.Println("Starting Swagger UI server at http://localhost:8082/")
 	_ = http.ListenAndServe("localhost:8082", hh)
 }
diff --git a/openapi3/reflect_deprecated_test.go b/openapi3/reflect_deprecated_test.go
index af67140..81046a4 100644
--- a/openapi3/reflect_deprecated_test.go
+++ b/openapi3/reflect_deprecated_test.go
@@ -83,8 +83,9 @@ func TestReflector_SetRequest(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
 	s := reflector.SpecEns()
-	s.Info.Title = apiName
-	s.Info.Version = apiVersion
+	s.SetTitle(apiName)
+	s.SetVersion(apiVersion)
+	s.SetDescription("This a sample API description.")
 
 	op := openapi3.Operation{}
 
@@ -96,9 +97,9 @@ func TestReflector_SetRequest(t *testing.T) {
 	b, err := assertjson.MarshalIndentCompact(s, "", " ", 120)
 	assert.NoError(t, err)
 
-	require.NoError(t, os.WriteFile("_testdata/openapi_req_last_run.json", b, 0o600))
+	require.NoError(t, os.WriteFile("_testdata/openapi_req2_last_run.json", b, 0o600))
 
-	expected, err := os.ReadFile("_testdata/openapi_req.json")
+	expected, err := os.ReadFile("_testdata/openapi_req2.json")
 	require.NoError(t, err)
 
 	assertjson.Equal(t, expected, b)
diff --git a/openapi3/reflect_test.go b/openapi3/reflect_test.go
index 93f0f19..7231167 100644
--- a/openapi3/reflect_test.go
+++ b/openapi3/reflect_test.go
@@ -162,13 +162,18 @@ func TestReflector_AddOperation_uploadInterface(t *testing.T) {
 func TestReflector_AddOperation_request(t *testing.T) {
 	reflector := openapi3.Reflector{}
 
-	s := reflector.SpecEns()
-	s.Info.Title = apiName
-	s.Info.Version = apiVersion
+	s := reflector.SpecSchema()
+	s.SetTitle(apiName)
+	s.SetVersion(apiVersion)
+	s.SetDescription("This a sample API description.")
 
 	oc, err := reflector.NewOperationContext(http.MethodGet, "/somewhere/{in_path}")
 	require.NoError(t, err)
 	oc.AddReqStructure(new(GetReq))
+	oc.AddReqStructure(nil, func(cu *openapi.ContentUnit) {
+		cu.ContentType = "text/csv"
+		cu.Description = "Request body in CSV format."
+	})
 
 	require.NoError(t, reflector.AddOperation(oc))