Repository: ogen-go/ogen
Pull request #597 (2022-09-29 07:51:34)

feat(parser): parse webhooks

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/597?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
Base: **72.40**% // Head: **72.51**% // Increases project coverage by **`+0.10%`** :tada:
> Coverage data is based on head [(`b8585e0`)](https://codecov.io/gh/ogen-go/ogen/pull/597?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) compared to base [(`06e8e71`)](https://codecov.io/gh/ogen-go/ogen/commit/06e8e71549567576b2ffcb26f44024586ec90320?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> Patch coverage: 97.50% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #597      +/-   ##
==========================================
+ Coverage   72.40%   72.51%   +0.10%     
==========================================
  Files         160      161       +1     
  Lines       11457    11493      +36     
==========================================
+ Hits         8296     8334      +38     
+ Misses       2732     2731       -1     
+ Partials      429      428       -1     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [jsonschema/resolve.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-anNvbnNjaGVtYS9yZXNvbHZlLmdv) | `65.38% <0.00%> (ø)` | |
| [openapi/parser/parse\_header.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfaGVhZGVyLmdv) | `100.00% <ø> (ø)` | |
| [openapi/parser/parse\_parameter.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfcGFyYW1ldGVyLmdv) | `94.69% <ø> (-0.03%)` | :arrow_down: |
| [openapi/parser/parse\_path\_item.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VfcGF0aF9pdGVtLmdv) | `94.87% <100.00%> (+0.22%)` | :arrow_up: |
| [openapi/parser/parse\_webhook.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2Vfd2ViaG9vay5nbw==) | `100.00% <100.00%> (ø)` | |
| [openapi/parser/parser.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-b3BlbmFwaS9wYXJzZXIvcGFyc2VyLmdv) | `100.00% <100.00%> (ø)` | |
| [gen/gen\_contents.go](https://codecov.io/gh/ogen-go/ogen/pull/597/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL2dlbl9jb250ZW50cy5nbw==) | `72.88% <0.00%> (+0.67%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/597?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/jsonschema/resolve.go b/jsonschema/resolve.go
index bb55aef7..cf766852 100644
--- a/jsonschema/resolve.go
+++ b/jsonschema/resolve.go
@@ -69,7 +69,7 @@ func (p *Parser) resolve(ref string, ctx *jsonpointer.ResolveCtx) (*Schema, erro
 
 	raw, err := resolver.ResolveReference(key.Ref)
 	if err != nil {
-		return nil, errors.Wrap(err, "find schema")
+		return nil, err
 	}
 
 	return p.parse1(raw, ctx, func(s *Schema) *Schema {
diff --git a/openapi/api.go b/openapi/api.go
index dc2da459..c7eec0f0 100644
--- a/openapi/api.go
+++ b/openapi/api.go
@@ -7,6 +7,7 @@ import "github.com/ogen-go/ogen/jsonschema"
 type API struct {
 	Servers    []Server
 	Operations []*Operation
+	Webhooks   []Webhook
 	Components *Components
 }
 
diff --git a/openapi/parser/parse_header.go b/openapi/parser/parse_header.go
index 2f726f59..c8389980 100644
--- a/openapi/parser/parse_header.go
+++ b/openapi/parser/parse_header.go
@@ -61,7 +61,6 @@ func (p *parser) parseHeader(name string, header *ogen.Header, ctx *jsonpointer.
 
 	schema, err := p.parseSchema(header.Schema, ctx)
 	if err != nil {
-		err := errors.Wrap(err, "schema")
 		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
 	}
 
diff --git a/openapi/parser/parse_parameter.go b/openapi/parser/parse_parameter.go
index 1dd44822..1d676478 100644
--- a/openapi/parser/parse_parameter.go
+++ b/openapi/parser/parse_parameter.go
@@ -149,7 +149,6 @@ func (p *parser) parseParameter(param *ogen.Parameter, ctx *jsonpointer.ResolveC
 
 	schema, err := p.parseSchema(param.Schema, ctx)
 	if err != nil {
-		err := errors.Wrap(err, "schema")
 		return nil, p.wrapField("schema", ctx.LastLoc(), locator, err)
 	}
 
diff --git a/openapi/parser/parse_path_item.go b/openapi/parser/parse_path_item.go
index 1aac1235..86fd5b20 100644
--- a/openapi/parser/parse_path_item.go
+++ b/openapi/parser/parse_path_item.go
@@ -40,9 +40,15 @@ func (p *parser) parsePathItem(
 
 	var ops []*openapi.Operation
 	if err := forEachOps(item, func(method string, op ogen.Operation) error {
+		locator := op.Common.Locator
+		defer func() {
+			rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+		}()
+
 		if id := op.OperationID; id != "" {
 			if _, ok := operationIDs[id]; ok {
-				return errors.Errorf("duplicate operationId: %q", id)
+				err := errors.Errorf("duplicate operationId: %q", id)
+				return p.wrapField("operationId", ctx.LastLoc(), locator, err)
 			}
 			operationIDs[id] = struct{}{}
 		}
diff --git a/openapi/parser/parse_webhook.go b/openapi/parser/parse_webhook.go
new file mode 100644
index 00000000..69f6e52d
--- /dev/null
+++ b/openapi/parser/parse_webhook.go
@@ -0,0 +1,48 @@
+package parser
+
+import (
+	"github.com/go-faster/errors"
+
+	"github.com/ogen-go/ogen"
+	"github.com/ogen-go/ogen/internal/jsonpointer"
+	"github.com/ogen-go/ogen/internal/xmaps"
+	"github.com/ogen-go/ogen/openapi"
+)
+
+func (p *parser) parseWebhook(name string, item *ogen.PathItem, ctx *jsonpointer.ResolveCtx) (openapi.Webhook, error) {
+	operations := make(map[string]struct{})
+	// FIXME(tdakkota): we are passing "/" path, but webhook has no path.
+	pi, err := p.parsePathItem("/", item, operations, ctx)
+	if err != nil {
+		return openapi.Webhook{}, errors.Wrap(err, "parse pathItem")
+	}
+	return openapi.Webhook{
+		Name:       name,
+		Operations: pi,
+	}, nil
+}
+
+func (p *parser) parseWebhooks(webhooks map[string]*ogen.PathItem) (r []openapi.Webhook, rerr error) {
+	if len(webhooks) == 0 {
+		return nil, nil
+	}
+	var (
+		locator = p.rootLoc.Field("webhooks")
+		ctx     = jsonpointer.NewResolveCtx(p.depthLimit)
+	)
+	defer func() {
+		rerr = p.wrapLocation(ctx.LastLoc(), locator, rerr)
+	}()
+
+	r = make([]openapi.Webhook, 0, len(webhooks))
+	for _, name := range xmaps.SortedKeys(webhooks) {
+		item := webhooks[name]
+		webhook, err := p.parseWebhook(name, item, ctx)
+		if err != nil {
+			err := errors.Wrapf(err, "parse webhook %q", name)
+			return nil, p.wrapLocation(ctx.LastLoc(), locator.Field(name), err)
+		}
+		r = append(r, webhook)
+	}
+	return r, nil
+}
diff --git a/openapi/parser/parser.go b/openapi/parser/parser.go
index 5a40b7c1..33337b7a 100644
--- a/openapi/parser/parser.go
+++ b/openapi/parser/parser.go
@@ -112,9 +112,15 @@ func Parse(spec *ogen.Spec, s Settings) (*openapi.API, error) {
 		return nil, errors.Wrap(err, "parse servers")
 	}
 
+	webhooks, err := p.parseWebhooks(p.spec.Webhooks)
+	if err != nil {
+		return nil, errors.Wrap(err, "parse webhooks")
+	}
+
 	return &openapi.API{
 		Servers:    servers,
 		Operations: p.operations,
+		Webhooks:   webhooks,
 		Components: components,
 	}, nil
 }
diff --git a/openapi/webhook.go b/openapi/webhook.go
new file mode 100644
index 00000000..d7e29e23
--- /dev/null
+++ b/openapi/webhook.go
@@ -0,0 +1,9 @@
+package openapi
+
+// Webhook is an OpenAPI Webhook.
+type Webhook struct {
+	// Name of the webhook.
+	Name string
+	// Operations of the webhook's Path Item.
+	Operations []*Operation
+}


Test patch:
diff --git a/_testdata/examples/redoc/pathItems.json b/_testdata/examples/redoc/pathItems.json
index 0bd88b05..9895dd15 100644
--- a/_testdata/examples/redoc/pathItems.json
+++ b/_testdata/examples/redoc/pathItems.json
@@ -6,12 +6,27 @@
   },
   "webhooks": {
     "myWebhook": {
-      "$ref": "#/components/pathItems/catsWebhook",
-      "description": "Overriding description",
-      "summary": "Overriding summary"
+      "$ref": "#/components/pathItems/catsWebhook"
     }
   },
   "components": {
+    "schemas": {
+      "Pet": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "integer"
+          },
+          "name": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "id",
+          "name"
+        ]
+      }
+    },
     "pathItems": {
       "catsWebhook": {
         "put": {
diff --git a/openapi/parser/_testdata/negative/null/webhook_pathItem.json b/openapi/parser/_testdata/negative/null/webhook_pathItem.json
new file mode 100644
index 00000000..9241142f
--- /dev/null
+++ b/openapi/parser/_testdata/negative/null/webhook_pathItem.json
@@ -0,0 +1,10 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "webhooks": {
+    "nullWebhook": null
+  }
+}
diff --git a/openapi/parser/_testdata/negative/paths/duplicate_operationId.json b/openapi/parser/_testdata/negative/paths/duplicate_operationId.json
new file mode 100644
index 00000000..5ecf162d
--- /dev/null
+++ b/openapi/parser/_testdata/negative/paths/duplicate_operationId.json
@@ -0,0 +1,29 @@
+{
+  "openapi": "3.0.3",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "paths": {
+    "/users/get": {
+      "get": {
+        "operationId": "usersGet",
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    },
+    "/users/list": {
+      "get": {
+        "operationId": "usersGet",
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/openapi/parser/_testdata/negative/webhook/duplicate_operationId.json b/openapi/parser/_testdata/negative/webhook/duplicate_operationId.json
new file mode 100644
index 00000000..d3c80cc8
--- /dev/null
+++ b/openapi/parser/_testdata/negative/webhook/duplicate_operationId.json
@@ -0,0 +1,27 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "title",
+    "version": "v0.1.0"
+  },
+  "webhooks": {
+    "usersGet": {
+      "get": {
+        "operationId": "usersGet",
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      },
+      "post": {
+        "operationId": "usersGet",
+        "responses": {
+          "200": {
+            "description": "User info"
+          }
+        }
+      }
+    }
+  }
+}