Repository: ogen-go/ogen
Pull request #445 (2022-06-30 03:41:20)

feat(parser): resolve components from complicated JSON pointers

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
> Merging [#445](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (509a0f7) into [main](https://codecov.io/gh/ogen-go/ogen/commit/8c478edfc40e322fe2ec3d804b66814e5f02300d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (8c478ed) will **decrease** coverage by `0.02%`.
> The diff coverage is `67.04%`.

```diff
@@            Coverage Diff             @@
##             main     #445      +/-   ##
==========================================
- Coverage   70.13%   70.11%   -0.03%     
==========================================
  Files         138      139       +1     
  Lines        9792     9806      +14     
==========================================
+ Hits         6868     6875       +7     
- Misses       2504     2510       +6     
- Partials      420      421       +1     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [openapi/parser/resolve.go](https://codecov.io/gh/ogen-go/ogen/pull/445/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcmVzb2x2ZS5nbw==) | `64.88% <45.23%> (ø)` | |
| [openapi/parser/resolve\_ctx.go](https://codecov.io/gh/ogen-go/ogen/pull/445/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcmVzb2x2ZV9jdHguZ28=) | `86.95% <86.95%> (ø)` | |
| [gen/genctx.go](https://codecov.io/gh/ogen-go/ogen/pull/445/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbmN0eC5nbw==) | `85.00% <0.00%> (-7.50%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Last update [8c478ed...509a0f7](https://codecov.io/gh/ogen-go/ogen/pull/445?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/openapi/parser/ref.go b/openapi/parser/resolve.go
similarity index 69%
rename from openapi/parser/ref.go
rename to openapi/parser/resolve.go
index c191a5e8..8bc568da 100644
--- a/openapi/parser/ref.go
+++ b/openapi/parser/resolve.go
@@ -3,7 +3,6 @@ package parser
 import (
 	"context"
 	"encoding/json"
-	"net/url"
 	"strings"
 
 	"github.com/go-faster/errors"
@@ -13,83 +12,6 @@ import (
 	"github.com/ogen-go/ogen/openapi"
 )
 
-type refKey struct {
-	loc string
-	ref string
-}
-
-func (r *refKey) fromURL(u *url.URL) {
-	{
-		// Make copy.
-		u2 := *u
-		u2.Fragment = ""
-		r.loc = u2.String()
-	}
-	r.ref = "#" + u.Fragment
-}
-
-type resolveCtx struct {
-	// Location stack. Used for context-depending resolving.
-	//
-	// For resolve trace like
-	//
-	// 	"#/components/schemas/Schema" ->
-	// 	"https://example.com/schema#Schema" ->
-	//	"#/definitions/SchemaProperty"
-	//
-	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
-	locstack []string
-	// Store references to detect infinite recursive references.
-	refs       map[refKey]struct{}
-	depthLimit int
-}
-
-func newResolveCtx(depthLimit int) *resolveCtx {
-	return &resolveCtx{
-		locstack:   nil,
-		refs:       map[refKey]struct{}{},
-		depthLimit: depthLimit,
-	}
-}
-
-func (r *resolveCtx) add(ref string) (key refKey, _ error) {
-	u, err := url.Parse(ref)
-	if err != nil {
-		return refKey{}, err
-	}
-	key.fromURL(u)
-
-	if r.depthLimit <= 0 {
-		return refKey{}, errors.New("depth limit exceeded")
-	}
-	if _, ok := r.refs[key]; ok {
-		return key, errors.New("infinite recursion")
-	}
-	r.refs[key] = struct{}{}
-	r.depthLimit--
-
-	if key.loc != "" {
-		r.locstack = append(r.locstack, key.loc)
-	}
-	return key, nil
-}
-
-func (r *resolveCtx) delete(key refKey) {
-	r.depthLimit++
-	delete(r.refs, key)
-	if key.loc != "" && len(r.locstack) > 0 {
-		r.locstack = r.locstack[:len(r.locstack)-1]
-	}
-}
-
-func (r *resolveCtx) lastLoc() string {
-	s := r.locstack
-	if len(s) == 0 {
-		return ""
-	}
-	return s[len(s)-1]
-}
-
 func (p *parser) getSchema(ctx *resolveCtx) ([]byte, error) {
 	loc := ctx.lastLoc()
 
@@ -107,18 +29,21 @@ func (p *parser) getSchema(ctx *resolveCtx) ([]byte, error) {
 	return r, nil
 }
 
-func (p *parser) resolve(key refKey, ctx *resolveCtx, to interface{}) error {
-	schema, err := p.getSchema(ctx)
+func resolvePointer(root []byte, ptr string, to interface{}) error {
+	data, err := jsonpointer.Resolve(ptr, root)
 	if err != nil {
 		return err
 	}
 
-	data, err := jsonpointer.Resolve(key.ref, schema)
+	return json.Unmarshal(data, to)
+}
+
+func (p *parser) resolve(key refKey, ctx *resolveCtx, to interface{}) error {
+	schema, err := p.getSchema(ctx)
 	if err != nil {
 		return err
 	}
-
-	return json.Unmarshal(data, to)
+	return resolvePointer(schema, key.ref, to)
 }
 
 func (p *parser) resolveRequestBody(ref string, ctx *resolveCtx) (*openapi.RequestBody, error) {
@@ -140,10 +65,13 @@ func (p *parser) resolveRequestBody(ref string, ctx *resolveCtx) (*openapi.Reque
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.RequestBodies[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
@@ -179,10 +107,13 @@ func (p *parser) resolveResponse(ref string, ctx *resolveCtx) (*openapi.Response
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Responses[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
@@ -218,10 +149,13 @@ func (p *parser) resolveParameter(ref string, ctx *resolveCtx) (*openapi.Paramet
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Parameters[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
@@ -257,10 +191,13 @@ func (p *parser) resolveHeader(headerName, ref string, ctx *resolveCtx) (*openap
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Headers[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
@@ -296,10 +233,13 @@ func (p *parser) resolveExample(ref string, ctx *resolveCtx) (*openapi.Example,
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.Examples[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
@@ -335,10 +275,13 @@ func (p *parser) resolveSecuritySchema(ref string, ctx *resolveCtx) (*ogen.Secur
 	if key.loc == "" && ctx.lastLoc() == "" {
 		name := strings.TrimPrefix(ref, prefix)
 		c, found := p.spec.Components.SecuritySchemes[name]
-		if !found {
-			return nil, errors.New("component by reference not found")
+		if found {
+			component = c
+		} else {
+			if err := resolvePointer(p.spec.Raw, ref, &component); err != nil {
+				return nil, err
+			}
 		}
-		component = c
 	} else {
 		if err := p.resolve(key, ctx, &component); err != nil {
 			return nil, err
diff --git a/openapi/parser/resolve_ctx.go b/openapi/parser/resolve_ctx.go
new file mode 100644
index 00000000..e1a092f4
--- /dev/null
+++ b/openapi/parser/resolve_ctx.go
@@ -0,0 +1,84 @@
+package parser
+
+import (
+	"net/url"
+
+	"github.com/go-faster/errors"
+)
+
+type refKey struct {
+	loc string
+	ref string
+}
+
+func (r *refKey) fromURL(u *url.URL) {
+	{
+		// Make copy.
+		u2 := *u
+		u2.Fragment = ""
+		r.loc = u2.String()
+	}
+	r.ref = "#" + u.Fragment
+}
+
+type resolveCtx struct {
+	// Location stack. Used for context-depending resolving.
+	//
+	// For resolve trace like
+	//
+	// 	"#/components/schemas/Schema" ->
+	// 	"https://example.com/schema#Schema" ->
+	//	"#/definitions/SchemaProperty"
+	//
+	// "#/definitions/SchemaProperty" should be resolved against "https://example.com/schema".
+	locstack []string
+	// Store references to detect infinite recursive references.
+	refs       map[refKey]struct{}
+	depthLimit int
+}
+
+func newResolveCtx(depthLimit int) *resolveCtx {
+	return &resolveCtx{
+		locstack:   nil,
+		refs:       map[refKey]struct{}{},
+		depthLimit: depthLimit,
+	}
+}
+
+func (r *resolveCtx) add(ref string) (key refKey, _ error) {
+	u, err := url.Parse(ref)
+	if err != nil {
+		return refKey{}, err
+	}
+	key.fromURL(u)
+
+	if r.depthLimit <= 0 {
+		return refKey{}, errors.New("depth limit exceeded")
+	}
+	if _, ok := r.refs[key]; ok {
+		return key, errors.New("infinite recursion")
+	}
+	r.refs[key] = struct{}{}
+	r.depthLimit--
+
+	if key.loc != "" {
+		r.locstack = append(r.locstack, key.loc)
+	}
+	return key, nil
+}
+
+func (r *resolveCtx) delete(key refKey) {
+	r.depthLimit++
+	delete(r.refs, key)
+	if key.loc != "" && len(r.locstack) > 0 {
+		r.locstack = r.locstack[:len(r.locstack)-1]
+	}
+}
+
+func (r *resolveCtx) lastLoc() string {
+	s := r.locstack
+	if len(s) == 0 {
+		return ""
+	}
+	return s[len(s)-1]
+}


Test patch:
diff --git a/openapi/parser/ref_test.go b/openapi/parser/resolve_test.go
similarity index 53%
rename from openapi/parser/ref_test.go
rename to openapi/parser/resolve_test.go
index ad814ff7..146e935f 100644
--- a/openapi/parser/ref_test.go
+++ b/openapi/parser/resolve_test.go
@@ -3,6 +3,7 @@ package parser
 import (
 	"context"
 	"encoding/json"
+	"sort"
 	"testing"
 
 	"github.com/go-faster/errors"
@@ -271,3 +272,229 @@ func TestExternalReference(t *testing.T) {
 		},
 	}, spec)
 }
+
+func TestComplicatedReference(t *testing.T) {
+	root := &ogen.Spec{
+		Paths: map[string]*ogen.PathItem{
+			"/get": {
+				Get: &ogen.Operation{
+					OperationID: "testGet",
+					Description: "operation description",
+					Parameters: []*ogen.Parameter{
+						{Ref: "#/paths/~1post/post/parameters/0"},
+					},
+					RequestBody: &ogen.RequestBody{
+						Ref: "#/paths/~1post/post/requestBody",
+					},
+					Responses: map[string]*ogen.Response{
+						"200": {
+							Ref: "#/paths/~1post/post/responses/200",
+						},
+						"201": {
+							Headers: map[string]*ogen.Header{
+								"ResponseHeader": {
+									Schema: &ogen.Schema{Type: "string"},
+									Style:  "simple",
+								},
+							},
+							Content: map[string]ogen.Media{
+								"application/json": {
+									Schema: &ogen.Schema{Type: "string"},
+								},
+							},
+						},
+					},
+				},
+			},
+			"/post": {
+				Post: &ogen.Operation{
+					OperationID: "testPost",
+					Description: "operation description",
+					Parameters: []*ogen.Parameter{
+						{
+							Name:  "param",
+							In:    "query",
+							Style: "form",
+							Schema: &ogen.Schema{
+								Type: "string",
+							},
+						},
+					},
+					RequestBody: &ogen.RequestBody{
+						Content: map[string]ogen.Media{
+							"application/json": {
+								Schema: &ogen.Schema{
+									Type: "string",
+								},
+							},
+						},
+					},
+					Responses: map[string]*ogen.Response{
+						"200": {
+							Headers: map[string]*ogen.Header{
+								"ResponseHeader": {
+									Schema: &ogen.Schema{Type: "string"},
+									Style:  "simple",
+								},
+							},
+							Content: map[string]ogen.Media{
+								"application/json": {
+									Schema: &ogen.Schema{
+										Ref: "#/paths/~1post/post/requestBody/content/application~1json/schema",
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	a := require.New(t)
+
+	raw, err := json.Marshal(root)
+	a.NoError(err)
+	root.Raw = raw
+
+	spec, err := Parse(root, Settings{})
+	a.NoError(err)
+
+	var (
+		responseHeader = &openapi.Header{
+			Name:   "ResponseHeader",
+			Schema: &jsonschema.Schema{Type: "string"},
+			In:     openapi.LocationHeader,
+			Style:  openapi.HeaderStyleSimple,
+		}
+		testGet = &openapi.Operation{
+			OperationID: "testGet",
+			Description: "operation description",
+			HTTPMethod:  "get",
+			Path: openapi.Path{
+				{Raw: "/get"},
+			},
+			Parameters: []*openapi.Parameter{
+				{
+					Ref:  "#/paths/~1post/post/parameters/0",
+					Name: "param",
+					Schema: &jsonschema.Schema{
+						Type: "string",
+					},
+					In:      openapi.LocationQuery,
+					Style:   openapi.QueryStyleForm,
+					Explode: true,
+				},
+			},
+			RequestBody: &openapi.RequestBody{
+				Ref: "#/paths/~1post/post/requestBody",
+				Content: map[string]*openapi.MediaType{
+					"application/json": {
+						Schema: &jsonschema.Schema{
+							Type: "string",
+						},
+						Examples: map[string]*openapi.Example{},
+						Encoding: map[string]*openapi.Encoding{},
+					},
+				},
+			},
+			Security: []openapi.SecurityRequirements{},
+			Responses: map[string]*openapi.Response{
+				"200": {
+					Ref: "#/paths/~1post/post/responses/200",
+					Headers: map[string]*openapi.Header{
+						"ResponseHeader": responseHeader,
+					},
+					Content: map[string]*openapi.MediaType{
+						"application/json": {
+							Schema: &jsonschema.Schema{
+								Ref:  "#/paths/~1post/post/requestBody/content/application~1json/schema",
+								Type: "string",
+							},
+							Examples: map[string]*openapi.Example{},
+							Encoding: map[string]*openapi.Encoding{},
+						},
+					},
+				},
+				"201": {
+					Headers: map[string]*openapi.Header{
+						"ResponseHeader": responseHeader,
+					},
+					Content: map[string]*openapi.MediaType{
+						"application/json": {
+							Schema:   &jsonschema.Schema{Type: "string"},
+							Examples: map[string]*openapi.Example{},
+							Encoding: map[string]*openapi.Encoding{},
+						},
+					},
+				},
+			},
+		}
+		testPost = &openapi.Operation{
+			OperationID: "testPost",
+			Description: "operation description",
+			HTTPMethod:  "post",
+			Path: openapi.Path{
+				{Raw: "/post"},
+			},
+			Parameters: []*openapi.Parameter{
+				{
+					Name: "param",
+					Schema: &jsonschema.Schema{
+						Type: "string",
+					},
+					In:      openapi.LocationQuery,
+					Style:   openapi.QueryStyleForm,
+					Explode: true,
+				},
+			},
+			RequestBody: &openapi.RequestBody{
+				Content: map[string]*openapi.MediaType{
+					"application/json": {
+						Schema: &jsonschema.Schema{
+							Type: "string",
+						},
+						Examples: map[string]*openapi.Example{},
+						Encoding: map[string]*openapi.Encoding{},
+					},
+				},
+			},
+			Security: []openapi.SecurityRequirements{},
+			Responses: map[string]*openapi.Response{
+				"200": {
+					Headers: map[string]*openapi.Header{
+						"ResponseHeader": responseHeader,
+					},
+					Content: map[string]*openapi.MediaType{
+						"application/json": {
+							Schema: &jsonschema.Schema{
+								Ref:  "#/paths/~1post/post/requestBody/content/application~1json/schema",
+								Type: "string",
+							},
+							Examples: map[string]*openapi.Example{},
+							Encoding: map[string]*openapi.Encoding{},
+						},
+					},
+				},
+			},
+		}
+	)
+	{
+		s := spec.Operations
+		sort.Slice(s, func(i, j int) bool {
+			return s[i].OperationID < s[j].OperationID
+		})
+	}
+	a.Equal(&openapi.API{
+		Operations: []*openapi.Operation{
+			testGet,
+			testPost,
+		},
+		Components: &openapi.Components{
+			Parameters:    map[string]*openapi.Parameter{},
+			Schemas:       map[string]*jsonschema.Schema{},
+			RequestBodies: map[string]*openapi.RequestBody{},
+			Responses:     map[string]*openapi.Response{},
+		},
+	}, spec)
+}