diff --git a/collator.go b/collator.go index cafd16b9..162f7976 100644 --- a/collator.go +++ b/collator.go @@ -272,20 +272,29 @@ func (c *Collator) mergePaths(rv *ResourceVersion) error { } var errs error for k, v := range rv.T.Paths { - route := routeForPath(k) - if _, ok := c.seenRoutes[route]; ok { - if c.useFirstRoute { - continue + for opName, opValue := range v.Operations() { + route := routeForPath(k, opName) + if _, ok := c.seenRoutes[route]; ok { + if c.useFirstRoute { + continue + } else { + errs = multierr.Append( + errs, + fmt.Errorf("conflict in #/paths %s: declared in both %s and %s", k, rv.path, c.pathSources[k]), + ) + } } else { - errs = multierr.Append( - errs, - fmt.Errorf("conflict in #/paths %s: declared in both %s and %s", k, rv.path, c.pathSources[k]), - ) + c.seenRoutes[route] = struct{}{} + if c.result.Paths[k] == nil { + // Path doesn't exist in output + c.result.Paths[k] = v + } else { + // There is another operation on this path, merge the + // current operation into that one + c.result.Paths[k].SetOperation(opName, opValue) + } + c.pathSources[k] = rv.path } - } else { - c.seenRoutes[route] = struct{}{} - c.result.Paths[k] = v - c.pathSources[k] = rv.path } } return errs @@ -293,6 +302,6 @@ func (c *Collator) mergePaths(rv *ResourceVersion) error { var routeForPathRE = regexp.MustCompile(`\{[^}]*\}`) -func routeForPath(path string) string { - return routeForPathRE.ReplaceAllString(path, "{}") +func routeForPath(path, operation string) string { + return fmt.Sprintf("%s %s", operation, routeForPathRE.ReplaceAllString(path, "{}")) } diff --git a/collator_test.go b/collator_test.go index 23df379a..3e09de30 100644 --- a/collator_test.go +++ b/collator_test.go @@ -217,3 +217,31 @@ func TestCollateMergingResources(t *testing.T) { result := collator.Result() c.Assert(result.Paths["/orgs/{org_id}/projects/{project_id}"].Delete.Responses["204"], qt.IsNotNil) } + +func TestCollateOperationsOnSamePath(t *testing.T) { + c := qt.New(t) + collator := vervet.NewCollator(vervet.UseFirstRoute(true)) + examples1, err := vervet.LoadResourceVersions(testdata.Path("operation-change/_examples")) + c.Assert(err, qt.IsNil) + examples1v, err := examples1.At("2021-06-15~experimental") + c.Assert(err, qt.IsNil) + + examples2, err := vervet.LoadResourceVersions(testdata.Path("operation-change/_examples2")) + c.Assert(err, qt.IsNil) + examples2v, err := examples2.At("2021-06-15~experimental") + c.Assert(err, qt.IsNil) + + err = collator.Collate(examples1v) + c.Assert(err, qt.IsNil) + err = collator.Collate(examples2v) + c.Assert(err, qt.IsNil) + + result := collator.Result() + + c.Assert(result.Paths["/examples/hello-world"].Get, qt.Not(qt.IsNil)) + c.Assert(result.Paths["/examples/hello-world"].Get.Description, qt.Contains, " - from example 1") + c.Assert(result.Paths["/examples/hello-world"].Post, qt.Not(qt.IsNil)) + c.Assert(result.Paths["/examples/hello-world"].Post.Description, qt.Contains, " - from example 1") + c.Assert(result.Paths["/examples/hello-world"].Put, qt.Not(qt.IsNil)) + c.Assert(result.Paths["/examples/hello-world"].Put.Description, qt.Contains, " - from example 2") +} diff --git a/internal/scraper/gcs_scraper_test.go b/internal/scraper/gcs_scraper_test.go index 1e723957..8c63a905 100644 --- a/internal/scraper/gcs_scraper_test.go +++ b/internal/scraper/gcs_scraper_test.go @@ -24,8 +24,8 @@ func TestGCSScraper(t *testing.T) { tests := []struct { service, version, digest string }{ - {"petfood", "2021-09-01", "sha256:I20cAQ3VEjDrY7O0B678yq+0pYN2h3sxQy7vmdlo4+w="}, - {"animals", "2021-10-16", "sha256:P1FEFvnhtxJSqXr/p6fMNKE+HYwN6iwKccBGHIVZbyg="}, + {"petfood", "2021-09-01", "sha256:zCgJaPeR8R21wsAlYn46xO6NE3XJiyFtLnYrP4DpM3U="}, + {"animals", "2021-10-16", "sha256:hcv2i7awT6CcSCecw9WrYBokFyzYNVaQArGgqHqdj7s="}, } cfg := &config.ServerConfig{ @@ -95,8 +95,8 @@ func TestGCSScraperCollation(t *testing.T) { tests := []struct { service, version, digest string }{ - {"petfood", "2021-09-01", "sha256:I20cAQ3VEjDrY7O0B678yq+0pYN2h3sxQy7vmdlo4+w="}, - {"animals", "2021-10-16", "sha256:P1FEFvnhtxJSqXr/p6fMNKE+HYwN6iwKccBGHIVZbyg="}, + {"petfood", "2021-09-01", "sha256:zCgJaPeR8R21wsAlYn46xO6NE3XJiyFtLnYrP4DpM3U="}, + {"animals", "2021-10-16", "sha256:hcv2i7awT6CcSCecw9WrYBokFyzYNVaQArGgqHqdj7s="}, } cfg := &config.ServerConfig{ diff --git a/internal/scraper/s3_scraper_test.go b/internal/scraper/s3_scraper_test.go index e5efb99d..32c82ace 100644 --- a/internal/scraper/s3_scraper_test.go +++ b/internal/scraper/s3_scraper_test.go @@ -24,8 +24,8 @@ func TestS3Scraper(t *testing.T) { tests := []struct { name, version, digest string }{ - {"petfood", "2021-09-01", "sha256:I20cAQ3VEjDrY7O0B678yq+0pYN2h3sxQy7vmdlo4+w="}, - {"animals", "2021-10-16", "sha256:P1FEFvnhtxJSqXr/p6fMNKE+HYwN6iwKccBGHIVZbyg="}, + {"petfood", "2021-09-01", "sha256:zCgJaPeR8R21wsAlYn46xO6NE3XJiyFtLnYrP4DpM3U="}, + {"animals", "2021-10-16", "sha256:hcv2i7awT6CcSCecw9WrYBokFyzYNVaQArGgqHqdj7s="}, } cfg := &config.ServerConfig{ @@ -90,9 +90,9 @@ func TestS3ScraperCollation(t *testing.T) { tests := []struct { name, version, digest string }{{ - "petfood", "2021-09-01", "sha256:I20cAQ3VEjDrY7O0B678yq+0pYN2h3sxQy7vmdlo4+w=", + "petfood", "2021-09-01", "sha256:zCgJaPeR8R21wsAlYn46xO6NE3XJiyFtLnYrP4DpM3U=", }, { - "animals", "2021-10-16", "sha256:P1FEFvnhtxJSqXr/p6fMNKE+HYwN6iwKccBGHIVZbyg=", + "animals", "2021-10-16", "sha256:hcv2i7awT6CcSCecw9WrYBokFyzYNVaQArGgqHqdj7s=", }} cfg := &config.ServerConfig{ diff --git a/internal/scraper/scraper_test.go b/internal/scraper/scraper_test.go index 6ae4e823..c326218b 100644 --- a/internal/scraper/scraper_test.go +++ b/internal/scraper/scraper_test.go @@ -31,16 +31,16 @@ var ( petfood = &testService{ versions: []string{"2021-09-01", "2021-09-16"}, contents: map[string]string{ - "2021-09-01": `{"paths":{"/crickets": {}}}`, - "2021-09-16": `{"paths":{"/crickets": {}, "/kibble": {}}}`, + "2021-09-01": `{"paths":{"/crickets": {"get": {}}}}`, + "2021-09-16": `{"paths":{"/crickets": {"get": {}}, "/kibble": {"get": {}}}}`, }, } animals = &testService{ versions: []string{"2021-01-01", "2021-10-01", "2021-10-16"}, contents: map[string]string{ - "2021-01-01": `{"paths":{"/legacy": {}}}`, - "2021-10-01": `{"paths":{"/geckos": {}}}`, - "2021-10-16": `{"paths":{"/geckos": {}, "/puppies": {}}}`, + "2021-01-01": `{"paths":{"/legacy": {"get": {}}}}`, + "2021-10-01": `{"paths":{"/geckos": {"get": {}}}}`, + "2021-10-16": `{"paths":{"/geckos": {"get": {}}, "/puppies": {"get": {}}}}`, }, } ) @@ -85,8 +85,8 @@ func TestScraper(t *testing.T) { tests := []struct { name, version, digest string }{ - {"petfood", "2021-09-01", "sha256:I20cAQ3VEjDrY7O0B678yq+0pYN2h3sxQy7vmdlo4+w="}, - {"animals", "2021-10-16", "sha256:P1FEFvnhtxJSqXr/p6fMNKE+HYwN6iwKccBGHIVZbyg="}, + {"petfood", "2021-09-01", "sha256:zCgJaPeR8R21wsAlYn46xO6NE3XJiyFtLnYrP4DpM3U="}, + {"animals", "2021-10-16", "sha256:hcv2i7awT6CcSCecw9WrYBokFyzYNVaQArGgqHqdj7s="}, } cfg := &config.ServerConfig{ diff --git a/testdata/operation-change/_examples/2021-06-15/spec.yaml b/testdata/operation-change/_examples/2021-06-15/spec.yaml new file mode 100644 index 00000000..0d049f6d --- /dev/null +++ b/testdata/operation-change/_examples/2021-06-15/spec.yaml @@ -0,0 +1,52 @@ +--- +openapi: 3.0.3 +x-snyk-api-stability: beta +info: + title: Registry + version: 3.0.0 +servers: + - url: /api/v3 + description: Snyk Registry +paths: + /examples/hello-world: + post: + description: Create a single result from the hello-world example - from example 1 + operationId: helloWorldCreate + requestBody: + content: + application/vnd.api+json: + schema: + type: object + properties: + attributes: + type: object + properties: + message: + type: string + betaField: + type: string + additionalProperties: false + required: ['message', 'betaField'] + additionalProperties: false + required: ['attributes'] + responses: + '201': + description: 'A hello world entity being requested is returned' + content: + application/vnd.api+json: + schema: + type: object + required: ['jsonapi', 'data', 'links'] + additionalProperties: false + get: + description: Get a list of hello-worlds example - from example 1 + operationId: helloWorldGetList + responses: + '200': + description: 'A hello world entity being requested is returned' + content: + application/vnd.api+json: + schema: + type: object + required: ['jsonapi', 'data', 'links'] + additionalProperties: false diff --git a/testdata/operation-change/_examples2/2021-06-15/spec.yaml b/testdata/operation-change/_examples2/2021-06-15/spec.yaml new file mode 100644 index 00000000..f0100be5 --- /dev/null +++ b/testdata/operation-change/_examples2/2021-06-15/spec.yaml @@ -0,0 +1,69 @@ +--- +openapi: 3.0.3 +x-snyk-api-stability: beta +info: + title: Registry + version: 3.0.0 +servers: + - url: /api/v3 + description: Snyk Registry +paths: + /examples/hello-world: + post: + description: Create a single result from the hello-world example - from example 2 + operationId: helloWorldCreate + requestBody: + content: + application/vnd.api+json: + schema: + type: object + properties: + attributes: + type: object + properties: + message: + type: string + betaField: + type: string + additionalProperties: false + required: ['message', 'betaField'] + additionalProperties: false + required: ['attributes'] + responses: + '201': + description: 'A hello world entity being requested is returned' + content: + application/vnd.api+json: + schema: + type: object + required: ['jsonapi', 'data', 'links'] + additionalProperties: false + put: + description: Force create a hello-world example - from example 2 + operationId: helloWorldForceCreate + requestBody: + content: + application/vnd.api+json: + schema: + type: object + properties: + attributes: + type: object + properties: + message: + type: string + betaField: + type: string + additionalProperties: false + required: ['message', 'betaField'] + additionalProperties: false + required: ['attributes'] + responses: + '201': + description: 'A hello world entity being requested is returned' + content: + application/vnd.api+json: + schema: + type: object + required: ['jsonapi', 'data', 'links'] + additionalProperties: false