diff --git a/datamodel/low/model_builder_test.go b/datamodel/low/model_builder_test.go index 1989899..3ba6f3d 100644 --- a/datamodel/low/model_builder_test.go +++ b/datamodel/low/model_builder_test.go @@ -9,6 +9,7 @@ import ( type hotdog struct { Name NodeReference[string] + ValueName ValueReference[string] Fat NodeReference[int] Ketchup NodeReference[float32] Mustard NodeReference[float64] @@ -46,6 +47,7 @@ func TestBuildModel_Mismatch(t *testing.T) { func TestBuildModel(t *testing.T) { yml := `name: yummy +valueName: yammy beef: true fat: 200 ketchup: 200.45 @@ -106,9 +108,10 @@ there: hd := hotdog{} cErr := BuildModel(rootNode.Content[0], &hd) assert.Equal(t, 200, hd.Fat.Value) - assert.Equal(t, 3, hd.Fat.ValueNode.Line) + assert.Equal(t, 4, hd.Fat.ValueNode.Line) assert.Equal(t, true, hd.Grilled.Value) assert.Equal(t, "yummy", hd.Name.Value) + assert.Equal(t, "yammy", hd.ValueName.Value) assert.Equal(t, float32(200.45), hd.Ketchup.Value) assert.Len(t, hd.Drinks, 3) assert.Len(t, hd.Sides, 4) @@ -119,7 +122,7 @@ there: assert.Len(t, hd.MaxTempAlt, 5) assert.Equal(t, int64(7392837462032342), hd.MaxTempHigh.Value) assert.Equal(t, 2, hd.Temps[1].Value) - assert.Equal(t, 26, hd.Temps[1].ValueNode.Line) + assert.Equal(t, 27, hd.Temps[1].ValueNode.Line) assert.Len(t, hd.UnknownElements.Value, 2) assert.Len(t, hd.LotsOfUnknowns, 3) assert.Len(t, hd.Where, 2) diff --git a/datamodel/low/v2/swagger_test.go b/datamodel/low/v2/swagger_test.go index 86baff0..169a4bb 100644 --- a/datamodel/low/v2/swagger_test.go +++ b/datamodel/low/v2/swagger_test.go @@ -59,6 +59,7 @@ func TestCreateDocument(t *testing.T) { assert.Equal(t, "http://swagger.io", doc.ExternalDocs.Value.URL.Value) assert.Equal(t, true, doc.FindExtension("x-pet").Value) assert.Equal(t, true, doc.FindExtension("X-Pet").Value) + assert.NotNil(t, doc.GetExternalDocs()) } func TestCreateDocument_Info(t *testing.T) { diff --git a/datamodel/low/v3/components.go b/datamodel/low/v3/components.go index 631f053..0e34fb4 100644 --- a/datamodel/low/v3/components.go +++ b/datamodel/low/v3/components.go @@ -194,7 +194,7 @@ type componentBuildResult[T any] struct { func extractComponentValues[T low.Buildable[N], N any](label string, root *yaml.Node, skip chan bool, errorChan chan<- error, resultChan chan<- low.NodeReference[map[low.KeyReference[string]]low.ValueReference[T]], idx *index.SpecIndex) { - _, nodeLabel, nodeValue := utils.FindKeyNodeFull(label, root.Content) + _, nodeLabel, nodeValue := utils.FindKeyNodeFullTop(label, root.Content) if nodeValue == nil { skip <- true return diff --git a/datamodel/low/v3/components_test.go b/datamodel/low/v3/components_test.go index deb363f..d0fc397 100644 --- a/datamodel/low/v3/components_test.go +++ b/datamodel/low/v3/components_test.go @@ -11,7 +11,8 @@ import ( "testing" ) -var testComponentsYaml = `components: +var testComponentsYaml = ` + x-pizza: crispy schemas: one: description: one of many @@ -97,6 +98,9 @@ func TestComponents_Build_Success(t *testing.T) { assert.Equal(t, "eighteen of many", n.FindCallback("eighteen").Value.FindExpression("{raference}").Value.Post.Value.Description.Value) + assert.Equal(t, "7add1a6c63a354b1a8ffe22552c213fe26d1229beb0b0cbe7c7ca06e63f9a364", + low.GenerateHashString(&n)) + } func TestComponents_Build_Success_Skip(t *testing.T) { @@ -119,7 +123,7 @@ func TestComponents_Build_Success_Skip(t *testing.T) { func TestComponents_Build_Fail(t *testing.T) { - yml := `components: + yml := ` parameters: schema: $ref: '#/this is a problem.'` @@ -140,7 +144,7 @@ func TestComponents_Build_Fail(t *testing.T) { func TestComponents_Build_ParameterFail(t *testing.T) { - yml := `components: + yml := ` parameters: pizza: schema: @@ -162,7 +166,7 @@ func TestComponents_Build_ParameterFail(t *testing.T) { func TestComponents_Build_Fail_TypeFail(t *testing.T) { - yml := `components: + yml := ` parameters: - schema: $ref: #/this is a problem.` @@ -178,7 +182,6 @@ func TestComponents_Build_Fail_TypeFail(t *testing.T) { err = n.Build(idxNode.Content[0], idx) assert.Error(t, err) - } func TestComponents_Build_ExtensionTest(t *testing.T) { @@ -201,3 +204,25 @@ headers: assert.Equal(t, "seagull", n.FindExtension("x-curry").Value) } + +func TestComponents_Build_HashEmpty(t *testing.T) { + + yml := `x-curry: seagull` + + var idxNode yaml.Node + mErr := yaml.Unmarshal([]byte(yml), &idxNode) + assert.NoError(t, mErr) + idx := index.NewSpecIndex(&idxNode) + + var n Components + err := low.BuildModel(&idxNode, &n) + assert.NoError(t, err) + + err = n.Build(idxNode.Content[0], idx) + assert.NoError(t, err) + assert.Equal(t, "seagull", n.FindExtension("x-curry").Value) + + assert.Equal(t, "9cf2c6ab3f9ff7e5231fcb391c8af5c47406711d2ca366533f21a8bb2f67edfe", + low.GenerateHashString(&n)) + +} diff --git a/datamodel/low/v3/create_document_test.go b/datamodel/low/v3/create_document_test.go index cca34bd..2e5797d 100644 --- a/datamodel/low/v3/create_document_test.go +++ b/datamodel/low/v3/create_document_test.go @@ -477,6 +477,9 @@ func TestCreateDocument_Component_Discriminator(t *testing.T) { assert.Equal(t, "drinkType", dsc.PropertyName.Value) assert.Equal(t, "some value", dsc.FindMappingValue("drink").Value) assert.Nil(t, dsc.FindMappingValue("don't exist")) + assert.NotNil(t, doc.GetExternalDocs()) + assert.Nil(t, doc.FindSecurityRequirement("scooby doo")) + } func TestCreateDocument_CheckAdditionalProperties_Schema(t *testing.T) { @@ -518,8 +521,21 @@ components: assert.Error(t, ob.GetBuildError()) } +func TestCreateDocument_Webhooks_Error(t *testing.T) { + yml := `openapi: 3.0 +webhooks: + aHook: + $ref: #bork` + + info, _ := datamodel.ExtractSpecInfo([]byte(yml)) + var err []error + doc, err = CreateDocument(info) + assert.Len(t, err, 1) +} + func TestCreateDocument_Components_Error_Extract(t *testing.T) { - yml := `components: + yml := `openapi: 3.0 +components: parameters: bork: $ref: #bork` @@ -532,7 +548,8 @@ func TestCreateDocument_Components_Error_Extract(t *testing.T) { } func TestCreateDocument_Paths_Errors(t *testing.T) { - yml := `paths: + yml := `openapi: 3.0 +paths: /p: $ref: #bork` @@ -543,7 +560,8 @@ func TestCreateDocument_Paths_Errors(t *testing.T) { } func TestCreateDocument_Tags_Errors(t *testing.T) { - yml := `tags: + yml := `openapi: 3.0 +tags: - $ref: #bork` info, _ := datamodel.ExtractSpecInfo([]byte(yml)) @@ -553,7 +571,8 @@ func TestCreateDocument_Tags_Errors(t *testing.T) { } func TestCreateDocument_Security_Error(t *testing.T) { - yml := `security: + yml := `openapi: 3.0 +security: $ref: #bork` info, _ := datamodel.ExtractSpecInfo([]byte(yml)) @@ -563,7 +582,8 @@ func TestCreateDocument_Security_Error(t *testing.T) { } func TestCreateDocument_ExternalDoc_Error(t *testing.T) { - yml := `externalDocs: + yml := `openapi: 3.0 +externalDocs: $ref: #bork` info, _ := datamodel.ExtractSpecInfo([]byte(yml)) diff --git a/test_specs/burgershop.openapi-modified.yaml b/test_specs/burgershop.openapi-modified.yaml new file mode 100644 index 0000000..fcfdfae --- /dev/null +++ b/test_specs/burgershop.openapi-modified.yaml @@ -0,0 +1,548 @@ +openapi: 3.1.0 +info: + title: Burger Shop + description: | + The best burger API at princess beef. You can find the testiest burgers in the world + termsOfService: https://pb33f.io + contact: + name: pb33f + email: buckaroo@pb33f.io + url: https://pb33f.io + license: + name: pb33f + url: https://pb33f.io/made-up + version: "1.2" +security: + - OAuthScheme: + - read:burgers + - write:burgers +tags: + - name: "Burgers" + description: "All kinds of yummy burgers." + externalDocs: + description: "Find out more" + url: "https://pb33f.io" + x-internal-ting: somethingSpecial + x-internal-tong: 1 + x-internal-tang: 1.2 + x-internal-tung: true + x-internal-arr: + - one + - two + x-internal-arrmap: + - what: now + - why: that + x-something-else: + ok: + - what: now? + - name: "Dressing" + description: "Variety of dressings: cheese, veggie, oil and a lot more" + externalDocs: + description: "Find out more information about our products)" + url: "https://pb33f.io" +servers: + - url: "{scheme}://api.pb33f.io" + description: "this is our main API server, for all fun API things." + variables: + scheme: + enum: [https, wss] + default: https + description: this is a server variable for the scheme + - url: "https://{domain}.{host}.com" + description: "this is our second API server, for all fun API things." + variables: + domain: + default: "api" + description: the default API domain is 'api' + host: + default: "pb33f.io" + description: the default host for this API is 'pb33f.io' +paths: + x-milky-milk: milky + /burgers: + x-burger-meta: meaty + post: + operationId: createBurger + tags: + - "Burgers" + summary: Create a new burger + description: A new burger for our menu, yummy yum yum. + requestBody: + $ref: '#/components/requestBodies/BurgerRequest' + responses: + "200": + headers: + UseOil: + $ref: '#/components/headers/UseOil' + description: A tasty burger for you to eat. + content: + application/json: + schema: + $ref: '#/components/schemas/Burger' + examples: + quarterPounder: + $ref: '#/components/examples/QuarterPounder' + filetOFish: + summary: a cripsy fish sammich filled with ocean goodness. + value: + name: Filet-O-Fish + numPatties: 1 + links: + LocateBurger: + $ref: '#/components/links/LocateBurger' + AnotherLocateBurger: + $ref: '#/components/links/AnotherLocateBurger' + "500": + description: Unexpected error creating a new burger. Sorry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + unexpectedError: + summary: oh my goodness + value: + message: something went terribly wrong my friend, no new burger for you. + "422": + description: Unprocessable entity + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + unexpectedError: + summary: invalid request + value: + message: unable to accept this request, looks bad, missing something. + security: + - OAuthScheme: + - read:burgers + - write:burgers + servers: + - url: https://pb33f.io + description: this is an alternative server for this operation. + /burgers/{burgerId}: + get: + callbacks: + burgerCallback: + $ref: '#/components/callbacks/BurgerCallback' + operationId: locateBurger + tags: + - "Burgers" + summary: Search a burger by ID - returns the burger with that identifier + description: Look up a tasty burger take it and enjoy it + parameters: + - $ref: '#/components/parameters/BurgerId' + - $ref: '#/components/parameters/BurgerHeader' + responses: + "200": + description: A tasty burger for you to eat. Wide variety of products to choose from + content: + application/json: + schema: + $ref: '#/components/schemas/Burger' + examples: + quarterPounder: + $ref: '#/components/examples/QuarterPounder' + filetOFish: + summary: A tasty treat from the sea + value: + name: Filet-O-Fish + numPatties: 1 + links: + ListBurgerDressings: + operationId: listBurgerDressings + parameters: + dressingId: 'something here' + description: 'Try the ketchup!' + "404": + description: Cannot find your burger. Sorry. We may have sold out of this type + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + notFound: + summary: burger missing + value: + message: can't find a burger with that ID, we may have sold out my friend. + "500": + description: Unexpected error. Sorry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + examples: + unexpectedError: + summary: oh my stars + value: + message: something went terribly wrong my friend, burger location crashed! + /burgers/{burgerId}/dressings: + get: + operationId: listBurgerDressings + tags: + - "Dressing" + summary: Get a list of all dressings available + description: Same as the summary, look up a tasty burger, by its ID - the burger identifier + parameters: + - in: path + name: burgerId + schema: + type: string + example: big-mac + description: the name of the our fantastic burger. You can pick a name from our menu + required: true + responses: + "200": + $ref: '#/components/responses/DressingResponse' + "404": + description: Cannot find your burger in which to list dressings. Sorry + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: There is no burger here + "500": + description: Unexpected error listing dressings for burger. Sorry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: computer says no dressings for this burger. + /dressings/{dressingId}: + get: + operationId: getDressing + tags: + - "Dressing" + summary: Get a specific dressing - you can choose the dressing from our menu + description: Same as the summary, get a dressing, by its ID + parameters: + - in: path + name: dressingId + schema: + type: string + example: cheese + description: This is the unique identifier for the dressing items. + required: true + responses: + "200": + description: a dressing + content: + application/json: + schema: + $ref: '#/components/schemas/Dressing' + example: + name: Butter Sauce + "404": + description: Cannot find your dressing, sorry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: No such dressing as 'Pizza' + "500": + description: Unexpected error getting a dressing. Sorry. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: failed looking up dressing by ID, our server borked. + /dressings: + get: + operationId: getAllDressings + tags: + - "Dressing" + summary: Get all dressings available in our store + description: Get all dressings and choose from them + responses: + "200": + description: an array of dressings + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Dressing' + example: + - name: Burger Sauce + "418": + description: I am a teapot. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: It's teapot time. + "500": + description: Something went wrong with getting dressings. + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + example: + message: "failed looking up all dressings, something went wrong." +components: + callbacks: + BurgerCallback: + x-break-everything: please + "{$request.query.queryUrl}": + post: + requestBody: + description: Callback payload + content: + 'application/json': + schema: + $ref: '#/components/schemas/SomePayload' + responses: + '200': + description: callback successfully processes + links: + LocateBurger: + operationId: locateBurger + parameters: + burgerId: '$response.body#/id' + description: Go and get a tasty burger + AnotherLocateBurger: + operationId: locateBurger + parameters: + burgerId: '$response.body#/id' + description: Go and get another really tasty burger + server: + url: https://pb33f.io + headers: + UseOil: + description: this is a header example for UseOil + schema: + type: string + requestBodies: + BurgerRequest: + description: Give us the new burger! + content: + application/json: + schema: + $ref: '#/components/schemas/Burger' + examples: + pbjBurger: + summary: A horrible, nutty, sticky mess. + value: + name: Peanut And Jelly + numPatties: 3 + cakeBurger: + summary: A sickly, sweet, atrocity + value: + name: Chocolate Cake Burger + numPatties: 5 + examples: + QuarterPounder: + summary: A juicy two hander sammich + value: + name: Quarter Pounder with Cheese + numPatties: 1 + responses: + DressingResponse: + description: all the dressings for a burger. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Dressing' + example: + - name: Thousand Island + securitySchemes: + APIKeyScheme: + type: apiKey + description: an apiKey security scheme + name: apiKeyScheme + in: query + JWTScheme: + type: http + description: an JWT security scheme + name: aJWTThing + scheme: bearer + bearerFormat: JWT + OAuthScheme: + type: oauth2 + description: an oAuth security scheme + name: oAuthy + flows: + implicit: + authorizationUrl: https://pb33f.io/oauth + scopes: + write:burgers: modify and add new burgers + read:burgers: read all burgers + authorizationCode: + authorizationUrl: https://pb33f.io/oauth + tokenUrl: https://api.pb33f.io/oauth/token + scopes: + write:burgers: modify burgers and stuff + read:burgers: read all the burgers + parameters: + BurgerHeader: + in: header + name: burgerHeader + schema: + properties: + burgerTheme: + type: string + description: something about a theme goes in here? + burgerTime: + type: number + description: number of burgers ordered so far this year. + example: big-mac + description: the name of the burger. use this to order your food + required: true + content: + application/json: + example: somethingNice + encoding: + burgerTheme: + contentType: text/plain + headers: + someHeader: + description: this is a header + schema: + type: string + schema: + type: object + required: [burgerTheme, burgerTime] + properties: + burgerTheme: + type: string + description: something about a theme? + burgerTime: + type: number + description: number of burgers ordered this year. + BurgerId: + in: path + name: burgerId + schema: + type: string + example: big-mac + description: the name of the burger. use this to order your tasty burger + required: true + schemas: + Error: + type: object + description: Error defining what went wrong when providing a specification. The message should help indicate the issue clearly. + properties: + message: + type: string + description: returns the error message if something wrong happens + example: No such burger as 'Big-Whopper' + Burger: + type: object + description: The tastiest food on the planet you would love to eat everyday + required: + - name + - numPatties + properties: + name: + type: string + description: The name of your tasty burger - burger names are listed in our menus + example: Big Mac + numPatties: + type: integer + description: The number of burger patties used + example: 2 + numTomatoes: + type: integer + description: how many slices of orange goodness would you like? + example: 1 + fries: + $ref: '#/components/schemas/Fries' + Fries: + type: object + description: golden slices of happy fun joy + required: + - potatoShape + - favoriteDrink + properties: + seasoning: + type: array + description: herbs and spices for your golden joy + items: + type: string + description: type of herb or spice used to liven up the yummy + example: salt + potatoShape: + type: string + description: what type of potato shape? wedges? shoestring? + example: Crispy Shoestring + favoriteDrink: + $ref: '#/components/schemas/Drink' + Dressing: + type: object + description: This is the object that contains the information about the content of the dressing + required: + - name + properties: + name: + type: string + description: The name of your dressing you can pick up from the menu + example: Cheese + additionalProperties: + type: object + description: something in here. + Drink: + type: object + description: a frosty cold beverage can be coke or sprite + required: + - size + - drinkType + properties: + ice: + type: boolean + drinkType: + description: select from coke or sprite + enum: + - coke + - sprite + size: + type: string + description: what size man? S/M/L + example: M + additionalProperties: true + discriminator: + propertyName: drinkType + mapping: + drink: some value + SomePayload: + type: string + description: some kind of payload for something. + xml: + name: is html programming? yes. + externalDocs: + url: https://pb33f.io/docs + oneOf: + - $ref: '#/components/schemas/Drink' + anyOf: + - $ref: '#/components/schemas/Drink' + allOf: + - $ref: '#/components/schemas/Drink' + not: + type: string + items: + - $ref: '#/components/schemas/Drink' + x-screaming-baby: loud +x-something-something: darkside +externalDocs: + description: "Find out more information about our products and services" + url: "https://pb33f.io" +jsonSchemaDialect: https://pb33f.io/schema +webhooks: + someHook: + post: + requestBody: + description: Information about a new burger + content: + application/json: + schema: + $ref: "#/components/schemas/Burger" + responses: + "200": + description: the hook is good! you have a new burger. \ No newline at end of file diff --git a/test_specs/circular-tests.yaml b/test_specs/circular-tests.yaml index 815e8e6..1b1cb14 100644 --- a/test_specs/circular-tests.yaml +++ b/test_specs/circular-tests.yaml @@ -31,7 +31,7 @@ components: yester: "$ref": "#/components/schemas/Seven" Four: - desription: "test four" + description: "test four" properties: lemons: "$ref": "#/components/schemas/Nine" diff --git a/what-changed/model/components.go b/what-changed/model/components.go index c66896f..4cea55b 100644 --- a/what-changed/model/components.go +++ b/what-changed/model/components.go @@ -100,10 +100,6 @@ func CompareComponents(l, r any) *ComponentsChanges { lComponents := l.(*v3.Components) rComponents := r.(*v3.Components) - if lComponents == nil && rComponents == nil { - return nil - } - if low.AreEqual(lComponents, rComponents) { return nil } diff --git a/what-changed/model/document.go b/what-changed/model/document.go index e430104..7485af5 100644 --- a/what-changed/model/document.go +++ b/what-changed/model/document.go @@ -203,6 +203,14 @@ func CompareDocuments(l, r any) *DocumentChanges { dc.ComponentsChanges = n } } + if !lDoc.Components.IsEmpty() && rDoc.Components.IsEmpty() { + CreateChange(&changes, PropertyRemoved, v3.ComponentsLabel, + lDoc.Components.ValueNode, nil, true, lDoc.Components.Value, nil) + } + if lDoc.Components.IsEmpty() && !rDoc.Components.IsEmpty() { + CreateChange(&changes, PropertyAdded, v3.ComponentsLabel, + rDoc.Components.ValueNode, nil, false, nil, lDoc.Components.Value) + } // compare servers if n := checkServers(lDoc.Servers, rDoc.Servers, &changes); n != nil { @@ -210,7 +218,10 @@ func CompareDocuments(l, r any) *DocumentChanges { } // compare webhooks - CheckMapForChanges(lDoc.Webhooks.Value, rDoc.Webhooks.Value, &changes, v3.WebhooksLabel, ComparePathItemsV3) + dc.WebhookChanges = CheckMapForChanges(lDoc.Webhooks.Value, rDoc.Webhooks.Value, &changes, + v3.WebhooksLabel, ComparePathItemsV3) + + // extensions dc.ExtensionChanges = CompareExtensions(lDoc.Extensions, rDoc.Extensions) } diff --git a/what-changed/model/document_test.go b/what-changed/model/document_test.go index a997d1f..09d7401 100644 --- a/what-changed/model/document_test.go +++ b/what-changed/model/document_test.go @@ -216,6 +216,8 @@ externalDocs: assert.Equal(t, 0, extChanges.TotalBreakingChanges()) assert.Equal(t, v3.ExternalDocsLabel, extChanges.Changes[0].Property) assert.Equal(t, PropertyAdded, extChanges.Changes[0].ChangeType) + assert.NotNil(t, lDoc.GetExternalDocs()) + } func TestCompareDocuments_Swagger_ExternalDocs_Removed(t *testing.T) { @@ -724,6 +726,8 @@ jsonSchemaDialect: https://pb33f.io/schema` // compare. extChanges := CompareDocuments(&lDoc, &rDoc) assert.Nil(t, extChanges) + assert.NotNil(t, lDoc.GetExternalDocs()) + assert.Nil(t, lDoc.FindSecurityRequirement("chewy")) // because why not. } func TestCompareDocuments_OpenAPI_BaseProperties_Modified(t *testing.T) { @@ -753,3 +757,240 @@ jsonSchemaDialect: https://pb33f.io/schema/changed` assert.Equal(t, 3, extChanges.TotalChanges()) assert.Equal(t, 2, extChanges.TotalBreakingChanges()) } + +func TestCompareDocuments_OpenAPI_AddComponents(t *testing.T) { + + left := `openapi: 3.1` + + right := `openapi: 3.1 +components: + schemas: + thing: + type: int` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) + assert.Equal(t, PropertyAdded, extChanges.Changes[0].ChangeType) +} + +func TestCompareDocuments_OpenAPI_Removed(t *testing.T) { + + left := `openapi: 3.1` + + right := `openapi: 3.1 +components: + schemas: + thing: + type: int` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(rDoc, lDoc) + + assert.Equal(t, 1, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) + assert.Equal(t, PropertyRemoved, extChanges.Changes[0].ChangeType) +} + +func TestCompareDocuments_OpenAPI_ModifyPaths(t *testing.T) { + + left := `openapi: 3.1 +paths: + /brown/cow: + get: + description: brown cow + /brown/hen: + get: + description: brown hen` + + right := `openapi: 3.1 +paths: + /brown/cow: + get: + description: brown cow modified + /brown/hen: + get: + description: brown hen modified` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) +} + +func TestCompareDocuments_OpenAPI_Identical_Security(t *testing.T) { + + left := `openapi: 3.1 +security: + - cakes: + - chocolate + - vanilla + - shoes: + - white + - black` + + right := `openapi: 3.1 +security: + - shoes: + - black + - white + - cakes: + - vanilla + - chocolate ` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + assert.Nil(t, extChanges) +} + +func TestCompareDocuments_OpenAPI_ModifyComponents(t *testing.T) { + + left := `openapi: 3.1 +components: + schemas: + athing: + description: a schema + nothing: + description: nothing` + + right := `openapi: 3.1 +components: + schemas: + athing: + description: a schema that changed + nothing: + description: nothing with an update` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) +} + +func TestCompareDocuments_OpenAPI_ModifyServers(t *testing.T) { + + left := `openapi: 3.1 +servers: + - url: https://pb33f.io + - url: https://quobix.com` + + right := `openapi: 3.1 +servers: + - url: https://pb33f.io + description: hello! + - url: https://api.pb33f.io` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + + assert.Equal(t, 3, extChanges.TotalChanges()) + assert.Equal(t, 1, extChanges.TotalBreakingChanges()) +} + +func TestCompareDocuments_OpenAPI_ModifyWebhooks(t *testing.T) { + + left := `openapi: 3.1 +webhooks: + bHook: + get: + description: coffee + aHook: + get: + description: jazz` + + right := `openapi: 3.1 +webhooks: + bHook: + get: + description: coffee in the morning + aHook: + get: + description: jazz in the evening` + + var lNode, rNode yaml.Node + _ = yaml.Unmarshal([]byte(left), &lNode) + _ = yaml.Unmarshal([]byte(right), &rNode) + + // have to build docs fully to get access to objects + siLeft, _ := datamodel.ExtractSpecInfo([]byte(left)) + siRight, _ := datamodel.ExtractSpecInfo([]byte(right)) + + lDoc, _ := v3.CreateDocument(siLeft) + rDoc, _ := v3.CreateDocument(siRight) + + // compare. + extChanges := CompareDocuments(lDoc, rDoc) + + assert.Equal(t, 2, extChanges.TotalChanges()) + assert.Equal(t, 0, extChanges.TotalBreakingChanges()) +} diff --git a/what-changed/model/tags_test.go b/what-changed/model/tags_test.go index 9da63ae..bf8d37a 100644 --- a/what-changed/model/tags_test.go +++ b/what-changed/model/tags_test.go @@ -265,3 +265,59 @@ tags: assert.Nil(t, changes) } + +func TestCompareTags_AddExternalDocs(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - name: something else` + + right := `openapi: 3.0.1 +tags: + - name: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(lDoc.Tags.Value, rDoc.Tags.Value) + + // evaluate. + assert.Equal(t, 1, changes.TotalChanges()) + assert.Equal(t, ObjectAdded, changes.Changes[0].ChangeType) + +} + +func TestCompareTags_RemoveExternalDocs(t *testing.T) { + + left := `openapi: 3.0.1 +tags: + - name: something else` + + right := `openapi: 3.0.1 +tags: + - name: something else + externalDocs: + url: https://pb33f.io + description: cooler` + + // create document (which will create our correct tags low level structures) + lInfo, _ := datamodel.ExtractSpecInfo([]byte(left)) + rInfo, _ := datamodel.ExtractSpecInfo([]byte(right)) + lDoc, _ := lowv3.CreateDocument(lInfo) + rDoc, _ := lowv3.CreateDocument(rInfo) + + // compare. + changes := CompareTags(rDoc.Tags.Value, lDoc.Tags.Value) + + // evaluate. + assert.Equal(t, 1, changes.TotalChanges()) + assert.Equal(t, ObjectRemoved, changes.Changes[0].ChangeType) + +} diff --git a/what-changed/what_changed.go b/what-changed/what_changed.go index 6e78fbd..6c6d068 100644 --- a/what-changed/what_changed.go +++ b/what-changed/what_changed.go @@ -3,3 +3,16 @@ package what_changed +import ( + "github.com/pb33f/libopenapi/datamodel/low/v2" + "github.com/pb33f/libopenapi/datamodel/low/v3" + "github.com/pb33f/libopenapi/what-changed/model" +) + +func CompareOpenAPIDocuments(original, updated *v3.Document) *model.DocumentChanges { + return model.CompareDocuments(original, updated) +} + +func CompareSwaggerDocuments(original, updated *v2.Swagger) *model.DocumentChanges { + return model.CompareDocuments(original, updated) +} diff --git a/what-changed/what_changed_test.go b/what-changed/what_changed_test.go index 0a95109..9726507 100644 --- a/what-changed/what_changed_test.go +++ b/what-changed/what_changed_test.go @@ -2,3 +2,19 @@ // SPDX-License-Identifier: MIT package what_changed + +//func TestCompareOpenAPIDocuments(t *testing.T) { +// +// original, _ := ioutil.ReadFile("../test_specs/burgershop.openapi.yaml") +// modified, _ := ioutil.ReadFile("../test_specs/burgershop.openapi-modified.yaml") +// infoOrig, _ := datamodel.ExtractSpecInfo(original) +// infoMod, _ := datamodel.ExtractSpecInfo(modified) +// +// origDoc, _ := v3.CreateDocument(infoOrig) +// modDoc, _ := v3.CreateDocument(infoMod) +// +// changes := CompareOpenAPIDocuments(origDoc, modDoc) +// +// assert.Nil(t, changes) +// +//}