diff --git a/.gitignore b/.gitignore index 618c6c87..6da7e20b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ testDiff.md testDeprecatedApi.html testNewApi.html + +.vscode/ + +.DS_Store diff --git a/Example/petstore_v3_testResponseRender.html b/Example/petstore_v3_testResponseRender.html new file mode 100644 index 00000000..021d6e31 --- /dev/null +++ b/Example/petstore_v3_testResponseRender.html @@ -0,0 +1 @@ +Changelog

Changelog

Versions


Changes from 1.0 to 1.1.

What's New


  1. GET/admin/item
  2. POST/admin/item
  3. GET/item

What's Deprecated


    What's Changed


    1. POST/item
      • Return Type

        • Change id (Data Type)
      • Produces

        • Add application/xml
      • Responses

        • Add response 201//created
        • Add response 400//bad request
        • Delete response 404//bad request
        • Response 200
          • Description ok change into update Success
          • Change id (Data Type)
          • Add content-type application/xml
          • Add header X-New-Header
          • Delete header X-Deprecated-Header
          • Change header X-Rate-Limit
    \ No newline at end of file diff --git a/Example/testDiff.json b/Example/testDiff.json new file mode 100644 index 00000000..50933396 --- /dev/null +++ b/Example/testDiff.json @@ -0,0 +1 @@ +{"changedEndpoints":[{"changedOperations":{"PUT":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newFeild","newEnums":false,"property":{"description":"a feild demo by sayi","example":"a feild demo by sayi","exampleSetFlag":true,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"description":"a new user feild demo","exampleSetFlag":false,"extensions":{},"format":"int32","specVersion":"V30","type":"integer","types":["integer"]},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"category.name","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Update an existing pet"},"POST":{"addConsumes":[],"addParameters":[{"description":"add new query param demo","explode":true,"extensions":{},"in":"query","name":"tags","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[0].changedOperations.POST.addParameters[0].extensions"},"items":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"specVersion":"V30","type":"array","types":["array"]},"style":"FORM"}],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":true,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Add a new pet to the store"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/pet"},{"changedOperations":{"GET":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[{"el":"newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[{"addContentTypes":[],"addHeaders":{},"addProps":[{"el":"newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"changedHeaders":[],"changedProps":[],"contentTypeChanged":false,"descriptionChanged":false,"diff":true,"headersChanged":false,"missingContentTypes":[],"missingHeaders":{},"missingProps":[{"el":"category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"newDescription":"successful operation","oldDescription":"successful operation","schemaChanged":true,"statusCode":"200"}],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":true,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[{"el":"category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingRequestProps":[],"missingResponses":{},"summary":"Finds Pets by status"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/pet/findByStatus"},{"changedOperations":{"GET":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[{"el":"newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[{"addContentTypes":[],"addHeaders":{},"addProps":[{"el":"newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"owner.newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"changedHeaders":[],"changedProps":[],"contentTypeChanged":false,"descriptionChanged":false,"diff":true,"headersChanged":false,"missingContentTypes":[],"missingHeaders":{},"missingProps":[{"el":"category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"newDescription":"successful operation","oldDescription":"successful operation","schemaChanged":true,"statusCode":"200"}],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":true,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[{"el":"category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"owner.phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingRequestProps":[],"missingResponses":{},"summary":"Finds Pets by tags"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/pet/findByTags"},{"changedOperations":{"DELETE":{"addConsumes":[],"addParameters":[{"extensions":{},"in":"header","name":"newHeaderParam","schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[3].changedOperations.DELETE.addParameters[0].extensions"},"specVersion":"V30","type":"string","types":["string"]}}],"addProduces":[],"addProps":[],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":true,"diffProduces":false,"diffProp":false,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[],"missingResponses":{},"summary":"Deletes a pet"}},"diff":true,"missingOperations":{"POST":{"extensions":{},"operationId":"updatePetWithForm","parameters":[{"description":"ID of pet that needs to be updated","extensions":{},"in":"path","name":"petId","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[3].missingOperations.POST.parameters[0].extensions"},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]}}],"requestBody":{"content":{"application/x-www-form-urlencoded":{"exampleSetFlag":false,"schema":{"exampleSetFlag":false,"properties":{"name":{"description":"Updated name of the pet","exampleSetFlag":false,"extensions":{},"name":"name","specVersion":"V30","type":"string","types":["string"]},"status":{"description":"Updated status of the pet","exampleSetFlag":false,"extensions":{},"name":"status","specVersion":"V30","type":"string","types":["string"]}},"specVersion":"V30","type":"object","types":["object"]}}}},"responses":{"405":{"content":{},"description":"Invalid input","extensions":{}}},"security":[{"petstore_auth":["write:pets","read:pets"]}],"summary":"Updates a pet in the store with form data","tags":["pet"]}},"newOperations":{"GET":{"description":"Returns a single pet","extensions":{},"operationId":"getPetById","parameters":[{"description":"ID of pet to return","extensions":{},"in":"path","name":"petId","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[3].newOperations.GET.parameters[0].extensions"},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]}}],"responses":{"200":{"content":{"application/xml":{"exampleSetFlag":false,"schema":{"exampleSetFlag":false,"jsonSchema":{"type":"object","required":["name","photoUrls"],"properties":{"id":{"type":"integer","format":"int64"},"category":{"$ref":"#/components/schemas/Category","originalRef":"#/components/schemas/Category"},"name":{"type":"string","example":"doggies"},"newFeild":{"type":"string","example":"a feild demo by sayi","description":"a feild demo by sayi"},"owner":{"$ref":"#/components/schemas/User","originalRef":"#/components/schemas/User"},"parent":{"$ref":"#/components/schemas/Pet","originalRef":"#/components/schemas/Pet"},"photoUrls":{"type":"array","xml":{"name":"photoUrl","wrapped":true},"items":{"type":"string"}},"tags":{"type":"array","xml":{"name":"tag","wrapped":true},"items":{"$ref":"#/components/schemas/Tag","originalRef":"#/components/schemas/Tag"}},"status":{"type":"string","description":"pet status in the store","enum":["available","pending","sold"]}},"xml":{"name":"Pet"}},"properties":{"id":{"exampleSetFlag":false,"extensions":{},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]},"category":{"exampleSetFlag":false,"jsonSchema":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"newCatFeild":{"type":"string"}},"xml":{"name":"Category"}},"properties":{"id":{"exampleSetFlag":false,"extensions":{},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]},"newCatFeild":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"}},"specVersion":"V30","type":"object","types":["object"],"xml":{"name":"Category"}},"name":{"example":"doggies","exampleSetFlag":true,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"newFeild":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"owner":{"exampleSetFlag":false,"jsonSchema":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"username":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"},"favorite":{"$ref":"#/components/schemas/Pet","originalRef":"#/components/schemas/Pet"},"userStatus":{"type":"integer","format":"int32","description":"User Status"},"newUserFeild":{"type":"integer","format":"int32","description":"a new user feild demo"}},"xml":{"name":"User"}},"properties":{"id":{"exampleSetFlag":false,"extensions":{},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]},"username":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"firstName":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"lastName":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"email":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"password":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"favorite":{"exampleSetFlag":false,"specVersion":"V30"},"userStatus":{"description":"User Status","exampleSetFlag":false,"extensions":{},"format":"int32","specVersion":"V30","type":"integer","types":["integer"]},"newUserFeild":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"}},"specVersion":"V30","type":"object","types":["object"],"xml":{"name":"User"}},"parent":{"$ref":"$.changedEndpoints[3].newOperations.GET.responses.200.content.application/xml.schema.properties.owner.properties.favorite"},"photoUrls":{"exampleSetFlag":false,"extensions":{},"items":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"specVersion":"V30","type":"array","types":["array"],"xml":{"name":"photoUrl","wrapped":true}},"tags":{"exampleSetFlag":false,"extensions":{},"items":{"exampleSetFlag":false,"jsonSchema":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"}},"xml":{"name":"Tag"}},"properties":{"id":{"exampleSetFlag":false,"extensions":{},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]},"name":{"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]}},"specVersion":"V30","type":"object","types":["object"],"xml":{"name":"Tag"}},"specVersion":"V30","type":"array","types":["array"],"xml":{"name":"tag","wrapped":true}},"status":{"description":"pet status in the store","enum":["available","pending","sold"],"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]}},"required":["name","photoUrls"],"specVersion":"V30","type":"object","types":["object"],"xml":{"name":"Pet"}}},"application/json":{"exampleSetFlag":false,"schema":{"$ref":"$.changedEndpoints[3].newOperations.GET.responses.200.content.application/xml.schema"}}},"description":"successful operation","extensions":{}},"400":{"content":{},"description":"Invalid ID supplied","extensions":{}},"404":{"content":{},"description":"Pet not found","extensions":{}}},"security":[{"api_key":[]}],"summary":"Find pet by ID","tags":["pet"]}},"pathUrl":"/pet/{petId}"},{"changedOperations":{"POST":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[],"addResponses":{},"changedParameter":[{"changeDescription":true,"changeRequired":false,"changed":[],"diff":true,"increased":[],"leftParameter":{"description":"ID of pet to update","extensions":{},"in":"path","name":"petId","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[4].changedOperations.POST.changedParameter[0].leftParameter.extensions"},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]}},"missing":[],"rightParameter":{"description":"ID of pet to update, default false","extensions":{},"in":"path","name":"petId","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[4].changedOperations.POST.changedParameter[0].rightParameter.extensions"},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]}}}],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":true,"diffProduces":false,"diffProp":false,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[],"missingResponses":{},"summary":"uploads an image for pet"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/pet/{petId}/uploadImage"},{"changedOperations":{"POST":{"addConsumes":["application/json"],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[{"el":"id","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"format":"int64","specVersion":"V30","type":"integer","types":["integer"]},"removedEnums":false,"typeChange":true},{"el":"status","newEnums":true,"property":{"description":"Order Status","enum":["placed","approved","delivered"],"exampleSetFlag":false,"extensions":{},"specVersion":"V30","type":"string","types":["string"]},"removedEnums":true,"typeChange":false}],"changedRequestProps":[{"el":"id","newEnums":false,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[0].property"},"removedEnums":false,"typeChange":true},{"el":"status","newEnums":true,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[1].property"},"removedEnums":true,"typeChange":false}],"changedResponses":[{"addContentTypes":[],"addHeaders":{},"addProps":[],"changedHeaders":[],"changedProps":[{"el":"id","newEnums":false,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[0].property"},"removedEnums":false,"typeChange":true},{"el":"status","newEnums":true,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[1].property"},"removedEnums":true,"typeChange":false}],"contentTypeChanged":true,"descriptionChanged":false,"diff":true,"headersChanged":false,"missingContentTypes":["application/xml"],"missingHeaders":{},"missingProps":[],"newDescription":"successful operation","oldDescription":"successful operation","schemaChanged":true,"statusCode":"200"}],"diff":true,"diffConsumes":true,"diffParam":false,"diffProduces":true,"diffProp":true,"diffRequestProp":true,"missingConsumes":["application/xml"],"missingParameters":[],"missingProduces":["application/xml"],"missingProps":[],"missingRequestProps":[],"missingResponses":{},"summary":"Place an order for a pet"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/store/order"},{"changedOperations":{"GET":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[{"el":"id","newEnums":false,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[0].property"},"removedEnums":false,"typeChange":true},{"el":"status","newEnums":true,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[1].property"},"removedEnums":true,"typeChange":false}],"changedRequestProps":[],"changedResponses":[{"addContentTypes":[],"addHeaders":{},"addProps":[],"changedHeaders":[],"changedProps":[{"el":"id","newEnums":false,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[0].property"},"removedEnums":false,"typeChange":true},{"el":"status","newEnums":true,"property":{"$ref":"$.changedEndpoints[5].changedOperations.POST.changedProps[1].property"},"removedEnums":true,"typeChange":false}],"contentTypeChanged":false,"descriptionChanged":false,"diff":true,"headersChanged":false,"missingContentTypes":[],"missingHeaders":{},"missingProps":[],"newDescription":"successful operation","oldDescription":"successful operation","schemaChanged":true,"statusCode":"200"}],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":true,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[],"missingResponses":{},"summary":"Find purchase order by ID"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/store/order/{orderId}"},{"changedOperations":{"POST":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Create user"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/user"},{"changedOperations":{"POST":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Creates list of users with given input array"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/user/createWithArray"},{"changedOperations":{"POST":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Creates list of users with given input array"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/user/createWithList"},{"changedOperations":{"GET":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":true,"diffProduces":false,"diffProp":false,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[{"description":"The password for login in clear text","extensions":{},"in":"query","name":"password","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[10].changedOperations.GET.missingParameters[0].extensions"},"specVersion":"V30","type":"string","types":["string"]}}],"missingProduces":[],"missingProps":[],"missingRequestProps":[],"missingResponses":{},"summary":"Logs user into the system"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/user/login"},{"changedOperations":{"GET":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"addRequestProps":[],"addResponses":{},"changedParameter":[{"changeDescription":true,"changeRequired":false,"changed":[{"el":"username","newEnums":false,"property":{"exampleSetFlag":false,"extensions":{},"format":"int32","specVersion":"V30","type":"integer","types":["integer"]},"removedEnums":false,"typeChange":false}],"diff":true,"increased":[],"leftParameter":{"description":"The name that needs to be fetched. Use user1 for testing. ","extensions":{},"in":"path","name":"username","required":true,"schema":{"exampleSetFlag":false,"extensions":{"$ref":"$.changedEndpoints[11].changedOperations.GET.changedParameter[0].leftParameter.extensions"},"specVersion":"V30","type":"string","types":["string"]}},"missing":[],"rightParameter":{"description":"The name that needs to be fetched. Use user1 for testing. edited","extensions":{"$ref":"$.changedEndpoints[11].changedOperations.GET.changedParameter[0].changed[0].property.extensions"},"in":"path","name":"username","required":true,"schema":{"$ref":"$.changedEndpoints[11].changedOperations.GET.changedParameter[0].changed[0].property"}}}],"changedProps":[],"changedRequestProps":[],"changedResponses":[{"addContentTypes":[],"addHeaders":{},"addProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"changedHeaders":[],"changedProps":[],"contentTypeChanged":false,"descriptionChanged":false,"diff":true,"headersChanged":false,"missingContentTypes":[],"missingHeaders":{},"missingProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"newDescription":"successful operation","oldDescription":"successful operation","schemaChanged":true,"statusCode":"200"}],"diff":true,"diffConsumes":false,"diffParam":true,"diffProduces":false,"diffProp":true,"diffRequestProp":false,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingRequestProps":[],"missingResponses":{},"summary":"Get user by user name"},"PUT":{"addConsumes":[],"addParameters":[],"addProduces":[],"addProps":[],"addRequestProps":[{"el":"newUserFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[2].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.newFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.newCatFeild","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.addRequestProps[1].property"},"removedEnums":false,"typeChange":false}],"addResponses":{},"changedParameter":[],"changedProps":[],"changedRequestProps":[],"changedResponses":[],"diff":true,"diffConsumes":false,"diffParam":false,"diffProduces":false,"diffProp":false,"diffRequestProp":true,"missingConsumes":[],"missingParameters":[],"missingProduces":[],"missingProps":[],"missingRequestProps":[{"el":"phone","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[1].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.category.name","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[0].property"},"removedEnums":false,"typeChange":false},{"el":"favorite.tags.removedField","newEnums":false,"property":{"$ref":"$.changedEndpoints[0].changedOperations.PUT.missingRequestProps[2].property"},"removedEnums":false,"typeChange":false}],"missingResponses":{},"summary":"Updated user"}},"diff":true,"missingOperations":{},"newOperations":{},"pathUrl":"/user/{username}"}],"missingEndpoints":[{"method":"POST","operation":{"$ref":"$.changedEndpoints[3].missingOperations.POST"},"pathUrl":"/pet/{petId}","summary":"Updates a pet in the store with form data"}],"newEndpoints":[{"method":"GET","operation":{"$ref":"$.changedEndpoints[3].newOperations.GET"},"pathUrl":"/pet/{petId}","summary":"Find pet by ID"}],"newVersion":"1.0.2","oldVersion":"1.0.0"} \ No newline at end of file diff --git a/README.md b/README.md index 08adffd9..11c4c143 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Build Status](https://travis-ci.org/Sayi/swagger-diff.svg?branch=master) ![jdk1.8+](https://img.shields.io/badge/jdk-1.8%2B-orange.svg) [![Coverage Status](https://coveralls.io/repos/github/Sayi/swagger-diff/badge.svg)](https://coveralls.io/github/Sayi/swagger-diff) [![Maven](https://maven-badges.herokuapp.com/maven-central/com.deepoove/swagger-diff/badge.svg?style=plastic)](https://maven-badges.herokuapp.com/maven-central/com.deepoove/swagger-diff) -Compare two swagger API specifications(1.x or v2.0) and render the difference to html file or markdown file. +Compare two swagger/openAPI API specifications(1.x or v2.0 or openAPI 3.0.x) and render the difference to html file or markdown file. ## :black_large_square: Command line interface (CLI) @@ -15,7 +15,7 @@ Usage: java -jar swagger-diff.jar [options] * -new new api-doc location:Json file path or Http url -v - swagger version:1.0 or 2.0 + swagger version:1.0 or 2.0 or 3.0 Default: 2.0 -output-mode render mode: markdown or html @@ -36,7 +36,7 @@ java -jar swagger-diff.jar \ Download the fatJar or view the changelog on the **[Release Page](https://github.com/Sayi/swagger-diff/releases),** and thanks to all contributors. ## Feature -* Supports swagger spec v1.x and v2.0. +* Supports swagger spec v1.x v2.0 and openAPI 3.0.x * Depth comparison of parameters, responses, notes, http method(GET,POST,PUT,DELETE...) * Supports swagger api Authorization * Render difference of property with Expression Language @@ -48,13 +48,13 @@ Download the fatJar or view the changelog on the **[Release Page](https://github com.deepoove swagger-diff - 1.2.2 + 1.3.0 ``` ## Gradle ```shell -compile group: 'com.deepoove', name: 'swagger-diff', version: '1.2.2' +compile group: 'com.deepoove', name: 'swagger-diff', version: '1.3.0' ``` ## Usage @@ -73,6 +73,11 @@ v2.0 SwaggerDiff.compareV2("petstore_v2_1.json", "petstore_v2_2.json"); ``` +openAPI 3.0.x +```java +SwaggerDiff.compareV3("petstore_v3_1.json", "petstore_v3_2.json"); +``` + ## Render difference #### HTML ```java @@ -157,7 +162,3 @@ try { ## How it works ![image](./swagger-diff.png) - - - - diff --git a/pom.xml b/pom.xml index 27fc64c4..ebd917dc 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ com.deepoove swagger-diff - 1.2.2 + 1.3.0 jar swagger-diff @@ -57,14 +57,14 @@ - io.swagger + io.swagger.parser.v3 swagger-parser - 1.0.31 + 2.1.22 - io.swagger - swagger-compat-spec-parser - 1.0.31 + org.apache.commons + commons-lang3 + 3.12.0 com.j2html @@ -93,6 +93,11 @@ 4.8.2 test + + org.slf4j + slf4j-simple + 1.7.36 + @@ -199,6 +204,53 @@ coveralls-maven-plugin 4.3.0 + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.deepoove.swagger.diff.cli.CLI + + + + + + maven-assembly-plugin + + + + com.deepoove.swagger.diff.cli.CLI + + + + jar-with-dependencies + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + + prepare-agent + + prepare-agent + + + + + report + test + + report + + + + diff --git a/src/.DS_Store b/src/.DS_Store new file mode 100644 index 00000000..f8524ee5 Binary files /dev/null and b/src/.DS_Store differ diff --git a/src/main/java/com/deepoove/swagger/diff/SwaggerDiff.java b/src/main/java/com/deepoove/swagger/diff/SwaggerDiff.java index dfb3ebed..98bf550f 100644 --- a/src/main/java/com/deepoove/swagger/diff/SwaggerDiff.java +++ b/src/main/java/com/deepoove/swagger/diff/SwaggerDiff.java @@ -1,6 +1,5 @@ package com.deepoove.swagger.diff; -import java.io.IOException; import java.util.List; import org.slf4j.Logger; @@ -9,129 +8,99 @@ import com.deepoove.swagger.diff.compare.SpecificationDiff; import com.deepoove.swagger.diff.model.ChangedEndpoint; import com.deepoove.swagger.diff.model.Endpoint; -import com.fasterxml.jackson.databind.JsonNode; -import io.swagger.models.Swagger; -import io.swagger.models.auth.AuthorizationValue; -import io.swagger.parser.SwaggerCompatConverter; -import io.swagger.parser.SwaggerParser; +import io.swagger.parser.OpenAPIParser; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.parser.core.models.ParseOptions; +import io.swagger.v3.parser.core.models.SwaggerParseResult; public class SwaggerDiff { public static final String SWAGGER_VERSION_V2 = "2.0"; + public static final String OPENAPI_VERSION_V3 = "3.0"; private static Logger logger = LoggerFactory.getLogger(SwaggerDiff.class); - private Swagger oldSpecSwagger; - private Swagger newSpecSwagger; + private OpenAPI oldSpecSwagger; + private OpenAPI newSpecSwagger; private List newEndpoints; private List missingEndpoints; private List changedEndpoints; /** - * compare two swagger 1.x doc - * - * @param oldSpec - * old api-doc location:Json or Http - * @param newSpec - * new api-doc location:Json or Http - */ - public static SwaggerDiff compareV1(String oldSpec, String newSpec) { - return compare(oldSpec, newSpec, null, null); - } - - /** - * compare two swagger v2.0 doc - * - * @param oldSpec - * old api-doc location:Json or Http - * @param newSpec - * new api-doc location:Json or Http + * Compare two OpenAPI/Swagger documents (supports both Swagger 2.0 and OpenAPI 3.x). + * + * @param oldSpec old api-doc location: file path or HTTP URL + * @param newSpec new api-doc location: file path or HTTP URL */ public static SwaggerDiff compareV2(String oldSpec, String newSpec) { - return compare(oldSpec, newSpec, null, SWAGGER_VERSION_V2); + return compare(oldSpec, newSpec); } - /** - * compare two swagger v2.0 Sring + * Compare two OpenAPI 3.x documents. * - * @param oldSpec old api-doc json as string - * @param newSpec new api-doc json as string + * @param oldSpec old api-doc location: file path or HTTP URL + * @param newSpec new api-doc location: file path or HTTP URL */ - public static SwaggerDiff compareV2Raw(String oldSpec, String newSpec) { - return new SwaggerDiff(oldSpec, newSpec).compare(); + public static SwaggerDiff compareV3(String oldSpec, String newSpec) { + return compare(oldSpec, newSpec); } - + /** - * Compare two swagger v2.0 docs by JsonNode - * - * @param oldSpec - * old Swagger specification document in v2.0 format as a JsonNode - * @param newSpec - * new Swagger specification document in v2.0 format as a JsonNode + * Compare two OpenAPI/Swagger documents from raw JSON/YAML strings. */ - public static SwaggerDiff compareV2(JsonNode oldSpec, JsonNode newSpec) { - return new SwaggerDiff(oldSpec, newSpec).compare(); + public static SwaggerDiff compareRaw(String oldSpec, String newSpec) { + return new SwaggerDiff(oldSpec, newSpec, true).compare(); } - public static SwaggerDiff compare(String oldSpec, String newSpec, - List auths, String version) { - return new SwaggerDiff(oldSpec, newSpec, auths, version).compare(); + /** @deprecated Use compareV2 or compareV3 */ + @Deprecated + public static SwaggerDiff compareV2Raw(String oldSpec, String newSpec) { + return compareRaw(oldSpec, newSpec); } + public static SwaggerDiff compare(String oldSpec, String newSpec) { + return new SwaggerDiff(oldSpec, newSpec, false).compare(); + } - /** - * @param rawOldSpec - * @param rawNewSpec - */ - private SwaggerDiff(String rawOldSpec, String rawNewSpec) { - SwaggerParser swaggerParser = new SwaggerParser(); - oldSpecSwagger = swaggerParser.parse(rawOldSpec); - newSpecSwagger = swaggerParser.parse(rawNewSpec); + // ---- Constructors ---- - if (null == oldSpecSwagger || null == newSpecSwagger) { - throw new RuntimeException("cannot read api-doc from spec."); - } - } + /** Constructor for file path / URL */ + private SwaggerDiff(String oldSpec, String newSpec, boolean isRawContent) { + ParseOptions options = new ParseOptions(); + options.setResolve(true); + options.setResolveFully(true); - /** - * @param oldSpec - * @param newSpec - * @param auths - * @param version - */ - private SwaggerDiff(String oldSpec, String newSpec, List auths, - String version) { - if (SWAGGER_VERSION_V2.equals(version)) { - SwaggerParser swaggerParser = new SwaggerParser(); - oldSpecSwagger = swaggerParser.read(oldSpec, auths, true); - newSpecSwagger = swaggerParser.read(newSpec, auths, true); - } else { - SwaggerCompatConverter swaggerCompatConverter = new SwaggerCompatConverter(); - try { - oldSpecSwagger = swaggerCompatConverter.read(oldSpec, auths); - newSpecSwagger = swaggerCompatConverter.read(newSpec, auths); - } catch (IOException e) { - logger.error("cannot read api-doc from spec[version_v1.x]", e); - return; - } + OpenAPIParser parser = new OpenAPIParser(); + + SwaggerParseResult oldResult = isRawContent + ? parser.readContents(oldSpec, null, options) + : parser.readLocation(oldSpec, null, options); + SwaggerParseResult newResult = isRawContent + ? parser.readContents(newSpec, null, options) + : parser.readLocation(newSpec, null, options); + + oldSpecSwagger = oldResult.getOpenAPI(); + newSpecSwagger = newResult.getOpenAPI(); + + if (oldResult.getMessages() != null && !oldResult.getMessages().isEmpty()) { + logger.warn("Warnings parsing old spec: {}", oldResult.getMessages()); + } + if (newResult.getMessages() != null && !newResult.getMessages().isEmpty()) { + logger.warn("Warnings parsing new spec: {}", newResult.getMessages()); } - if (null == oldSpecSwagger || null == newSpecSwagger) { throw new RuntimeException( - "cannot read api-doc from spec."); } - } - private SwaggerDiff(JsonNode oldSpec, JsonNode newSpec) { - SwaggerParser swaggerParser = new SwaggerParser(); - oldSpecSwagger = swaggerParser.read(oldSpec, true); - newSpecSwagger = swaggerParser.read(newSpec, true); - if (null == oldSpecSwagger || null == newSpecSwagger) { throw new RuntimeException( - "cannot read api-doc from spec."); } + if (null == oldSpecSwagger || null == newSpecSwagger) { + throw new RuntimeException("Cannot read api-doc from spec. " + + "Old: " + oldResult.getMessages() + + " New: " + newResult.getMessages()); + } } private SwaggerDiff compare() { - SpecificationDiff diff = SpecificationDiff.diff(oldSpecSwagger, newSpecSwagger); + SpecificationDiff diff = SpecificationDiff.diff(oldSpecSwagger, newSpecSwagger); this.newEndpoints = diff.getNewEndpoints(); this.missingEndpoints = diff.getMissingEndpoints(); this.changedEndpoints = diff.getChangedEndpoints(); @@ -151,10 +120,10 @@ public List getChangedEndpoints() { } public String getOldVersion() { - return oldSpecSwagger.getInfo().getVersion(); + return oldSpecSwagger.getInfo() != null ? oldSpecSwagger.getInfo().getVersion() : "unknown"; } public String getNewVersion() { - return newSpecSwagger.getInfo().getVersion(); + return newSpecSwagger.getInfo() != null ? newSpecSwagger.getInfo().getVersion() : "unknown"; } } diff --git a/src/main/java/com/deepoove/swagger/diff/cli/CLI.java b/src/main/java/com/deepoove/swagger/diff/cli/CLI.java index 98c195ba..c1857009 100644 --- a/src/main/java/com/deepoove/swagger/diff/cli/CLI.java +++ b/src/main/java/com/deepoove/swagger/diff/cli/CLI.java @@ -9,30 +9,33 @@ import com.deepoove.swagger.diff.output.Render; /** - * $java -jar swagger-diff.jar -old http://www.petstore.com/swagger.json \n - * -new http://www.petstore.com/swagger_new.json \n - * -v 2.0 \n - * -output-mode markdown \n + * $java -jar swagger-diff.jar -old http://www.petstore.com/swagger.json + * -new http://www.petstore.com/swagger_new.json + * -v 2.0 + * -output-mode markdown + * + * Supports both Swagger 2.0 (-v 2.0) and OpenAPI 3.x (-v 3.0). * * @author Sayi - * @version */ public class CLI { private static final String OUTPUT_MODE_MARKDOWN = "markdown"; private static final String OUTPUT_MODE_JSON = "json"; - @Parameter(names = "-old", description = "old api-doc location:Json file path or Http url", required = true, order = 0) + @Parameter(names = "-old", description = "old api-doc location: file path or HTTP URL", required = true, order = 0) private String oldSpec; - @Parameter(names = "-new", description = "new api-doc location:Json file path or Http url", required = true, order = 1) + @Parameter(names = "-new", description = "new api-doc location: file path or HTTP URL", required = true, order = 1) private String newSpec; - @Parameter(names = "-v", description = "swagger version:1.0 or 2.0", validateWith= RegexValidator.class, order = 2) - @Regex("(2\\.0|1\\.0)") + @Parameter(names = "-v", description = "swagger/openapi version: 2.0 or 3.0", + validateWith = RegexValidator.class, order = 2) + @Regex("(2\\.0|1\\.0|3\\.0)") private String version = SwaggerDiff.SWAGGER_VERSION_V2; - @Parameter(names = "-output-mode", description = "render mode: markdown, html or json", validateWith = RegexValidator.class, order = 3) + @Parameter(names = "-output-mode", description = "render mode: markdown, html or json", + validateWith = RegexValidator.class, order = 3) @Regex("(markdown|html|json)") private String outputMode = OUTPUT_MODE_MARKDOWN; @@ -52,47 +55,32 @@ public static void main(String[] args) { } public void run(JCommander jCommander) { - if (help){ + if (help) { jCommander.setProgramName("java -jar swagger-diff.jar"); jCommander.usage(); return; } - if (v){ - JCommander.getConsole().println("1.2.2"); + if (v) { + JCommander.getConsole().println("1.2.2-oas3"); return; } - SwaggerDiff diff = SwaggerDiff.SWAGGER_VERSION_V2.equals(version) - ? SwaggerDiff.compareV2(oldSpec, newSpec) : SwaggerDiff.compareV1(oldSpec, newSpec); + SwaggerDiff diff = SwaggerDiff.OPENAPI_VERSION_V3.equals(version) + ? SwaggerDiff.compareV3(oldSpec, newSpec) + : SwaggerDiff.compareV2(oldSpec, newSpec); String render = getRender(outputMode).render(diff); JCommander.getConsole().println(render); } private Render getRender(String outputMode) { - if (OUTPUT_MODE_MARKDOWN.equals(outputMode)) { - return new MarkdownRender(); - } else if (OUTPUT_MODE_JSON.equals(outputMode)) { - return new JsonRender(); - } + if (OUTPUT_MODE_MARKDOWN.equals(outputMode)) return new MarkdownRender(); + if (OUTPUT_MODE_JSON.equals(outputMode)) return new JsonRender(); return new HtmlRender("Changelog", "http://deepoove.com/swagger-diff/stylesheets/demo.css"); } - public String getOldSpec() { - return oldSpec; - } - - public String getNewSpec() { - return newSpec; - } - - public String getVersion() { - return version; - } - - public String getOutputMode() { - return outputMode; - } - - + public String getOldSpec() { return oldSpec; } + public String getNewSpec() { return newSpec; } + public String getVersion() { return version; } + public String getOutputMode() { return outputMode; } } diff --git a/src/main/java/com/deepoove/swagger/diff/compare/ModelDiff.java b/src/main/java/com/deepoove/swagger/diff/compare/ModelDiff.java index 600132f5..543471ad 100644 --- a/src/main/java/com/deepoove/swagger/diff/compare/ModelDiff.java +++ b/src/main/java/com/deepoove/swagger/diff/compare/ModelDiff.java @@ -3,6 +3,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -11,100 +12,148 @@ import com.deepoove.swagger.diff.model.ElProperty; -import io.swagger.models.ArrayModel; -import io.swagger.models.Model; -import io.swagger.models.RefModel; -import io.swagger.models.properties.ArrayProperty; -import io.swagger.models.properties.Property; -import io.swagger.models.properties.RefProperty; -import io.swagger.models.properties.StringProperty; +import io.swagger.v3.oas.models.media.Schema; /** - * compare two model - * - * @author Sayi - * @version + * compare two OAS3 Schema (replaces Model + Property from Swagger 2.0) + * + * @author Sayi (adapted for OAS3) */ +@SuppressWarnings({"rawtypes", "unchecked"}) public class ModelDiff { private List increased; private List missing; private List changed; - Map oldDedinitions; - Map newDedinitions; + Map oldDefinitions; + Map newDefinitions; private ModelDiff() { - increased = new ArrayList(); - missing = new ArrayList(); - changed = new ArrayList(); + increased = new ArrayList<>(); + missing = new ArrayList<>(); + changed = new ArrayList<>(); } - public static ModelDiff buildWithDefinition(Map left, Map right) { + public static ModelDiff buildWithDefinition(Map left, Map right) { ModelDiff diff = new ModelDiff(); - diff.oldDedinitions = left; - diff.newDedinitions = right; + diff.oldDefinitions = left != null ? left : new HashMap<>(); + diff.newDefinitions = right != null ? right : new HashMap<>(); return diff; } - public ModelDiff diff(Model leftModel, Model rightModel) { - return this.diff(leftModel, rightModel, null, new HashSet()); + public ModelDiff diff(Schema leftModel, Schema rightModel) { + return this.diff(leftModel, rightModel, null, new HashSet<>()); } - public ModelDiff diff(Model leftModel, Model rightModel, String parentEl) { - return this.diff(leftModel, rightModel, parentEl, new HashSet()); + public ModelDiff diff(Schema leftModel, Schema rightModel, String parentEl) { + return this.diff(leftModel, rightModel, parentEl, new HashSet<>()); } - public ModelDiff diff(Property leftProperty, Property rightProperty) { - return this.diff(findModel(leftProperty, oldDedinitions), findModel(rightProperty, newDedinitions)); + /** Entry point when comparing two property-level schemas (resolves $ref before diffing) */ + public ModelDiff diffSchema(Schema leftProperty, Schema rightProperty) { + return this.diff(resolveRef(leftProperty, oldDefinitions), + resolveRef(rightProperty, newDefinitions)); } - private ModelDiff diff(Model leftInputModel, Model rightInputModel, String parentEl, Set visited) { - // Stop recursing if both models are null - // OR either model is already contained in the visiting history - if ((null == leftInputModel && null == rightInputModel) || visited.contains(leftInputModel) + private ModelDiff diff(Schema leftInputModel, Schema rightInputModel, + String parentEl, Set visited) { + if ((null == leftInputModel && null == rightInputModel) + || visited.contains(leftInputModel) || visited.contains(rightInputModel)) { return this; } - Model leftModel = isModelReference(leftInputModel) ? findReferenceModel(leftInputModel, oldDedinitions) - : leftInputModel; - Model rightModel = isModelReference(rightInputModel) ? findReferenceModel(rightInputModel, newDedinitions) - : rightInputModel; - Map leftProperties = null == leftModel ? null : leftModel.getProperties(); - Map rightProperties = null == rightModel ? null : rightModel.getProperties(); - // Diff the properties - MapKeyDiff propertyDiff = MapKeyDiff.diff(leftProperties, rightProperties); + Schema leftModel = resolveRef(leftInputModel, oldDefinitions); + Schema rightModel = resolveRef(rightInputModel, newDefinitions); - increased.addAll(convert2ElPropertys(propertyDiff.getIncreased(), parentEl)); - missing.addAll(convert2ElPropertys(propertyDiff.getMissing(), parentEl)); + while (leftModel != null && leftModel.getItems() != null && rightModel != null && rightModel.getItems() != null) { + leftModel = resolveRef(leftModel.getItems(), oldDefinitions); + rightModel = resolveRef(rightModel.getItems(), newDefinitions); + } + + Map leftProperties = leftModel == null ? null : leftModel.getProperties(); + Map rightProperties = rightModel == null ? null : rightModel.getProperties(); + + MapKeyDiff propertyDiff = MapKeyDiff.diff(leftProperties, rightProperties); + + increased.addAll(convert2ElProperties(propertyDiff.getIncreased(), parentEl)); + missing.addAll(convert2ElProperties(propertyDiff.getMissing(), parentEl)); + + Set newVisited = copyAndAdd(visited, leftModel, rightModel); - // Recursively find the diff between properties List sharedKey = propertyDiff.getSharedKey(); - sharedKey.stream().forEach((key) -> { - Property left = leftProperties.get(key); - Property right = rightProperties.get(key); - Model leftSubModel = findModel(left, oldDedinitions); - Model rightSubModel = findModel(left, newDedinitions); - if (leftSubModel != null || rightSubModel != null) { - diff(leftSubModel, rightSubModel, buildElString(parentEl, key), - copyAndAdd(visited, leftModel, rightModel)); - } else if (left != null && right != null && !left.equals(right)) { - // Add a changed ElProperty if not a Reference - // Useless - changed.add(addChangeMetadata(convert2ElProperty(key, parentEl, left), left, right)); + sharedKey.forEach(key -> { + Schema left = leftProperties.get(key); + Schema right = rightProperties.get(key); + + // Try to resolve as sub-model (ref or object with properties) + Schema leftSub = resolveToSubModel(left, oldDefinitions); + Schema rightSub = resolveToSubModel(right, newDefinitions); + + if (leftSub != null || rightSub != null) { + diff(leftSub, rightSub, buildElString(parentEl, key), newVisited); + } else if (left != null && right != null) { + ElProperty diffProp = convert2ElProperty(key, parentEl, left); + addChangeMetadata(diffProp, left, right); + if (diffProp.isTypeChange() || diffProp.isNewEnums() || diffProp.isRemovedEnums()) { + changed.add(diffProp); + } } }); + return this; } - private Collection convert2ElPropertys(Map propMap, String parentEl) { + /** Returns the resolved object schema if schema is a $ref or has properties; null otherwise */ + private Schema resolveToSubModel(Schema schema, Map definitions) { + if (schema == null) return null; + if (schema.get$ref() != null) { + return definitions.get(getSimpleRef(schema.get$ref())); + } + // Array: try to resolve items + if (schema.getItems() != null) { + String itemRef = schema.getItems().get$ref(); + if (itemRef != null) { + return definitions.get(getSimpleRef(itemRef)); + } + if (schema.getItems().getProperties() != null && !schema.getItems().getProperties().isEmpty()) { + return schema.getItems(); + } + } + // Object inline with properties + if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { + return schema; + } + return null; + } + + /** Resolve a $ref schema to the referenced schema in definitions */ + private Schema resolveRef(Schema schema, Map definitions) { + if (schema == null) return null; + if (schema.get$ref() != null) { + String name = getSimpleRef(schema.get$ref()); + Schema resolved = definitions.get(name); + return resolved != null ? resolved : schema; + } + return schema; + } - List result = new ArrayList(); - if (null == propMap) return result; + private boolean schemasCompatible(Schema left, Schema right) { + String lt = left.getType(); + String rt = right.getType(); + if (lt != null && !lt.equals(rt)) return false; + String lr = left.get$ref(); + String rr = right.get$ref(); + if (lr != null && !lr.equals(rr)) return false; + return true; + } - for (Entry entry : propMap.entrySet()) { - // TODO Recursively get the properties + private Collection convert2ElProperties( + Map propMap, String parentEl) { + List result = new ArrayList<>(); + if (propMap == null) return result; + for (Entry entry : propMap.entrySet()) { result.add(convert2ElProperty(entry.getKey(), parentEl, entry.getValue())); } return result; @@ -114,21 +163,25 @@ private String buildElString(String parentEl, String propName) { return null == parentEl ? propName : (parentEl + "." + propName); } - private ElProperty convert2ElProperty(String propName, String parentEl, Property property) { + private ElProperty convert2ElProperty(String propName, String parentEl, Schema schema) { ElProperty pWithPath = new ElProperty(); - pWithPath.setProperty(property); + pWithPath.setProperty(schema); pWithPath.setEl(buildElString(parentEl, propName)); return pWithPath; } - private ElProperty addChangeMetadata(ElProperty diffProperty, Property left, Property right) { - diffProperty.setTypeChange(!left.getType().equalsIgnoreCase(right.getType())); + private ElProperty addChangeMetadata(ElProperty diffProperty, Schema left, Schema right) { + String leftType = left.getType(); + String rightType = right.getType(); + diffProperty.setTypeChange(leftType != null && !leftType.equalsIgnoreCase( + rightType != null ? rightType : "")); + List leftEnums = enumValues(left); List rightEnums = enumValues(right); if (!leftEnums.isEmpty() && !rightEnums.isEmpty()) { ListDiff enumDiff = ListDiff.diff(leftEnums, rightEnums, (t, enumVal) -> { for (String value : t) { - if (enumVal.equalsIgnoreCase(value)) { return value; } + if (enumVal.equalsIgnoreCase(value)) return value; } return null; }); @@ -140,70 +193,33 @@ private ElProperty addChangeMetadata(ElProperty diffProperty, Property left, Pro @SuppressWarnings("unchecked") private Set copyAndAdd(Set set, T... add) { - Set newSet = new HashSet(set); + Set newSet = new HashSet<>(set); newSet.addAll(Arrays.asList(add)); return newSet; } - private List enumValues(Property prop) { - if (prop instanceof StringProperty && ((StringProperty) prop).getEnum() != null) { - return ((StringProperty) prop).getEnum(); - } else { - return new ArrayList<>(); - } - } - - private Model findModel(Property property, Map modelMap) { - String modelName = null; - if (property instanceof RefProperty) { - modelName = ((RefProperty) property).getSimpleRef(); - } else if (property instanceof ArrayProperty) { - Property arrayType = ((ArrayProperty) property).getItems(); - if (arrayType instanceof RefProperty) { - modelName = ((RefProperty) arrayType).getSimpleRef(); - } - } - return modelName == null ? null : modelMap.get(modelName); - } - - private boolean isModelReference(Model model) { - return model instanceof RefModel || model instanceof ArrayModel; - } - - private Model findReferenceModel(Model model, Map modelMap) { - String modelName = null; - if (model instanceof RefModel) { - modelName = ((RefModel) model).getSimpleRef(); - } else if (model instanceof ArrayModel) { - Property arrayType = ((ArrayModel) model).getItems(); - if (arrayType instanceof RefProperty) { - modelName = ((RefProperty) arrayType).getSimpleRef(); + private List enumValues(Schema schema) { + List result = new ArrayList<>(); + if (schema.getEnum() != null) { + for (Object val : schema.getEnum()) { + if (val != null) result.add(val.toString()); } } - return modelName == null ? null : modelMap.get(modelName); - } - - public List getIncreased() { - return increased; - } - - public void setIncreased(List increased) { - this.increased = increased; + return result; } - public List getMissing() { - return missing; + private String getSimpleRef(String ref) { + if (ref == null) return null; + int lastSlash = ref.lastIndexOf('/'); + return lastSlash >= 0 ? ref.substring(lastSlash + 1) : ref; } - public void setMissing(List missing) { - this.missing = missing; - } + public List getIncreased() { return increased; } + public void setIncreased(List increased) { this.increased = increased; } - public List getChanged() { - return changed; - } + public List getMissing() { return missing; } + public void setMissing(List missing) { this.missing = missing; } - public void setChanged(List changed) { - this.changed = changed; - } + public List getChanged() { return changed; } + public void setChanged(List changed) { this.changed = changed; } } diff --git a/src/main/java/com/deepoove/swagger/diff/compare/ParameterDiff.java b/src/main/java/com/deepoove/swagger/diff/compare/ParameterDiff.java index 30e8f3ba..b45a126e 100644 --- a/src/main/java/com/deepoove/swagger/diff/compare/ParameterDiff.java +++ b/src/main/java/com/deepoove/swagger/diff/compare/ParameterDiff.java @@ -1,6 +1,7 @@ package com.deepoove.swagger.diff.compare; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -10,38 +11,35 @@ import com.deepoove.swagger.diff.model.ElProperty; import com.google.common.collect.Lists; -import io.swagger.models.Model; -import io.swagger.models.parameters.AbstractSerializableParameter; -import io.swagger.models.parameters.BodyParameter; -import io.swagger.models.parameters.Parameter; -import io.swagger.models.properties.Property; -import io.swagger.models.properties.StringProperty; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; /** - * compare two parameter + * Compare two lists of OAS3 parameters (path/query/header/cookie only; + * request body is handled separately in SpecificationDiff). * - * @author Sayi - * @version + * @author Sayi (adapted for OAS3) */ +@SuppressWarnings({"rawtypes"}) public class ParameterDiff { private List increased; private List missing; private List changed; - Map oldDedinitions; - Map newDedinitions; + Map oldDefinitions; + Map newDefinitions; private ParameterDiff() { - this.increased = new ArrayList(); - this.missing = new ArrayList(); - this.changed = new ArrayList(); + this.increased = new ArrayList<>(); + this.missing = new ArrayList<>(); + this.changed = new ArrayList<>(); } - public static ParameterDiff buildWithDefinition(Map left, Map right) { + public static ParameterDiff buildWithDefinition(Map left, Map right) { ParameterDiff diff = new ParameterDiff(); - diff.oldDedinitions = left; - diff.newDedinitions = right; + diff.oldDefinitions = left != null ? left : new HashMap<>(); + diff.newDefinitions = right != null ? right : new HashMap<>(); return diff; } @@ -51,97 +49,59 @@ public ParameterDiff diff(List left, List right) { ListDiff paramDiff = ListDiff.diff(left, right, (t, param) -> { for (Parameter para : t) { - if (param.getName().equals(para.getName())) { return para; } + if (param.getName() != null && param.getName().equals(para.getName())) return para; } return null; }); this.increased.addAll(paramDiff.getIncreased()); this.missing.addAll(paramDiff.getMissing()); + Map shared = paramDiff.getShared(); shared.forEach((leftPara, rightPara) -> { ChangedParameter changedParameter = new ChangedParameter(); changedParameter.setLeftParameter(leftPara); changedParameter.setRightParameter(rightPara); - if (leftPara instanceof BodyParameter && rightPara instanceof BodyParameter) { - BodyParameter leftBodyPara = (BodyParameter) leftPara; - Model leftSchema = leftBodyPara.getSchema(); - BodyParameter rightBodyPara = (BodyParameter) rightPara; - Model rightSchema = rightBodyPara.getSchema(); - - ModelDiff diff = ModelDiff.buildWithDefinition(oldDedinitions, newDedinitions).diff(leftSchema, - rightSchema, leftPara.getName()); - changedParameter.setIncreased(diff.getIncreased()); - changedParameter.setMissing(diff.getMissing()); - changedParameter.setChanged(diff.getChanged()); - - } - // Let's handle the case where the new API has fx changed the type - // of PathParameter from being of type String to type integer - if (leftPara instanceof AbstractSerializableParameter - && rightPara instanceof AbstractSerializableParameter) { - if (!leftPara.equals(rightPara)) { + // Compare schema type changes (replaces AbstractSerializableParameter check) + Schema leftSchema = leftPara.getSchema(); + Schema rightSchema = rightPara.getSchema(); + if (leftSchema != null && rightSchema != null) { + String leftType = leftSchema.getType(); + String rightType = rightSchema.getType(); + if (leftType != null && !leftType.equals(rightType)) { ElProperty elProperty = new ElProperty(); elProperty.setEl(rightPara.getName()); - elProperty.setProperty(mapToProperty(rightPara)); + elProperty.setProperty(rightSchema); changedParameter.setChanged(Lists.newArrayList(elProperty)); } } - // is requried - boolean rightRequired = rightPara.getRequired(); - boolean leftRequired = leftPara.getRequired(); + // required + boolean rightRequired = Boolean.TRUE.equals(rightPara.getRequired()); + boolean leftRequired = Boolean.TRUE.equals(leftPara.getRequired()); changedParameter.setChangeRequired(leftRequired != rightRequired); // description String description = rightPara.getDescription(); - String oldPescription = leftPara.getDescription(); + String oldDescription = leftPara.getDescription(); if (StringUtils.isBlank(description)) description = ""; - if (StringUtils.isBlank(oldPescription)) oldPescription = ""; - changedParameter.setChangeDescription(!description.equals(oldPescription)); + if (StringUtils.isBlank(oldDescription)) oldDescription = ""; + changedParameter.setChangeDescription(!description.equals(oldDescription)); if (changedParameter.isDiff()) { this.changed.add(changedParameter); } - }); return this; } - private Property mapToProperty(Parameter rightPara) { - Property prop = new StringProperty(); - prop.setAccess(rightPara.getAccess()); - prop.setAllowEmptyValue(rightPara.getAllowEmptyValue()); - prop.setDescription(rightPara.getDescription()); - prop.setName(rightPara.getName()); - prop.setReadOnly(rightPara.isReadOnly()); - prop.setRequired(rightPara.getRequired()); - return prop; - } - - public List getIncreased() { - return increased; - } - - public void setIncreased(List increased) { - this.increased = increased; - } - - public List getMissing() { - return missing; - } - - public void setMissing(List missing) { - this.missing = missing; - } - - public List getChanged() { - return changed; - } + public List getIncreased() { return increased; } + public void setIncreased(List increased) { this.increased = increased; } - public void setChanged(List changed) { - this.changed = changed; - } + public List getMissing() { return missing; } + public void setMissing(List missing) { this.missing = missing; } + public List getChanged() { return changed; } + public void setChanged(List changed) { this.changed = changed; } } diff --git a/src/main/java/com/deepoove/swagger/diff/compare/PropertyDiff.java b/src/main/java/com/deepoove/swagger/diff/compare/PropertyDiff.java index 97817501..ae9df385 100644 --- a/src/main/java/com/deepoove/swagger/diff/compare/PropertyDiff.java +++ b/src/main/java/com/deepoove/swagger/diff/compare/PropertyDiff.java @@ -1,65 +1,52 @@ package com.deepoove.swagger.diff.compare; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import com.deepoove.swagger.diff.model.ElProperty; -import io.swagger.models.Model; -import io.swagger.models.properties.Property; +import io.swagger.v3.oas.models.media.Schema; +@SuppressWarnings({"rawtypes"}) public class PropertyDiff { private List increased; private List missing; private List changed; - Map oldDedinitions; - Map newDedinitions; + Map oldDefinitions; + Map newDefinitions; private PropertyDiff() { - increased = new ArrayList(); - missing = new ArrayList(); - changed = new ArrayList(); + increased = new ArrayList<>(); + missing = new ArrayList<>(); + changed = new ArrayList<>(); } - public static PropertyDiff buildWithDefinition(Map left, Map right) { + public static PropertyDiff buildWithDefinition(Map left, Map right) { PropertyDiff diff = new PropertyDiff(); - diff.oldDedinitions = left; - diff.newDedinitions = right; + diff.oldDefinitions = left != null ? left : new HashMap<>(); + diff.newDefinitions = right != null ? right : new HashMap<>(); return diff; } - public PropertyDiff diff(Property left, Property right) { - ModelDiff diff = ModelDiff.buildWithDefinition(oldDedinitions, newDedinitions).diff(left, right); - increased.addAll(diff.getIncreased()); - missing.addAll(diff.getMissing()); - changed.addAll(diff.getChanged()); + public PropertyDiff diff(Schema left, Schema right) { + ModelDiff modelDiff = ModelDiff.buildWithDefinition(oldDefinitions, newDefinitions) + .diffSchema(left, right); + increased.addAll(modelDiff.getIncreased()); + missing.addAll(modelDiff.getMissing()); + changed.addAll(modelDiff.getChanged()); return this; } - public List getIncreased() { - return increased; - } + public List getIncreased() { return increased; } + public void setIncreased(List increased) { this.increased = increased; } - public void setIncreased(List increased) { - this.increased = increased; - } + public List getMissing() { return missing; } + public void setMissing(List missing) { this.missing = missing; } - public List getMissing() { - return missing; - } - - public void setMissing(List missing) { - this.missing = missing; - } - - public List getChanged() { - return changed; - } - - public void setChanged(List changed) { - this.changed = changed; - } + public List getChanged() { return changed; } + public void setChanged(List changed) { this.changed = changed; } } diff --git a/src/main/java/com/deepoove/swagger/diff/compare/SpecificationDiff.java b/src/main/java/com/deepoove/swagger/diff/compare/SpecificationDiff.java index 2e0e6d66..0bc1441a 100644 --- a/src/main/java/com/deepoove/swagger/diff/compare/SpecificationDiff.java +++ b/src/main/java/com/deepoove/swagger/diff/compare/SpecificationDiff.java @@ -8,22 +8,28 @@ import com.deepoove.swagger.diff.model.ChangedEndpoint; import com.deepoove.swagger.diff.model.ChangedOperation; +import com.deepoove.swagger.diff.model.ChangedResponse; import com.deepoove.swagger.diff.model.Endpoint; -import io.swagger.models.HttpMethod; -import io.swagger.models.Operation; -import io.swagger.models.Path; -import io.swagger.models.Response; -import io.swagger.models.Swagger; -import io.swagger.models.parameters.Parameter; -import io.swagger.models.properties.Property; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import io.swagger.v3.oas.models.headers.Header; + /** - * compare two Swagger - * - * @author Sayi + * Compare two OpenAPI 3.x specifications. * + * @author Sayi (adapted for OAS3) */ +@SuppressWarnings({"rawtypes"}) public class SpecificationDiff { private List newEndpoints; @@ -32,70 +38,152 @@ public class SpecificationDiff { private SpecificationDiff() {} - public static SpecificationDiff diff(Swagger oldSpec, Swagger newSpec) { - if (null == oldSpec || null == newSpec) { throw new IllegalArgumentException("cannot diff null spec."); } + public static SpecificationDiff diff(OpenAPI oldSpec, OpenAPI newSpec) { + if (null == oldSpec || null == newSpec) { + throw new IllegalArgumentException("cannot diff null spec."); + } SpecificationDiff instance = new SpecificationDiff(); - Map oldPaths = oldSpec.getPaths(); - Map newPaths = newSpec.getPaths(); - // Diff path - MapKeyDiff pathDiff = MapKeyDiff.diff(oldPaths, newPaths); + Map oldDefs = getSchemas(oldSpec); + Map newDefs = getSchemas(newSpec); + + Map oldPaths = oldSpec.getPaths() != null ? oldSpec.getPaths() : new HashMap<>(); + Map newPaths = newSpec.getPaths() != null ? newSpec.getPaths() : new HashMap<>(); + + // Diff paths + MapKeyDiff pathDiff = MapKeyDiff.diff(oldPaths, newPaths); instance.newEndpoints = convert2EndpointList(pathDiff.getIncreased()); instance.missingEndpoints = convert2EndpointList(pathDiff.getMissing()); instance.changedEndpoints = new ArrayList<>(); List sharedKey = pathDiff.getSharedKey(); - sharedKey.stream().forEach((pathUrl) -> { + sharedKey.forEach(pathUrl -> { ChangedEndpoint changedEndpoint = new ChangedEndpoint(); changedEndpoint.setPathUrl(pathUrl); - Path oldPath = oldPaths.get(pathUrl); - Path newPath = newPaths.get(pathUrl); - - // Diff Operation - Map oldOperationMap = oldPath.getOperationMap(); - Map newOperationMap = newPath.getOperationMap(); - MapKeyDiff operationDiff = MapKeyDiff.diff(oldOperationMap, newOperationMap); - Map increasedOperation = operationDiff.getIncreased(); - Map missingOperation = operationDiff.getMissing(); - changedEndpoint.setNewOperations(increasedOperation); - changedEndpoint.setMissingOperations(missingOperation); - - List sharedMethods = operationDiff.getSharedKey(); - Map operas = new HashMap<>(); - sharedMethods.stream().forEach((method) -> { + PathItem oldPath = oldPaths.get(pathUrl); + PathItem newPath = newPaths.get(pathUrl); + + // Diff operations + Map oldOpMap = oldPath.readOperationsMap() != null + ? oldPath.readOperationsMap() : new HashMap<>(); + Map newOpMap = newPath.readOperationsMap() != null + ? newPath.readOperationsMap() : new HashMap<>(); + + MapKeyDiff operationDiff = + MapKeyDiff.diff(oldOpMap, newOpMap); + + changedEndpoint.setNewOperations(operationDiff.getIncreased()); + changedEndpoint.setMissingOperations(operationDiff.getMissing()); + + Map operas = new HashMap<>(); + operationDiff.getSharedKey().forEach(method -> { ChangedOperation changedOperation = new ChangedOperation(); - Operation oldOperation = oldOperationMap.get(method); - Operation newOperation = newOperationMap.get(method); - changedOperation.setSummary(newOperation.getSummary()); - - // Diff Parameter - List oldParameters = oldOperation.getParameters(); - List newParameters = newOperation.getParameters(); - ParameterDiff parameterDiff = ParameterDiff - .buildWithDefinition(oldSpec.getDefinitions(), newSpec.getDefinitions()) - .diff(oldParameters, newParameters); - changedOperation.setAddParameters(parameterDiff.getIncreased()); - changedOperation.setMissingParameters(parameterDiff.getMissing()); - changedOperation.setChangedParameter(parameterDiff.getChanged()); - - // Diff response - Property oldResponseProperty = getResponseProperty(oldOperation); - Property newResponseProperty = getResponseProperty(newOperation); - PropertyDiff propertyDiff = PropertyDiff.buildWithDefinition(oldSpec.getDefinitions(), - newSpec.getDefinitions()); - propertyDiff.diff(oldResponseProperty, newResponseProperty); + Operation oldOp = oldOpMap.get(method); + Operation newOp = newOpMap.get(method); + changedOperation.setSummary(newOp.getSummary()); + + // Diff parameters (path/query/header/cookie — NOT body) + List oldParams = oldOp.getParameters() != null + ? oldOp.getParameters() : new ArrayList<>(); + List newParams = newOp.getParameters() != null + ? newOp.getParameters() : new ArrayList<>(); + ParameterDiff paramDiff = ParameterDiff.buildWithDefinition(oldDefs, newDefs) + .diff(oldParams, newParams); + changedOperation.setAddParameters(paramDiff.getIncreased()); + changedOperation.setMissingParameters(paramDiff.getMissing()); + changedOperation.setChangedParameter(paramDiff.getChanged()); + + // Diff request body schema + Schema oldRequestSchema = getRequestSchema(oldOp); + Schema newRequestSchema = getRequestSchema(newOp); + PropertyDiff requestPropertyDiff = PropertyDiff.buildWithDefinition(oldDefs, newDefs); + requestPropertyDiff.diff(oldRequestSchema, newRequestSchema); + changedOperation.setAddRequestProps(requestPropertyDiff.getIncreased()); + changedOperation.setMissingRequestProps(requestPropertyDiff.getMissing()); + changedOperation.setChangedRequestProps(requestPropertyDiff.getChanged()); + + // --- LEGACY: diff schema del 200 sui campi addProps/missingProps/changedProps + // (mantenuto per retrocompatibilità con i test/render esistenti) --- + Schema oldResponseSchema = getResponseSchema(oldOp); // il vecchio helper che prende il 200 + Schema newResponseSchema = getResponseSchema(newOp); + PropertyDiff propertyDiff = PropertyDiff.buildWithDefinition(oldDefs, newDefs); + propertyDiff.diff(oldResponseSchema, newResponseSchema); changedOperation.setAddProps(propertyDiff.getIncreased()); changedOperation.setMissingProps(propertyDiff.getMissing()); changedOperation.setChangedProps(propertyDiff.getChanged()); + // Diff RESPONSES per status code + ApiResponses oldResponses = oldOp.getResponses() != null + ? oldOp.getResponses() : new ApiResponses(); + ApiResponses newResponses = newOp.getResponses() != null + ? newOp.getResponses() : new ApiResponses(); + + MapKeyDiff responseDiff = MapKeyDiff.diff(oldResponses, newResponses); + + // status aggiunti (es. nuovo 201) e rimossi (es. 404 tolto) + changedOperation.setAddResponses(responseDiff.getIncreased()); + changedOperation.setMissingResponses(responseDiff.getMissing()); + + // status presenti in entrambi: diff dello schema per ciascuno + List changedResponseList = new ArrayList<>(); + + responseDiff.getSharedKey().forEach(statusCode -> { + ApiResponse oldResp = oldResponses.get(statusCode); + ApiResponse newResp = newResponses.get(statusCode); + + ChangedResponse cr = new ChangedResponse(); + cr.setStatusCode(statusCode); + + // --- description --- + cr.setOldDescription(oldResp != null ? oldResp.getDescription() : null); + cr.setNewDescription(newResp != null ? newResp.getDescription() : null); + + // --- schema (application/json, come prima) --- + Schema oldRespSchema = getResponseSchema(oldResp); + Schema newRespSchema = getResponseSchema(newResp); + PropertyDiff respPropDiff = PropertyDiff.buildWithDefinition(oldDefs, newDefs); + respPropDiff.diff(oldRespSchema, newRespSchema); + cr.setAddProps(respPropDiff.getIncreased()); + cr.setMissingProps(respPropDiff.getMissing()); + cr.setChangedProps(respPropDiff.getChanged()); + + // --- content-type --- + ListDiff ctDiff = getMediaTypeDiff( + getContentTypes(oldResp), getContentTypes(newResp)); + cr.setAddContentTypes(ctDiff.getIncreased()); + cr.setMissingContentTypes(ctDiff.getMissing()); + + // --- headers --- + Map oldHeaders = getHeaders(oldResp); + Map newHeaders = getHeaders(newResp); + MapKeyDiff headerDiff = MapKeyDiff.diff(oldHeaders, newHeaders); + cr.setAddHeaders(headerDiff.getIncreased()); + cr.setMissingHeaders(headerDiff.getMissing()); + // header presenti in entrambi ma cambiati (per tipo/descrizione) + List changedHeaderNames = new ArrayList<>(); + headerDiff.getSharedKey().forEach(headerName -> { + Header oh = oldHeaders.get(headerName); + Header nh = newHeaders.get(headerName); + if (isHeaderChanged(oh, nh)) { + changedHeaderNames.add(headerName); + } + }); + cr.setChangedHeaders(changedHeaderNames); - // Diff Consumes - ListDiff consumeDiff = getMediaTypeDiff(oldOperation.getConsumes(), newOperation.getConsumes()); + if (cr.isDiff()) { + changedResponseList.add(cr); + } + }); + changedOperation.setChangedResponses(changedResponseList); + + // Diff consumes (request body content types) + ListDiff consumeDiff = getMediaTypeDiff( + getRequestContentTypes(oldOp), getRequestContentTypes(newOp)); changedOperation.setAddConsumes(consumeDiff.getIncreased()); changedOperation.setMissingConsumes(consumeDiff.getMissing()); - // Diff Produces - ListDiff producesDiff = getMediaTypeDiff(oldOperation.getProduces(), - newOperation.getProduces()); + // Diff produces (response content types) + ListDiff producesDiff = getMediaTypeDiff( + getResponseContentTypes(oldOp), getResponseContentTypes(newOp)); changedOperation.setAddProduces(producesDiff.getIncreased()); changedOperation.setMissingProduces(producesDiff.getMissing()); @@ -103,12 +191,13 @@ public static SpecificationDiff diff(Swagger oldSpec, Swagger newSpec) { operas.put(method, changedOperation); } }); + changedEndpoint.setChangedOperations(operas); - instance.newEndpoints - .addAll(convert2EndpointList(changedEndpoint.getPathUrl(), changedEndpoint.getNewOperations())); - instance.missingEndpoints - .addAll(convert2EndpointList(changedEndpoint.getPathUrl(), changedEndpoint.getMissingOperations())); + instance.newEndpoints.addAll( + convert2EndpointList(changedEndpoint.getPathUrl(), changedEndpoint.getNewOperations())); + instance.missingEndpoints.addAll( + convert2EndpointList(changedEndpoint.getPathUrl(), changedEndpoint.getMissingOperations())); if (changedEndpoint.isDiff()) { instance.changedEndpoints.add(changedEndpoint); @@ -116,28 +205,86 @@ public static SpecificationDiff diff(Swagger oldSpec, Swagger newSpec) { }); return instance; + } + // ---- Helpers ---- + + @SuppressWarnings("unchecked") + private static Map getSchemas(OpenAPI api) { + if (api.getComponents() == null) return new HashMap<>(); + Map schemas = api.getComponents().getSchemas(); + return schemas != null ? schemas : new HashMap<>(); } - private static Property getResponseProperty(Operation operation) { - Map responses = operation.getResponses(); - // temporary workaround for missing response messages + // TODO: remove this old method that only manage 200 response + private static Schema getResponseSchema(Operation operation) { + ApiResponses responses = operation.getResponses(); if (responses == null) return null; - Response response = responses.get("200"); - return null == response ? null : response.getSchema(); + ApiResponse response = responses.get("200"); + if (response == null && !responses.isEmpty()) { + response = responses.values().iterator().next(); + } + if (response == null) return null; + Content content = response.getContent(); + if (content == null || content.isEmpty()) return null; + MediaType mediaType = content.get("application/json"); + if (mediaType == null) mediaType = content.values().iterator().next(); + return mediaType != null ? mediaType.getSchema() : null; + } + +private static Schema getResponseSchema(ApiResponse response) { + if (response == null) { System.out.println(">>> response NULL"); return null; } + Content content = response.getContent(); + //System.out.println(">>> response content keys = " + (content != null ? content.keySet() : "NULL")); + if (content == null || content.isEmpty()) return null; + MediaType mediaType = content.get("application/json"); + if (mediaType == null) mediaType = content.values().iterator().next(); + Schema s = mediaType != null ? mediaType.getSchema() : null; + //System.out.println(">>> response schema = " + (s != null ? s.getType() + " ref=" + s.get$ref() : "NULL")); + return s; +} + + private static List getResponseContentTypes(Operation operation) { + ApiResponses responses = operation.getResponses(); + if (responses == null) return new ArrayList<>(); + ApiResponse response = responses.get("200"); + if (response == null && !responses.isEmpty()) { + response = responses.values().iterator().next(); + } + if (response == null) return new ArrayList<>(); + Content content = response.getContent(); + return content != null ? new ArrayList<>(content.keySet()) : new ArrayList<>(); + } + + private static List getRequestContentTypes(Operation operation) { + RequestBody requestBody = operation.getRequestBody(); + if (requestBody == null) return new ArrayList<>(); + Content content = requestBody.getContent(); + return content != null ? new ArrayList<>(content.keySet()) : new ArrayList<>(); + } + + private static Schema getRequestSchema(Operation operation) { + RequestBody requestBody = operation.getRequestBody(); + if (requestBody == null) return null; + Content content = requestBody.getContent(); + if (content == null || content.isEmpty()) return null; + MediaType mediaType = content.get("application/json"); + if (mediaType == null) mediaType = content.values().iterator().next(); + return mediaType != null ? mediaType.getSchema() : null; } - private static List convert2EndpointList(Map map) { - List endpoints = new ArrayList(); + private static List convert2EndpointList(Map map) { + List endpoints = new ArrayList<>(); if (null == map) return endpoints; - map.forEach((url, path) -> { - Map operationMap = path.getOperationMap(); - operationMap.forEach((httpMethod, operation) -> { + map.forEach((url, pathItem) -> { + Map opMap = pathItem.readOperationsMap(); + if (opMap == null) return; + opMap.forEach((httpMethod, operation) -> { Endpoint endpoint = new Endpoint(); endpoint.setPathUrl(url); endpoint.setMethod(httpMethod); endpoint.setSummary(operation.getSummary()); - endpoint.setPath(path); + endpoint.setPath(pathItem); endpoint.setOperation(operation); endpoints.add(endpoint); }); @@ -145,8 +292,9 @@ private static List convert2EndpointList(Map map) { return endpoints; } - private static Collection convert2EndpointList(String pathUrl, Map map) { - List endpoints = new ArrayList(); + private static Collection convert2EndpointList( + String pathUrl, Map map) { + List endpoints = new ArrayList<>(); if (null == map) return endpoints; map.forEach((httpMethod, operation) -> { Endpoint endpoint = new Endpoint(); @@ -162,22 +310,42 @@ private static Collection convert2EndpointList(String pathUr private static ListDiff getMediaTypeDiff(List oldTypes, List newTypes) { return ListDiff.diff(oldTypes, newTypes, (t, sample) -> { for (String mediaType : t) { - if (sample.equalsIgnoreCase(mediaType)) { return mediaType; } + if (sample.equalsIgnoreCase(mediaType)) return mediaType; } return null; }); } - public List getNewEndpoints() { - return newEndpoints; - } + public List getNewEndpoints() { return newEndpoints; } + public List getMissingEndpoints() { return missingEndpoints; } + public List getChangedEndpoints() { return changedEndpoints; } + - public List getMissingEndpoints() { - return missingEndpoints; + private static List getContentTypes(ApiResponse response) { + if (response == null) return new ArrayList<>(); + Content content = response.getContent(); + return content != null ? new ArrayList<>(content.keySet()) : new ArrayList<>(); } - public List getChangedEndpoints() { - return changedEndpoints; + private static Map getHeaders(ApiResponse response) { + if (response == null || response.getHeaders() == null) return new HashMap<>(); + return response.getHeaders(); } + /** + * Confronto "leggero" di due header: cambio di descrizione, deprecated o tipo schema. + */ + private static boolean isHeaderChanged(Header oldHeader, Header newHeader) { + if (oldHeader == null || newHeader == null) return oldHeader != newHeader; + + if (!java.util.Objects.equals(oldHeader.getDescription(), newHeader.getDescription())) return true; + if (!java.util.Objects.equals(oldHeader.getDeprecated(), newHeader.getDeprecated())) return true; + if (!java.util.Objects.equals(oldHeader.getRequired(), newHeader.getRequired())) return true; + + String oldType = oldHeader.getSchema() != null ? oldHeader.getSchema().getType() : null; + String newType = newHeader.getSchema() != null ? newHeader.getSchema().getType() : null; + if (!java.util.Objects.equals(oldType, newType)) return true; + + return false; + } } diff --git a/src/main/java/com/deepoove/swagger/diff/model/ChangedEndpoint.java b/src/main/java/com/deepoove/swagger/diff/model/ChangedEndpoint.java index 270972b2..77f07cfb 100644 --- a/src/main/java/com/deepoove/swagger/diff/model/ChangedEndpoint.java +++ b/src/main/java/com/deepoove/swagger/diff/model/ChangedEndpoint.java @@ -2,39 +2,39 @@ import java.util.Map; -import io.swagger.models.HttpMethod; -import io.swagger.models.Operation; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; public class ChangedEndpoint implements Changed { private String pathUrl; - private Map newOperations; - private Map missingOperations; + private Map newOperations; + private Map missingOperations; - private Map changedOperations; + private Map changedOperations; - public Map getNewOperations() { + public Map getNewOperations() { return newOperations; } - public void setNewOperations(Map newOperations) { + public void setNewOperations(Map newOperations) { this.newOperations = newOperations; } - public Map getMissingOperations() { + public Map getMissingOperations() { return missingOperations; } - public void setMissingOperations(Map missingOperations) { + public void setMissingOperations(Map missingOperations) { this.missingOperations = missingOperations; } - public Map getChangedOperations() { + public Map getChangedOperations() { return changedOperations; } - public void setChangedOperations(Map changedOperations) { + public void setChangedOperations(Map changedOperations) { this.changedOperations = changedOperations; } @@ -47,9 +47,6 @@ public void setPathUrl(String pathUrl) { } public boolean isDiff() { - // newOperations.isEmpty() - // || !missingOperations.isEmpty() - // || return !changedOperations.isEmpty(); } diff --git a/src/main/java/com/deepoove/swagger/diff/model/ChangedOperation.java b/src/main/java/com/deepoove/swagger/diff/model/ChangedOperation.java index 8703cf5c..6e5a2175 100644 --- a/src/main/java/com/deepoove/swagger/diff/model/ChangedOperation.java +++ b/src/main/java/com/deepoove/swagger/diff/model/ChangedOperation.java @@ -1,40 +1,50 @@ package com.deepoove.swagger.diff.model; import java.util.ArrayList; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; -import io.swagger.models.parameters.Parameter; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.Operation; + +import io.swagger.v3.oas.models.responses.ApiResponse; public class ChangedOperation implements Changed { private String summary; - private List addParameters = new ArrayList(); - private List missingParameters = new ArrayList(); + private List addParameters = new ArrayList<>(); + private List missingParameters = new ArrayList<>(); + + private List changedParameter = new ArrayList<>(); - private List changedParameter = new ArrayList(); + private List addProps = new ArrayList<>(); + private List missingProps = new ArrayList<>(); + private List changedProps = new ArrayList<>(); + + private List addRequestProps = new ArrayList<>(); + private List missingRequestProps = new ArrayList<>(); + private List changedRequestProps = new ArrayList<>(); - private List addProps = new ArrayList(); - private List missingProps = new ArrayList(); - private List changedProps = new ArrayList(); private List addConsumes = new ArrayList<>(); private List missingConsumes = new ArrayList<>(); private List addProduces = new ArrayList<>(); private List missingProduces = new ArrayList<>(); - public List getAddParameters() { + public List getAddParameters() { return addParameters; } - public void setAddParameters(List addParameters) { + public void setAddParameters(List addParameters) { this.addParameters = addParameters; } - public List getMissingParameters() { + public List getMissingParameters() { return missingParameters; } - public void setMissingParameters(List missingParameters) { + public void setMissingParameters(List missingParameters) { this.missingParameters = missingParameters; } @@ -79,14 +89,20 @@ public void setSummary(String summary) { } public boolean isDiff() { - return !addParameters.isEmpty() || !missingParameters.isEmpty() || !changedParameter.isEmpty() || isDiffProp() - || isDiffConsumes() || isDiffProduces(); + return !addParameters.isEmpty() || !missingParameters.isEmpty() || !changedParameter.isEmpty() + || isDiffProp() || isDiffRequestProp() || isDiffConsumes() || isDiffProduces()|| !addResponses.isEmpty() + || !missingResponses.isEmpty() || !changedResponses.isEmpty(); } + public boolean isDiffProp() { return !addProps.isEmpty() || !missingProps.isEmpty() || !changedProps.isEmpty(); } + public boolean isDiffRequestProp() { + return !addRequestProps.isEmpty() || !missingRequestProps.isEmpty() || !changedRequestProps.isEmpty(); + } + public boolean isDiffParam() { return !addParameters.isEmpty() || !missingParameters.isEmpty() || !changedParameter.isEmpty(); } @@ -130,4 +146,49 @@ public List getMissingProduces() { public void setMissingProduces(List missing) { this.missingProduces = missing == null ? new ArrayList<>() : missing; } -} + + public List getAddRequestProps() { + return addRequestProps; + } + + public void setAddRequestProps(List addRequestProps) { + this.addRequestProps = addRequestProps; + } + + public List getMissingRequestProps() { + return missingRequestProps; + } + + public void setMissingRequestProps(List missingRequestProps) { + this.missingRequestProps = missingRequestProps; + } + + public List getChangedRequestProps() { + return changedRequestProps; + } + + public void setChangedRequestProps(List changedRequestProps) { + this.changedRequestProps = changedRequestProps; + } + + // --- Response diffs per status code --- + private Map addResponses = new LinkedHashMap<>(); + private Map missingResponses = new LinkedHashMap<>(); + // status presenti in entrambi ma con schema cambiato + private List changedResponses = new ArrayList<>(); + + public Map getAddResponses() { return addResponses; } + public void setAddResponses(Map addResponses) { + this.addResponses = addResponses != null ? addResponses : new LinkedHashMap<>(); + } + + public Map getMissingResponses() { return missingResponses; } + public void setMissingResponses(Map missingResponses) { + this.missingResponses = missingResponses != null ? missingResponses : new LinkedHashMap<>(); + } + + public List getChangedResponses() { return changedResponses; } + public void setChangedResponses(List changedResponses) { + this.changedResponses = changedResponses != null ? changedResponses : new ArrayList<>(); + } +} \ No newline at end of file diff --git a/src/main/java/com/deepoove/swagger/diff/model/ChangedParameter.java b/src/main/java/com/deepoove/swagger/diff/model/ChangedParameter.java index 05d24b71..158ced32 100644 --- a/src/main/java/com/deepoove/swagger/diff/model/ChangedParameter.java +++ b/src/main/java/com/deepoove/swagger/diff/model/ChangedParameter.java @@ -3,7 +3,7 @@ import java.util.ArrayList; import java.util.List; -import io.swagger.models.parameters.Parameter; +import io.swagger.v3.oas.models.parameters.Parameter; public class ChangedParameter implements Changed { @@ -15,7 +15,6 @@ public class ChangedParameter implements Changed { private Parameter rightParameter; private boolean isChangeRequired; - // private boolean isChangeType; private boolean isChangeDescription; public boolean isChangeRequired() { diff --git a/src/main/java/com/deepoove/swagger/diff/model/ChangedResponse.java b/src/main/java/com/deepoove/swagger/diff/model/ChangedResponse.java new file mode 100644 index 00000000..012197e1 --- /dev/null +++ b/src/main/java/com/deepoove/swagger/diff/model/ChangedResponse.java @@ -0,0 +1,109 @@ +package com.deepoove.swagger.diff.model; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import io.swagger.v3.oas.models.headers.Header; + +public class ChangedResponse { + + private String statusCode; + + // --- description --- + private String oldDescription; + private String newDescription; + + // --- schema (proprietà del body) --- + private List addProps = new ArrayList<>(); + private List missingProps = new ArrayList<>(); + private List changedProps = new ArrayList<>(); + + // --- content-type (media types) --- + private List addContentTypes = new ArrayList<>(); + private List missingContentTypes = new ArrayList<>(); + + // --- headers --- + private Map addHeaders = new LinkedHashMap<>(); + private Map missingHeaders = new LinkedHashMap<>(); + private List changedHeaders = new ArrayList<>(); // nomi degli header cambiati + + public String getStatusCode() { return statusCode; } + public void setStatusCode(String statusCode) { this.statusCode = statusCode; } + + // --- description --- + public String getOldDescription() { return oldDescription; } + public void setOldDescription(String oldDescription) { this.oldDescription = oldDescription; } + + public String getNewDescription() { return newDescription; } + public void setNewDescription(String newDescription) { this.newDescription = newDescription; } + + public boolean isDescriptionChanged() { + return !Objects.equals(oldDescription, newDescription); + } + + // --- schema --- + public List getAddProps() { return addProps; } + public void setAddProps(List addProps) { + this.addProps = addProps != null ? addProps : new ArrayList<>(); + } + + public List getMissingProps() { return missingProps; } + public void setMissingProps(List missingProps) { + this.missingProps = missingProps != null ? missingProps : new ArrayList<>(); + } + + public List getChangedProps() { return changedProps; } + public void setChangedProps(List changedProps) { + this.changedProps = changedProps != null ? changedProps : new ArrayList<>(); + } + + public boolean isSchemaChanged() { + return !addProps.isEmpty() || !missingProps.isEmpty() || !changedProps.isEmpty(); + } + + // --- content-type --- + public List getAddContentTypes() { return addContentTypes; } + public void setAddContentTypes(List addContentTypes) { + this.addContentTypes = addContentTypes != null ? addContentTypes : new ArrayList<>(); + } + + public List getMissingContentTypes() { return missingContentTypes; } + public void setMissingContentTypes(List missingContentTypes) { + this.missingContentTypes = missingContentTypes != null ? missingContentTypes : new ArrayList<>(); + } + + public boolean isContentTypeChanged() { + return !addContentTypes.isEmpty() || !missingContentTypes.isEmpty(); + } + + // --- headers --- + public Map getAddHeaders() { return addHeaders; } + public void setAddHeaders(Map addHeaders) { + this.addHeaders = addHeaders != null ? addHeaders : new LinkedHashMap<>(); + } + + public Map getMissingHeaders() { return missingHeaders; } + public void setMissingHeaders(Map missingHeaders) { + this.missingHeaders = missingHeaders != null ? missingHeaders : new LinkedHashMap<>(); + } + + public List getChangedHeaders() { return changedHeaders; } + public void setChangedHeaders(List changedHeaders) { + this.changedHeaders = changedHeaders != null ? changedHeaders : new ArrayList<>(); + } + + public boolean isHeadersChanged() { + return !addHeaders.isEmpty() || !missingHeaders.isEmpty() || !changedHeaders.isEmpty(); + } + + // --- diff globale --- + public boolean isDiff() { + return isDescriptionChanged() + || isSchemaChanged() + || isContentTypeChanged() + || isHeadersChanged(); + } +} \ No newline at end of file diff --git a/src/main/java/com/deepoove/swagger/diff/model/ElProperty.java b/src/main/java/com/deepoove/swagger/diff/model/ElProperty.java index cb2bf282..c736f58f 100644 --- a/src/main/java/com/deepoove/swagger/diff/model/ElProperty.java +++ b/src/main/java/com/deepoove/swagger/diff/model/ElProperty.java @@ -1,10 +1,10 @@ package com.deepoove.swagger.diff.model; -import io.swagger.models.properties.Property; +import io.swagger.v3.oas.models.media.Schema; /** * property with expression Language grammar - * + * * @author Sayi * @version */ @@ -12,18 +12,18 @@ public class ElProperty { private String el; - private Property property; + private Schema property; // optional change metadata private boolean isTypeChange; private boolean newEnums; private boolean removedEnums; - public Property getProperty() { + public Schema getProperty() { return property; } - public void setProperty(Property property) { + public void setProperty(Schema property) { this.property = property; } diff --git a/src/main/java/com/deepoove/swagger/diff/model/Endpoint.java b/src/main/java/com/deepoove/swagger/diff/model/Endpoint.java index 6ea5f205..bd5d90a7 100644 --- a/src/main/java/com/deepoove/swagger/diff/model/Endpoint.java +++ b/src/main/java/com/deepoove/swagger/diff/model/Endpoint.java @@ -1,16 +1,15 @@ package com.deepoove.swagger.diff.model; -import io.swagger.models.HttpMethod; -import io.swagger.models.Operation; -import io.swagger.models.Path; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; public class Endpoint { private String pathUrl; - private HttpMethod method; + private PathItem.HttpMethod method; private String summary; - private Path path; + private PathItem path; private Operation operation; public String getPathUrl() { @@ -21,11 +20,11 @@ public void setPathUrl(String pathUrl) { this.pathUrl = pathUrl; } - public HttpMethod getMethod() { + public PathItem.HttpMethod getMethod() { return method; } - public void setMethod(HttpMethod method) { + public void setMethod(PathItem.HttpMethod method) { this.method = method; } @@ -37,11 +36,11 @@ public void setSummary(String summary) { this.summary = summary; } - public Path getPath() { + public PathItem getPath() { return path; } - public void setPath(Path path) { + public void setPath(PathItem path) { this.path = path; } diff --git a/src/main/java/com/deepoove/swagger/diff/output/HtmlRender.java b/src/main/java/com/deepoove/swagger/diff/output/HtmlRender.java index d7cb699c..8aa04bc8 100644 --- a/src/main/java/com/deepoove/swagger/diff/output/HtmlRender.java +++ b/src/main/java/com/deepoove/swagger/diff/output/HtmlRender.java @@ -2,9 +2,9 @@ import com.deepoove.swagger.diff.SwaggerDiff; import com.deepoove.swagger.diff.model.*; -import io.swagger.models.HttpMethod; -import io.swagger.models.parameters.Parameter; -import io.swagger.models.properties.Property; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; import j2html.tags.ContainerTag; import java.util.ArrayList; @@ -28,7 +28,6 @@ public HtmlRender(String title, String linkCss) { this.linkCss = linkCss; } - public String render(SwaggerDiff diff) { List newEndpoints = diff.getNewEndpoints(); ContainerTag ol_newEndpoint = ol_newEndpoint(newEndpoints); @@ -44,12 +43,13 @@ public String render(SwaggerDiff diff) { return renderHtml(ol_newEndpoint, ol_missingEndpoint, ol_changed, p_versions); } - public String renderHtml(ContainerTag ol_new, ContainerTag ol_miss, ContainerTag ol_changed, ContainerTag p_versions) { + public String renderHtml(ContainerTag ol_new, ContainerTag ol_miss, ContainerTag ol_changed, + ContainerTag p_versions) { ContainerTag html = html().attr("lang", "en").with( head().with( meta().withCharset("utf-8"), title(title), - script(rawHtml("function showHide(id){if(document.getElementById(id).style.display==\'none\'){document.getElementById(id).style.display=\'block\';document.getElementById(\'btn_\'+id).innerHTML=\'⇑\';}else{document.getElementById(id).style.display=\'none\';document.getElementById(\'btn_\'+id).innerHTML=\'⇓\';}return true;}")).withType("text/javascript"), + script(rawHtml("function showHide(id){if(document.getElementById(id).style.display=='none'){document.getElementById(id).style.display='block';document.getElementById('btn_'+id).innerHTML='⇑';}else{document.getElementById(id).style.display='none';document.getElementById('btn_'+id).innerHTML='⇓';}return true;}")).withType("text/javascript"), link().withRel("stylesheet").withHref(linkCss) ), body().with( @@ -62,50 +62,45 @@ public String renderHtml(ContainerTag ol_new, ContainerTag ol_miss, ContainerTag ) ) ); - return document().render() + html.render(); } private ContainerTag div_headArticle(final String title, final String type, final ContainerTag ol) { - return div().with(h2(title).with(a(rawHtml("⇑")).withId("btn_" + type).withClass("showhide").withHref("#").attr("onClick", "javascript:showHide('" + type + "');")), hr(), ol); + return div().with(h2(title).with(a(rawHtml("⇑")).withId("btn_" + type) + .withClass("showhide").withHref("#").attr("onClick", "javascript:showHide('" + type + "')")), + hr(), ol); } private ContainerTag p_versions(String oldVersion, String newVersion) { - ContainerTag p = p().withId("versions"); - p.withText("Changes from " + oldVersion + " to " + newVersion + "."); - return p; + return p().withId("versions").withText("Changes from " + oldVersion + " to " + newVersion + "."); } private ContainerTag ol_newEndpoint(List endpoints) { if (null == endpoints) return ol().withId("new"); ContainerTag ol = ol().withId("new"); for (Endpoint endpoint : endpoints) { - ol.with(li_newEndpoint(endpoint.getMethod().toString(), - endpoint.getPathUrl(), endpoint.getSummary())); + ol.with(li_newEndpoint(endpoint.getMethod().name(), endpoint.getPathUrl(), endpoint.getSummary())); } return ol; } - private ContainerTag li_newEndpoint(String method, String path, - String desc) { - return li().with(span(method).withClass(method)).withText(path + " ") - .with(span(null == desc ? "" : desc)); + private ContainerTag li_newEndpoint(String method, String path, String desc) { + return li().with(span(method).withClass(method.toLowerCase())) + .withText(path + " ").with(span(null == desc ? "" : desc)); } private ContainerTag ol_missingEndpoint(List endpoints) { if (null == endpoints) return ol().withId("deprecated"); ContainerTag ol = ol().withId("deprecated"); for (Endpoint endpoint : endpoints) { - ol.with(li_missingEndpoint(endpoint.getMethod().toString(), - endpoint.getPathUrl(), endpoint.getSummary())); + ol.with(li_missingEndpoint(endpoint.getMethod().name(), endpoint.getPathUrl(), endpoint.getSummary())); } return ol; } - private ContainerTag li_missingEndpoint(String method, String path, - String desc) { - return li().with(span(method).withClass(method), - del().withText(path)).with(span(null == desc ? "" : " " + desc)); + private ContainerTag li_missingEndpoint(String method, String path, String desc) { + return li().with(span(method).withClass(method.toLowerCase()), del().withText(path)) + .with(span(null == desc ? "" : " " + desc)); } private ContainerTag ol_changed(List changedEndpoints) { @@ -113,9 +108,9 @@ private ContainerTag ol_changed(List changedEndpoints) { ContainerTag ol = ol().withId("changed"); for (ChangedEndpoint changedEndpoint : changedEndpoints) { String pathUrl = changedEndpoint.getPathUrl(); - Map changedOperations = changedEndpoint.getChangedOperations(); - for (Entry entry : changedOperations.entrySet()) { - String method = entry.getKey().toString(); + Map changedOperations = changedEndpoint.getChangedOperations(); + for (Entry entry : changedOperations.entrySet()) { + String method = entry.getKey().name(); ChangedOperation changedOperation = entry.getValue(); String desc = changedOperation.getSummary(); @@ -132,56 +127,52 @@ private ContainerTag ol_changed(List changedEndpoints) { if (changedOperation.isDiffConsumes()) { ul_detail.with(li().with(h3("Consumes")).with(ul_consume(changedOperation))); } - ol.with(li().with(span(method).withClass(method)).withText(pathUrl + " ").with(span(null == desc ? "" : desc)) - .with(ul_detail)); + if (isDiffResponses(changedOperation)) { + ul_detail.with(li().with(h3("Responses")).with(ul_responses(changedOperation))); + } + ol.with(li().with(span(method).withClass(method.toLowerCase())) + .withText(pathUrl + " ").with(span(null == desc ? "" : desc)) + .with(ul_detail)); } } return ol; } + @SuppressWarnings("rawtypes") private ContainerTag ul_response(ChangedOperation changedOperation) { List addProps = changedOperation.getAddProps(); List delProps = changedOperation.getMissingProps(); List chgProps = changedOperation.getChangedProps(); ContainerTag ul = ul().withClass("change response"); - for (ElProperty prop : addProps) { - ul.with(li_addProp(prop)); - } - for (ElProperty prop : delProps) { - ul.with(li_missingProp(prop)); - } - for (ElProperty prop : chgProps) { - ul.with(li_changedProp(prop)); - } + for (ElProperty prop : addProps) ul.with(li_addProp(prop)); + for (ElProperty prop : delProps) ul.with(li_missingProp(prop)); + for (ElProperty prop : chgProps) ul.with(li_changedProp(prop)); return ul; } + @SuppressWarnings("rawtypes") private ContainerTag li_missingProp(ElProperty prop) { - Property property = prop.getProperty(); - return li().withClass("missing").withText("Delete").with(del(prop.getEl())).with(span(null == property.getDescription() ? "" : ("//" + property.getDescription())).withClass("comment")); + Schema property = prop.getProperty(); + String desc = property != null ? property.getDescription() : null; + return li().withClass("missing").withText("Delete").with(del(prop.getEl())) + .with(span(null == desc ? "" : ("//" + desc)).withClass("comment")); } + @SuppressWarnings("rawtypes") private ContainerTag li_addProp(ElProperty prop) { - Property property = prop.getProperty(); - return li().withText("Add " + prop.getEl()).with(span(null == property.getDescription() ? "" : ("//" + property.getDescription())).withClass("comment")); + Schema property = prop.getProperty(); + String desc = property != null ? property.getDescription() : null; + return li().withText("Add " + prop.getEl()) + .with(span(null == desc ? "" : ("//" + desc)).withClass("comment")); } private ContainerTag li_changedProp(ElProperty prop) { List changeDetails = new ArrayList<>(); - String changeDetailsHeading = ""; - if (prop.isTypeChange()) { - changeDetails.add("Data Type"); - } - if (prop.isNewEnums()) { - changeDetails.add("Added Enum"); - } - if (prop.isRemovedEnums()) { - changeDetails.add("Removed Enum"); - } - if (! changeDetails.isEmpty()) { - changeDetailsHeading = " (" + String.join(", ", changeDetails) + ")"; - } - return li().withText("Change " + prop.getEl()).with(span(changeDetailsHeading).withClass("comment")); + if (prop.isTypeChange()) changeDetails.add("Data Type"); + if (prop.isNewEnums()) changeDetails.add("Added Enum"); + if (prop.isRemovedEnums()) changeDetails.add("Removed Enum"); + String heading = changeDetails.isEmpty() ? "" : " (" + String.join(", ", changeDetails) + ")"; + return li().withText("Change " + prop.getEl()).with(span(heading).withClass("comment")); } private ContainerTag ul_param(ChangedOperation changedOperation) { @@ -189,85 +180,59 @@ private ContainerTag ul_param(ChangedOperation changedOperation) { List delParameters = changedOperation.getMissingParameters(); List changedParameters = changedOperation.getChangedParameter(); ContainerTag ul = ul().withClass("change param"); - for (Parameter param : addParameters) { - ul.with(li_addParam(param)); - } + for (Parameter param : addParameters) ul.with(li_addParam(param)); for (ChangedParameter param : changedParameters) { - List increased = param.getIncreased(); - for (ElProperty prop : increased) { - ul.with(li_addProp(prop)); - } + for (ElProperty prop : param.getIncreased()) ul.with(li_addProp(prop)); } for (ChangedParameter param : changedParameters) { - boolean changeRequired = param.isChangeRequired(); - boolean changeDescription = param.isChangeDescription(); - if (changeRequired || changeDescription) - ul.with(li_changedParam(param)); + if (param.isChangeRequired() || param.isChangeDescription()) ul.with(li_changedParam(param)); } for (ChangedParameter param : changedParameters) { - List missing = param.getMissing(); - for (ElProperty prop : missing) { - ul.with(li_missingProp(prop)); - } - } - for (ChangedParameter param : changedParameters) { - List changed = param.getChanged(); - for (ElProperty prop : changed) { - ul.with(li_changedProp(prop)); - } - } - for (Parameter param : delParameters) { - ul.with(li_missingParam(param)); + for (ElProperty prop : param.getMissing()) ul.with(li_missingProp(prop)); + for (ElProperty prop : param.getChanged()) ul.with(li_changedProp(prop)); } + for (Parameter param : delParameters) ul.with(li_missingParam(param)); return ul; } private ContainerTag li_addParam(Parameter param) { - return li().withText("Add " + param.getName()).with(span(null == param.getDescription() ? "" : ("//" + param.getDescription())).withClass("comment")); + return li().withText("Add " + param.getName()) + .with(span(null == param.getDescription() ? "" : ("//" + param.getDescription())).withClass("comment")); } private ContainerTag li_missingParam(Parameter param) { - return li().withClass("missing").with(span("Delete")).with(del(param.getName())).with(span(null == param.getDescription() ? "" : ("//" + param.getDescription())).withClass("comment")); + return li().withClass("missing").with(span("Delete")).with(del(param.getName())) + .with(span(null == param.getDescription() ? "" : ("//" + param.getDescription())).withClass("comment")); } private ContainerTag li_changedParam(ChangedParameter changeParam) { - boolean changeRequired = changeParam.isChangeRequired(); - boolean changeDescription = changeParam.isChangeDescription(); Parameter rightParam = changeParam.getRightParameter(); Parameter leftParam = changeParam.getLeftParameter(); ContainerTag li = li().withText(rightParam.getName()); - if (changeRequired) { - li.withText(" change into " + (rightParam.getRequired() ? "required" : "not required")); + if (changeParam.isChangeRequired()) { + boolean required = Boolean.TRUE.equals(rightParam.getRequired()); + li.withText(" change into " + (required ? "required" : "not required")); } - if (changeDescription) { - li.withText(" Notes ").with(del(leftParam.getDescription()).withClass("comment")).withText(" change into ").with(span(span(null == rightParam.getDescription() ? "" : rightParam.getDescription()).withClass("comment"))); + if (changeParam.isChangeDescription()) { + li.withText(" Notes ") + .with(del(leftParam.getDescription()).withClass("comment")) + .withText(" change into ") + .with(span(span(null == rightParam.getDescription() ? "" : rightParam.getDescription()).withClass("comment"))); } return li; } private ContainerTag ul_produce(ChangedOperation changedOperation) { - List addProduce = changedOperation.getAddProduces(); - List delProduce = changedOperation.getMissingProduces(); ContainerTag ul = ul().withClass("change produces"); - for (String mt : addProduce) { - ul.with(li_addMediaType(mt)); - } - for (String mt : delProduce) { - ul.with(li_missingMediaType(mt)); - } + for (String mt : changedOperation.getAddProduces()) ul.with(li_addMediaType(mt)); + for (String mt : changedOperation.getMissingProduces()) ul.with(li_missingMediaType(mt)); return ul; } private ContainerTag ul_consume(ChangedOperation changedOperation) { - List addConsume = changedOperation.getAddConsumes(); - List delConsume = changedOperation.getMissingConsumes(); ContainerTag ul = ul().withClass("change consumes"); - for (String mt : addConsume) { - ul.with(li_addMediaType(mt)); - } - for (String mt : delConsume) { - ul.with(li_missingMediaType(mt)); - } + for (String mt : changedOperation.getAddConsumes()) ul.with(li_addMediaType(mt)); + for (String mt : changedOperation.getMissingConsumes()) ul.with(li_missingMediaType(mt)); return ul; } @@ -278,4 +243,57 @@ private ContainerTag li_missingMediaType(String type) { private ContainerTag li_addMediaType(String type) { return li().withText("Add " + type).with(span("")); } + + private boolean isDiffResponses(ChangedOperation op) { + return !op.getAddResponses().isEmpty() + || !op.getMissingResponses().isEmpty() + || !op.getChangedResponses().isEmpty(); +} + + @SuppressWarnings("rawtypes") + private ContainerTag ul_responses(ChangedOperation op) { + ContainerTag ul = ul().withClass("change responses"); + + // status aggiunti + for (Map.Entry e : op.getAddResponses().entrySet()) { + String desc = e.getValue() != null ? e.getValue().getDescription() : null; + ul.with(li().withText("Add response " + e.getKey()) + .with(span(null == desc ? "" : "//" + desc).withClass("comment"))); + } + + // status rimossi + for (Map.Entry e : op.getMissingResponses().entrySet()) { + String desc = e.getValue() != null ? e.getValue().getDescription() : null; + ul.with(li().withClass("missing").withText("Delete response ").with(del(e.getKey())) + .with(span(null == desc ? "" : "//" + desc).withClass("comment"))); + } + + // status modificati + for (ChangedResponse cr : op.getChangedResponses()) { + ContainerTag sub = ul().withClass("change response-detail"); + + if (cr.isDescriptionChanged()) { + sub.with(li().withText("Description ") + .with(del(cr.getOldDescription() == null ? "" : cr.getOldDescription()).withClass("comment")) + .withText(" change into ") + .with(span(cr.getNewDescription() == null ? "" : cr.getNewDescription()).withClass("comment"))); + } + for (ElProperty p : cr.getAddProps()) sub.with(li_addProp(p)); + for (ElProperty p : cr.getMissingProps()) sub.with(li_missingProp(p)); + for (ElProperty p : cr.getChangedProps()) sub.with(li_changedProp(p)); + + for (String ct : cr.getAddContentTypes()) sub.with(li().withText("Add content-type " + ct)); + for (String ct : cr.getMissingContentTypes()) + sub.with(li().withClass("missing").withText("Delete content-type ").with(del(ct))); + + for (String h : cr.getAddHeaders().keySet()) sub.with(li().withText("Add header " + h)); + for (String h : cr.getMissingHeaders().keySet()) + sub.with(li().withClass("missing").withText("Delete header ").with(del(h))); + for (String h : cr.getChangedHeaders()) sub.with(li().withText("Change header " + h)); + + ul.with(li().withText("Response " + cr.getStatusCode()).with(sub)); + } + + return ul; + } } diff --git a/src/main/java/com/deepoove/swagger/diff/output/MarkdownRender.java b/src/main/java/com/deepoove/swagger/diff/output/MarkdownRender.java index 975e3bea..d1208a3f 100644 --- a/src/main/java/com/deepoove/swagger/diff/output/MarkdownRender.java +++ b/src/main/java/com/deepoove/swagger/diff/output/MarkdownRender.java @@ -7,9 +7,9 @@ import com.deepoove.swagger.diff.SwaggerDiff; import com.deepoove.swagger.diff.model.*; -import io.swagger.models.HttpMethod; -import io.swagger.models.parameters.Parameter; -import io.swagger.models.properties.Property; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; public class MarkdownRender implements Render { @@ -25,259 +25,216 @@ public class MarkdownRender implements Render { public MarkdownRender() {} public String render(SwaggerDiff diff) { - List newEndpoints = diff.getNewEndpoints(); - String ol_newEndpoint = ol_newEndpoint(newEndpoints); - - List missingEndpoints = diff.getMissingEndpoints(); - String ol_missingEndpoint = ol_missingEndpoint(missingEndpoints); - - List changedEndpoints = diff.getChangedEndpoints(); - String ol_changed = ol_changed(changedEndpoints); - - return renderHtml(diff.getOldVersion(), diff.getNewVersion(), ol_newEndpoint, ol_missingEndpoint, ol_changed); + String ol_new = ol_newEndpoint(diff.getNewEndpoints()); + String ol_miss = ol_missingEndpoint(diff.getMissingEndpoints()); + String ol_changed = ol_changed(diff.getChangedEndpoints()); + return renderHtml(diff.getOldVersion(), diff.getNewVersion(), ol_new, ol_miss, ol_changed); } - public String renderHtml(String oldVersion, String newVersion, String ol_new, String ol_miss, - String ol_changed) { - StringBuffer sb = new StringBuffer(); - sb.append(H2).append("Version " + oldVersion + " to " + newVersion).append("\n").append(HR); - sb.append(H3).append("What's New").append("\n").append(HR) - .append(ol_new).append("\n").append(H3) - .append("What's Deprecated").append("\n").append(HR) - .append(ol_miss).append("\n").append(H3) - .append("What's Changed").append("\n").append(HR) - .append(ol_changed); - return sb.toString(); + public String renderHtml(String oldVersion, String newVersion, String ol_new, + String ol_miss, String ol_changed) { + return new StringBuilder() + .append(H2).append("Version ").append(oldVersion).append(" to ").append(newVersion).append("\n").append(HR) + .append(H3).append("What's New").append("\n").append(HR).append(ol_new).append("\n") + .append(H3).append("What's Deprecated").append("\n").append(HR).append(ol_miss).append("\n") + .append(H3).append("What's Changed").append("\n").append(HR).append(ol_changed) + .toString(); } private String ol_newEndpoint(List endpoints) { if (null == endpoints) return ""; - StringBuffer sb = new StringBuffer(); - for (Endpoint endpoint : endpoints) { - sb.append(li_newEndpoint(endpoint.getMethod().toString(), - endpoint.getPathUrl(), endpoint.getSummary())); + StringBuilder sb = new StringBuilder(); + for (Endpoint e : endpoints) { + sb.append(li_newEndpoint(e.getMethod().name(), e.getPathUrl(), e.getSummary())); } return sb.toString(); } private String li_newEndpoint(String method, String path, String desc) { - StringBuffer sb = new StringBuffer(); - sb.append(LI).append(CODE).append(method).append(CODE) - .append(" " + path).append(" " + desc + "\n"); - return sb.toString(); + return LI + CODE + method + CODE + " " + path + " " + (desc != null ? desc : "") + "\n"; } private String ol_missingEndpoint(List endpoints) { if (null == endpoints) return ""; - StringBuffer sb = new StringBuffer(); - for (Endpoint endpoint : endpoints) { - sb.append(li_newEndpoint(endpoint.getMethod().toString(), - endpoint.getPathUrl(), endpoint.getSummary())); + StringBuilder sb = new StringBuilder(); + for (Endpoint e : endpoints) { + sb.append(li_newEndpoint(e.getMethod().name(), e.getPathUrl(), e.getSummary())); } return sb.toString(); } private String ol_changed(List changedEndpoints) { if (null == changedEndpoints) return ""; - StringBuffer sb = new StringBuffer(); + StringBuilder sb = new StringBuilder(); for (ChangedEndpoint changedEndpoint : changedEndpoints) { String pathUrl = changedEndpoint.getPathUrl(); - Map changedOperations = changedEndpoint - .getChangedOperations(); - for (Entry entry : changedOperations - .entrySet()) { - String method = entry.getKey().toString(); - ChangedOperation changedOperation = entry.getValue(); - String desc = changedOperation.getSummary(); - - StringBuffer ul_detail = new StringBuffer(); - if (changedOperation.isDiffParam()) { - ul_detail.append(PRE_LI).append("Parameters") - .append(ul_param(changedOperation)); - } - if (changedOperation.isDiffProp()) { - ul_detail.append(PRE_LI).append("Return Type") - .append(ul_response(changedOperation)); - } - if (changedOperation.isDiffProduces()) { - ul_detail.append(PRE_LI).append("Produces") - .append(ul_produce(changedOperation)); - } - if (changedOperation.isDiffConsumes()) { - ul_detail.append(PRE_LI).append("Consumes") - .append(ul_consume(changedOperation)); - } - sb.append(CODE).append(method).append(CODE) - .append(" " + pathUrl).append(" " + desc + " \n") - .append(ul_detail); + Map changedOps = changedEndpoint.getChangedOperations(); + for (Entry entry : changedOps.entrySet()) { + String method = entry.getKey().name(); + ChangedOperation op = entry.getValue(); + StringBuilder detail = new StringBuilder(); + if (op.isDiffParam()) detail.append(PRE_LI).append("Parameters").append(ul_param(op)); + if (op.isDiffProp()) detail.append(PRE_LI).append("Return Type").append(ul_response(op)); + if (op.isDiffProduces()) detail.append(PRE_LI).append("Produces").append(ul_produce(op)); + if (op.isDiffConsumes()) detail.append(PRE_LI).append("Consumes").append(ul_consume(op)); + if (isDiffResponses(op)) detail.append(PRE_LI).append("Responses").append(ul_responses(op)); + sb.append(CODE).append(method).append(CODE).append(" ").append(pathUrl) + .append(" ").append(op.getSummary() != null ? op.getSummary() : "").append(" \n") + .append(detail); } } return sb.toString(); } - private String ul_response(ChangedOperation changedOperation) { - List addProps = changedOperation.getAddProps(); - List delProps = changedOperation.getMissingProps(); - List changedProps = changedOperation.getChangedProps(); - StringBuffer sb = new StringBuffer("\n\n"); - - String prefix = PRE_LI + PRE_CODE; - for (ElProperty prop : addProps) { - sb.append(PRE_LI).append(PRE_CODE).append(li_addProp(prop) + "\n"); - } - for (ElProperty prop : delProps) { - sb.append(prefix).append(li_missingProp(prop) + "\n"); - } - for (ElProperty prop : changedProps) { - sb.append(prefix).append(li_changedProp(prop) + "\n"); - } + @SuppressWarnings("rawtypes") + private String ul_response(ChangedOperation op) { + StringBuilder sb = new StringBuilder("\n\n"); + for (ElProperty p : op.getAddProps()) sb.append(PRE_LI).append(PRE_CODE).append(li_addProp(p)).append("\n"); + for (ElProperty p : op.getMissingProps()) sb.append(PRE_LI).append(PRE_CODE).append(li_missingProp(p)).append("\n"); + for (ElProperty p : op.getChangedProps()) sb.append(PRE_LI).append(PRE_CODE).append(li_changedProp(p)).append("\n"); return sb.toString(); } + @SuppressWarnings("rawtypes") private String li_missingProp(ElProperty prop) { - Property property = prop.getProperty(); - StringBuffer sb = new StringBuffer(""); - sb.append("Delete ").append(prop.getEl()) - .append(null == property.getDescription() ? "" - : (" //" + property.getDescription())); - return sb.toString(); + Schema schema = prop.getProperty(); + String desc = schema != null ? schema.getDescription() : null; + return "Delete " + prop.getEl() + (null == desc ? "" : " //" + desc); } + @SuppressWarnings("rawtypes") private String li_addProp(ElProperty prop) { - Property property = prop.getProperty(); - StringBuffer sb = new StringBuffer(""); - sb.append("Insert ").append(prop.getEl()) - .append(null == property.getDescription() ? "" - : (" //" + property.getDescription())); - return sb.toString(); + Schema schema = prop.getProperty(); + String desc = schema != null ? schema.getDescription() : null; + return "Insert " + prop.getEl() + (null == desc ? "" : " //" + desc); } + @SuppressWarnings("rawtypes") private String li_changedProp(ElProperty prop) { - Property property = prop.getProperty(); - String prefix = "Modify "; - String desc = " //" + property.getDescription(); - String postfix = (null == property.getDescription() ? "" : desc); - - StringBuffer sb = new StringBuffer(""); - sb.append(prefix).append(prop.getEl()) - .append(postfix); - return sb.toString(); + Schema schema = prop.getProperty(); + String desc = schema != null ? schema.getDescription() : null; + return "Modify " + prop.getEl() + (null == desc ? "" : " //" + desc); } - private String ul_param(ChangedOperation changedOperation) { - List addParameters = changedOperation.getAddParameters(); - List delParameters = changedOperation.getMissingParameters(); - List changedParameters = changedOperation - .getChangedParameter(); - StringBuffer sb = new StringBuffer("\n\n"); - for (Parameter param : addParameters) { - sb.append(PRE_LI).append(PRE_CODE) - .append(li_addParam(param) + "\n"); + private String ul_param(ChangedOperation op) { + StringBuilder sb = new StringBuilder("\n\n"); + for (Parameter p : op.getAddParameters()) sb.append(PRE_LI).append(PRE_CODE).append(li_addParam(p)).append("\n"); + for (ChangedParameter cp : op.getChangedParameter()) { + for (ElProperty prop : cp.getIncreased()) sb.append(PRE_LI).append(PRE_CODE).append(li_addProp(prop)).append("\n"); } - for (ChangedParameter param : changedParameters) { - List increased = param.getIncreased(); - for (ElProperty prop : increased) { - sb.append(PRE_LI).append(PRE_CODE) - .append(li_addProp(prop) + "\n"); + for (ChangedParameter cp : op.getChangedParameter()) { + if (cp.isChangeRequired() || cp.isChangeDescription()) { + sb.append(PRE_LI).append(PRE_CODE).append(li_changedParam(cp)).append("\n"); } } - for (ChangedParameter param : changedParameters) { - boolean changeRequired = param.isChangeRequired(); - boolean changeDescription = param.isChangeDescription(); - if (changeRequired || changeDescription) sb.append(PRE_LI) - .append(PRE_CODE).append(li_changedParam(param) + "\n"); - } - for (ChangedParameter param : changedParameters) { - List missing = param.getMissing(); - List changed = param.getChanged(); - for (ElProperty prop : missing) { - sb.append(PRE_LI).append(PRE_CODE) - .append(li_missingProp(prop) + "\n"); - } - for (ElProperty prop : changed) { - sb.append(PRE_LI).append(PRE_CODE) - .append(li_changedProp(prop) + "\n"); - } - } - for (Parameter param : delParameters) { - sb.append(PRE_LI).append(PRE_CODE) - .append(li_missingParam(param) + "\n"); + for (ChangedParameter cp : op.getChangedParameter()) { + for (ElProperty prop : cp.getMissing()) sb.append(PRE_LI).append(PRE_CODE).append(li_missingProp(prop)).append("\n"); + for (ElProperty prop : cp.getChanged()) sb.append(PRE_LI).append(PRE_CODE).append(li_changedProp(prop)).append("\n"); } + for (Parameter p : op.getMissingParameters()) sb.append(PRE_LI).append(PRE_CODE).append(li_missingParam(p)).append("\n"); return sb.toString(); } private String li_addParam(Parameter param) { - StringBuffer sb = new StringBuffer(""); - sb.append("Add ").append(param.getName()) - .append(null == param.getDescription() ? "" - : (" //" + param.getDescription())); - return sb.toString(); + return "Add " + param.getName() + (null == param.getDescription() ? "" : " //" + param.getDescription()); } private String li_missingParam(Parameter param) { - StringBuffer sb = new StringBuffer(""); - sb.append("Delete ").append(param.getName()) - .append(null == param.getDescription() ? "" - : (" //" + param.getDescription())); - return sb.toString(); + return "Delete " + param.getName() + (null == param.getDescription() ? "" : " //" + param.getDescription()); } - private String li_changedParam(ChangedParameter changeParam) { - boolean changeRequired = changeParam.isChangeRequired(); - boolean changeDescription = changeParam.isChangeDescription(); - Parameter rightParam = changeParam.getRightParameter(); - Parameter leftParam = changeParam.getLeftParameter(); - StringBuffer sb = new StringBuffer(""); - sb.append(rightParam.getName()); - if (changeRequired) { - sb.append(" change into " + (rightParam.getRequired() ? "required" : "not required")); + private String li_changedParam(ChangedParameter cp) { + Parameter right = cp.getRightParameter(); + Parameter left = cp.getLeftParameter(); + StringBuilder sb = new StringBuilder(right.getName()); + if (cp.isChangeRequired()) { + sb.append(" change into ").append(Boolean.TRUE.equals(right.getRequired()) ? "required" : "not required"); } - if (changeDescription) { - sb.append(" Notes ").append(leftParam.getDescription()).append(" change into ") - .append(rightParam.getDescription()); + if (cp.isChangeDescription()) { + sb.append(" Notes ").append(left.getDescription()).append(" change into ").append(right.getDescription()); } return sb.toString(); } - private String ul_produce(ChangedOperation changedOperation) { - List addProduce = changedOperation.getAddProduces(); - List delProduce = changedOperation.getMissingProduces(); - StringBuffer sb = new StringBuffer("\n\n"); + private String ul_produce(ChangedOperation op) { + StringBuilder sb = new StringBuilder("\n\n"); + for (String mt : op.getAddProduces()) sb.append(PRE_LI).append(PRE_CODE).append("Insert ").append(mt).append("\n"); + for (String mt : op.getMissingProduces()) sb.append(PRE_LI).append(PRE_CODE).append("Delete ").append(mt).append("\n"); + return sb.toString(); + } - String prefix = PRE_LI + PRE_CODE; - for (String mt : addProduce) { - sb.append(PRE_LI).append(PRE_CODE).append(li_addMediaType(mt) + "\n"); - } - for (String mt : delProduce) { - sb.append(prefix).append(li_missingMediaType(mt) + "\n"); - } + private String ul_consume(ChangedOperation op) { + StringBuilder sb = new StringBuilder("\n\n"); + for (String mt : op.getAddConsumes()) sb.append(PRE_LI).append(PRE_CODE).append("Insert ").append(mt).append("\n"); + for (String mt : op.getMissingConsumes()) sb.append(PRE_LI).append(PRE_CODE).append("Delete ").append(mt).append("\n"); return sb.toString(); } - private String ul_consume(ChangedOperation changedOperation) { - List addConsume = changedOperation.getAddConsumes(); - List delConsume = changedOperation.getMissingConsumes(); - StringBuffer sb = new StringBuffer("\n\n"); + private boolean isDiffResponses(ChangedOperation op) { + return !op.getAddResponses().isEmpty() + || !op.getMissingResponses().isEmpty() + || !op.getChangedResponses().isEmpty(); + } + + private String ul_responses(ChangedOperation op) { + StringBuilder sb = new StringBuilder("\n\n"); - String prefix = PRE_LI + PRE_CODE; - for (String mt : addConsume) { - sb.append(PRE_LI).append(PRE_CODE).append(li_addMediaType(mt) + "\n"); + // status aggiunti + for (Map.Entry e : op.getAddResponses().entrySet()) { + String desc = e.getValue() != null ? e.getValue().getDescription() : null; + sb.append(PRE_LI).append(PRE_CODE) + .append("Insert response ").append(e.getKey()) + .append(null == desc ? "" : " //" + desc) + .append("\n"); } - for (String mt : delConsume) { - sb.append(prefix).append(li_missingMediaType(mt) + "\n"); + + // status rimossi + for (Map.Entry e : op.getMissingResponses().entrySet()) { + String desc = e.getValue() != null ? e.getValue().getDescription() : null; + sb.append(PRE_LI).append(PRE_CODE) + .append("Delete response ").append(e.getKey()) + .append(null == desc ? "" : " //" + desc) + .append("\n"); } - return sb.toString(); - } - private String li_missingMediaType(String type) { - StringBuffer sb = new StringBuffer(""); - sb.append("Delete ").append(type); - return sb.toString(); - } + // status modificati + for (ChangedResponse cr : op.getChangedResponses()) { + sb.append(PRE_LI).append(PRE_CODE) + .append("Modify response ").append(cr.getStatusCode()).append("\n"); + + // description + if (cr.isDescriptionChanged()) { + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE) + .append("Description ") + .append(cr.getOldDescription() != null ? cr.getOldDescription() : "") + .append(" change into ") + .append(cr.getNewDescription() != null ? cr.getNewDescription() : "") + .append("\n"); + } + + // schema (proprietà) + for (ElProperty p : cr.getAddProps()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append(li_addProp(p)).append("\n"); + for (ElProperty p : cr.getMissingProps()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append(li_missingProp(p)).append("\n"); + for (ElProperty p : cr.getChangedProps()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append(li_changedProp(p)).append("\n"); + + // content-type + for (String ct : cr.getAddContentTypes()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append("Insert content-type ").append(ct).append("\n"); + for (String ct : cr.getMissingContentTypes()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append("Delete content-type ").append(ct).append("\n"); + + // headers + for (String h : cr.getAddHeaders().keySet()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append("Insert header ").append(h).append("\n"); + for (String h : cr.getMissingHeaders().keySet()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append("Delete header ").append(h).append("\n"); + for (String h : cr.getChangedHeaders()) + sb.append(PRE_LI).append(PRE_CODE).append(PRE_CODE).append("Modify header ").append(h).append("\n"); + } - private String li_addMediaType(String type) { - StringBuffer sb = new StringBuffer(""); - sb.append("Insert ").append(type); return sb.toString(); } } diff --git a/src/test/.DS_Store b/src/test/.DS_Store new file mode 100644 index 00000000..53fb0515 Binary files /dev/null and b/src/test/.DS_Store differ diff --git a/src/test/java/com/deepoove/swagger/test/CLITest.java b/src/test/java/com/deepoove/swagger/test/CLITest.java index f250b018..4919c698 100644 --- a/src/test/java/com/deepoove/swagger/test/CLITest.java +++ b/src/test/java/com/deepoove/swagger/test/CLITest.java @@ -1,6 +1,8 @@ package com.deepoove.swagger.test; import java.io.ByteArrayOutputStream; +import java.io.FileWriter; +import java.io.IOException; import java.io.PrintStream; import org.junit.After; @@ -10,7 +12,10 @@ import com.beust.jcommander.JCommander; import com.beust.jcommander.ParameterException; +import com.deepoove.swagger.diff.SwaggerDiff; import com.deepoove.swagger.diff.cli.CLI; +import com.deepoove.swagger.diff.output.HtmlRender; +import com.deepoove.swagger.diff.output.MarkdownRender; public class CLITest { @@ -83,7 +88,7 @@ public void testVersion() { JCommander jCommander = JCommander.newBuilder().addObject(cli).build(); jCommander.parse(argv); cli.run(jCommander); - Assert.assertEquals(outContent.toString().trim(), "1.2.2"); + Assert.assertEquals("1.2.2-oas3", outContent.toString().trim()); } @Test @@ -93,7 +98,56 @@ public void testMain() { JCommander jCommander = JCommander.newBuilder().addObject(cli).build(); jCommander.parse(argv); cli.run(jCommander); + //System.setOut(System.out); // ripristina per vedere + //String out = outContent.toString(); + //System.err.println("=== OUTPUT ===\n" + out + "\n=== FINE ==="); + //Assert.assertTrue(out.startsWith("## Version 1.0.0 to 1.0.2")); Assert.assertTrue(outContent.toString().startsWith("## Version 1.0.0 to 1.0.2")); } + @Test + public void testResponseRenderMarkdown() { + SwaggerDiff diff = SwaggerDiff.compareV3( + "src/test/resources/petstore_v3_diff1.json", + "src/test/resources/petstore_v3_diff2.json" + ); + String md = new MarkdownRender().render(diff); + + Assert.assertTrue("manca sezione Responses", md.contains("Responses")); + Assert.assertTrue("manca 201 aggiunto", md.contains("Insert response 201")); + Assert.assertTrue("manca 404 rimosso", md.contains("Delete response 404")); + } + + @Test + public void testResponseRenderHtml() { + SwaggerDiff diff = SwaggerDiff.compareV3( + "src/test/resources/petstore_v3_diff1.json", + "src/test/resources/petstore_v3_diff2.json" + ); + String html = new HtmlRender("Changelog", + "http://deepoove.com/swagger-diff/stylesheets/demo.css") + .render(diff); + + // (opzionale) scrivi su file per ispezione visiva + try (FileWriter fw = new FileWriter("testResponseRender.html")) { + fw.write(html); + } catch (IOException e) { + e.printStackTrace(); + } + + // sezione Responses presente + Assert.assertTrue("manca header Responses", html.contains("

    Responses

    ")); + + // status aggiunti (201 e 400 nei tuoi dati) + Assert.assertTrue("manca 201 aggiunto", html.contains("Add response 201")); + Assert.assertTrue("manca 400 aggiunto", html.contains("Add response 400")); + + // status rimosso (404) + Assert.assertTrue("manca 404 rimosso", + html.contains("Delete response") && html.contains("404")); + + // status 200 modificato (description cambiata ok -> update Success) + Assert.assertTrue("manca riferimento al Response 200", html.contains("Response 200")); + } + } diff --git a/src/test/java/com/deepoove/swagger/test/SwaggerDiffTest.java b/src/test/java/com/deepoove/swagger/test/SwaggerDiffTest.java index 6405ab59..59f956a0 100644 --- a/src/test/java/com/deepoove/swagger/test/SwaggerDiffTest.java +++ b/src/test/java/com/deepoove/swagger/test/SwaggerDiffTest.java @@ -8,14 +8,17 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; -import io.swagger.models.HttpMethod; -import io.swagger.models.parameters.BodyParameter; +import io.swagger.v3.oas.models.PathItem.HttpMethod; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.parameters.Parameter; import org.junit.Assert; import org.junit.Test; import java.io.*; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -27,6 +30,14 @@ public class SwaggerDiffTest { final String SWAGGER_V2_EMPTY_DOC = "petstore_v2_empty.json"; final String SWAGGER_V2_HTTP = "http://petstore.swagger.io/v2/swagger.json"; + final String SWAGGER_V3_EMPTY_DOC = "petstore_v3_empty.json"; + + @Test + public void testEqualV3() { + SwaggerDiff diff = SwaggerDiff.compareV3(SWAGGER_V3_EMPTY_DOC, SWAGGER_V3_EMPTY_DOC); + assertEqual(diff); + } + @Test public void testEqual() { SwaggerDiff diff = SwaggerDiff.compareV2(SWAGGER_V2_DOC2, SWAGGER_V2_DOC2); @@ -160,7 +171,7 @@ public void testEqualJson() { try { InputStream inputStream = getClass().getClassLoader().getResourceAsStream(SWAGGER_V2_DOC1); JsonNode json = new ObjectMapper().readTree(inputStream); - SwaggerDiff diff = SwaggerDiff.compareV2(json, json); + SwaggerDiff diff = SwaggerDiff.compareRaw(json.toString(), json.toString()); assertEqual(diff); } catch (IOException e) { e.printStackTrace(); @@ -205,20 +216,18 @@ public void testInputBodyArray() { Assert.assertTrue("Expecting POST method change", endpoint.getChangedOperations().containsKey(HttpMethod.POST)); Assert.assertEquals(0, endpoint.getChangedOperations().get(HttpMethod.POST).getMissingParameters().size()); Assert.assertEquals(0, endpoint.getChangedOperations().get(HttpMethod.POST).getAddParameters().size()); - Assert.assertEquals(1, endpoint.getChangedOperations().get(HttpMethod.POST).getChangedParameter().size()); + Assert.assertEquals(0, endpoint.getChangedOperations().get(io.swagger.v3.oas.models.PathItem.HttpMethod.POST).getChangedParameter().size()); // assert changed property counts - ChangedParameter changedInput = endpoint.getChangedOperations().get(HttpMethod.POST).getChangedParameter().get(0); - Assert.assertTrue(changedInput.getLeftParameter() instanceof BodyParameter); - Assert.assertTrue(changedInput.getRightParameter() instanceof BodyParameter); - Assert.assertEquals(3, changedInput.getIncreased().size()); - Assert.assertEquals(3, changedInput.getMissing().size()); - Assert.assertEquals(1, changedInput.getChanged().size()); + ChangedOperation changedOp = endpoint.getChangedOperations().get(io.swagger.v3.oas.models.PathItem.HttpMethod.POST); + Assert.assertEquals(3, changedOp.getAddRequestProps().size()); + Assert.assertEquals(3, changedOp.getMissingRequestProps().size()); + Assert.assertEquals(0, changedOp.getChangedRequestProps().size()); // assert embedded array change is one of the missing properties - List missingProperties = changedInput.getMissing(); + List missingProperties = changedOp.getMissingRequestProps(); Set elementPaths = missingProperties.stream().map(ElProperty::getEl).collect(Collectors.toSet()); - Assert.assertTrue(elementPaths.contains("body.favorite.tags.removedField")); + Assert.assertTrue(elementPaths.contains("favorite.tags.removedField") || elementPaths.contains("body.favorite.tags.removedField")); }); } @@ -236,7 +245,7 @@ public void testResponseBodyArray() { ChangedOperation changedOutput = endpoint.getChangedOperations().get(HttpMethod.GET); Assert.assertEquals(3, changedOutput.getAddProps().size()); Assert.assertEquals(3, changedOutput.getMissingProps().size()); - Assert.assertEquals(1, changedOutput.getChangedProps().size()); + Assert.assertEquals(0, changedOutput.getChangedProps().size()); // assert embedded array change is one of the missing properties List missingProperties =changedOutput.getMissingProps(); @@ -269,17 +278,15 @@ public void testChangedPropertyMetadata() { Assert.assertTrue("Expecting changed endpoint " + postOrder, changedEndpointMap.containsKey(postOrder)); ChangedEndpoint postOrderChg = changedEndpointMap.get(postOrder); ChangedOperation postOrderChgOp = postOrderChg.getChangedOperations().get(HttpMethod.POST); - Assert.assertEquals(1, postOrderChgOp.getChangedParameter().size()); - - List postChgProps = postOrderChgOp.getChangedParameter().get(0).getChanged(); + List postChgProps = postOrderChgOp.getChangedRequestProps(); Assert.assertEquals(2, postChgProps.size()); ElProperty orderIdProp = postChgProps.stream().filter(cp -> { - return cp.getEl().equalsIgnoreCase("body.id");}).findFirst().get(); + return cp.getEl().equalsIgnoreCase("body.id") || cp.getEl().equalsIgnoreCase("id");}).findFirst().get(); Assert.assertTrue(orderIdProp.isTypeChange()); Assert.assertFalse(orderIdProp.isNewEnums()); Assert.assertFalse(orderIdProp.isRemovedEnums()); ElProperty statusProp = postChgProps.stream().filter(cp -> { - return cp.getEl().equalsIgnoreCase("body.status");}).findFirst().get(); + return cp.getEl().equalsIgnoreCase("body.status") || cp.getEl().equalsIgnoreCase("status");}).findFirst().get(); Assert.assertFalse(statusProp.isTypeChange()); Assert.assertTrue(statusProp.isNewEnums()); Assert.assertTrue(statusProp.isRemovedEnums()); @@ -312,5 +319,144 @@ private void assertEqual(SwaggerDiff diff) { Assert.assertTrue(changedEndPoints.isEmpty()); } + @Test + public void x() { + SwaggerDiff diff = SwaggerDiff.compareV3( + "src/test/resources/petstore_v3_diff1.json", + "src/test/resources/petstore_v3_diff2.json" + ); + + List changedEndpoints = diff.getChangedEndpoints(); + for(int i=0;i age = changedPostOp.getAddRequestProps().stream() + .filter(p -> "age".equals(p.getEl())).findFirst(); + + Assert.assertTrue("age property should be added", age.isPresent()); + Assert.assertTrue("age property should be added", age.get().getProperty().getDescription().equals("age of item")); + Assert.assertTrue("age property should be added", age.get().getProperty().getType().equals("integer")); + + // an old property oldName is removed + Assert.assertEquals(1, changedPostOp.getMissingRequestProps().size()); + Optional oldName = changedPostOp.getMissingRequestProps().stream() + .filter(p -> "oldName".equals(p.getEl())).findFirst(); + Assert.assertTrue("oldName property should be removed", oldName.isPresent()); + Assert.assertTrue("oldName property should be removed as deprecated", oldName.get().getProperty().getDeprecated()); + Assert.assertTrue("oldName property should be removed as type integer", oldName.get().getProperty().getType().equals("string")); + + + // GET: + // a new query parameter "type" is added + Operation getOp = + ce.getNewOperations().get(HttpMethod.GET); + + Assert.assertEquals(2, getOp.getParameters().size()); + + //check type parameter + Optional parameter = getOp.getParameters().stream() + .filter(p -> "type".equals(p.getName())).findFirst(); + + Assert.assertTrue("type query parameter should be added", parameter.isPresent()); + Assert.assertTrue("type query parameter should be added", parameter.get().getName().equals("type")); + Assert.assertTrue("type query parameter should be added", parameter.get().getIn().equals("query")); + Assert.assertTrue("type query parameter should be added", parameter.get().getSchema().getType().equals("string")); + Assert.assertTrue("type query parameter should be added", parameter.get().getSchema().getDescription().equals("the type of the item")); + + //check size parameter + Optional parameter2 = getOp.getParameters().stream() + .filter(p -> "size".equals(p.getName())).findFirst(); + + Assert.assertTrue("size query parameter should be added", parameter2.isPresent()); + Assert.assertTrue("size query parameter should be added", parameter2.get().getName().equals("size")); + Assert.assertTrue("size query parameter should be added", parameter2.get().getIn().equals("query")); + Assert.assertTrue("size query parameter should be added", parameter2.get().getSchema().getType().equals("integer")); + Assert.assertTrue("size query parameter should be added", parameter2.get().getDescription().equals("the size of the item")); + Assert.assertTrue("size query parameter should be added", parameter2.get().getRequired()); + + + + } + + @Test + public void testResponseStatusDiff() { + SwaggerDiff diff = SwaggerDiff.compareV3( + "src/test/resources/petstore_v3_diff1.json", + "src/test/resources/petstore_v3_diff2.json" + ); + + List changed = diff.getChangedEndpoints(); + Assert.assertFalse("Atteso almeno un endpoint cambiato", changed.isEmpty()); + + ChangedOperation op = changed.get(0).getChangedOperations() + .values().iterator().next(); + + // 201 aggiunto + Assert.assertTrue("Atteso status 201 aggiunto", + op.getAddResponses().containsKey("201")); + // 404 rimosso + Assert.assertTrue("Atteso status 404 rimosso", + op.getMissingResponses().containsKey("404")); + // 200 con schema cambiato (se hai modificato lo schema del 200) + boolean has200Changed = op.getChangedResponses().stream() + .anyMatch(cr -> "200".equals(cr.getStatusCode()) && cr.isDiff()); + Assert.assertTrue("Atteso schema del 200 cambiato", has200Changed); + } + + @Test + public void testResponseFullDiff() { + SwaggerDiff diff = SwaggerDiff.compareV3( + "src/test/resources/petstore_v3_diff1.json", + "src/test/resources/petstore_v3_diff2.json" + ); + + ChangedOperation op = diff.getChangedEndpoints().get(0) + .getChangedOperations().values().iterator().next(); + + ChangedResponse r200 = op.getChangedResponses().stream() + .filter(cr -> "200".equals(cr.getStatusCode())) + .findFirst().orElse(null); + + Assert.assertNotNull(r200); + + // description + Assert.assertTrue(r200.isDescriptionChanged()); + Assert.assertEquals("ok", r200.getOldDescription()); + Assert.assertEquals("update Success", r200.getNewDescription()); + } + + } diff --git a/src/test/resources/petstore_v3_diff1.json b/src/test/resources/petstore_v3_diff1.json new file mode 100644 index 00000000..cfaf2dc2 --- /dev/null +++ b/src/test/resources/petstore_v3_diff1.json @@ -0,0 +1,38 @@ +{ + "openapi": "3.0.1", + "info": { "title": "API", "version": "1.0" }, + "paths": { + "/item": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["name", "oldName", "size"], + "properties": { + "name": { "type": "string" }, + "oldName": { "type": "string", "deprecated": true }, + "size": { "type": "integer", "deprecated": true } + } + } + } + } + }, + "responses": { + "200": { + "description": "ok", + "headers": { + "X-Rate-Limit": { "description": "calls per hour", "schema": { "type": "integer" } }, + "X-Deprecated-Header": { "schema": { "type": "string" } } + }, + "content": { + "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "integer" } } } } + } + }, + "404": { "description": "bad request" } + } + } + } + } +} diff --git a/src/test/resources/petstore_v3_diff2.json b/src/test/resources/petstore_v3_diff2.json new file mode 100644 index 00000000..285e284c --- /dev/null +++ b/src/test/resources/petstore_v3_diff2.json @@ -0,0 +1,81 @@ +{ + "openapi": "3.0.1", + "info": { "title": "API", "version": "1.1" }, + "paths": { + "/item": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["name", "age", "size"], + "properties": { + "name": { "type": "string", "description": "name of item" }, + "age": { "type": "integer", "description": "age of item" }, + "size": { "type": "integer", "deprecated": true } + } + } + } + } + }, + "responses": { + "200": { + "description": "update Success", + "headers": { + "X-Rate-Limit": { "description": "max calls per hour", "schema": { "type": "string" } }, + "X-New-Header": { "schema": { "type": "string" } } + }, + "content": { + "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" } } } }, + "application/xml": { "schema": { "type": "object" } } + } +}, + "201": { "description": "created" }, + "400": { "description": "bad request", "content": { "application/json": { "schema": { "type": "string", "deprecated": true } } } } + } + }, + "get": { + "parameters": [ + { "name": "type", "in": "query", "schema": { "type": "string", "description": "the type of the item" } }, + { "name": "size", "in": "query", "required": true, "description": "the size of the item", "schema": { "type": "integer"} } + ], + "responses": { + "200": { "description": "ok" } + } + } + }, + "/admin/item": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "name of item" }, + "age": { "type": "integer", "description": "age of item" }, + "size": { "type": "integer", "deprecated": true } + } + } + } + } + }, + "responses": { + "200": { "description": "ok" }, + "201": { "description": "created" }, + "400": { "description": "bad request", "content": { "application/json": { "schema": { "type": "string", "deprecated": true } } } } + } + }, + "get": { + "parameters": [ + { "name": "type", "in": "query", "schema": { "type": "string", "description": "the type of the item" } }, + { "name": "size", "in": "query", "required": true, "description": "the size of the item", "schema": { "type": "integer"} } + ], + "responses": { + "200": { "description": "ok" } + } + } + } + } +} diff --git a/src/test/resources/petstore_v3_empty.json b/src/test/resources/petstore_v3_empty.json new file mode 100644 index 00000000..edd1b837 --- /dev/null +++ b/src/test/resources/petstore_v3_empty.json @@ -0,0 +1,53 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Swagger Petstore", + "description": "A sample API", + "version": "1.0.0" + }, + "servers": [ + { + "url": "http://petstore.swagger.io/v1" + } + ], + "paths": { + "/pets": { + "get": { + "summary": "List all pets", + "operationId": "listPets", + "responses": { + "200": { + "description": "A paged array of pets", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": ["id", "name"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + } + } + } + } +}