GithubHelp home page GithubHelp logo

svenbayer / spring-cloud-contract-swagger Goto Github PK

View Code? Open in Web Editor NEW
25.0 25.0 10.0 192 KB

Converts Swagger files to contracts for Spring Cloud Contract

License: Apache License 2.0

Java 72.07% Groovy 27.93%
spring-boot spring-cloud-contract

spring-cloud-contract-swagger's People

Contributors

marcingrzejszczak avatar stepan-aleksandrov avatar svenbayer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

spring-cloud-contract-swagger's Issues

Path Variable x-example values are ignored

There is no substitution of path variables (path parameters) with values specified in x-example. So the urlPath in the generated mappings JSON always contains the path variable name instead of the x-example value or the default value for integers.


Example 1 (string path variable):

swagger:

paths:
/color/{name}:
get:
summary: "Get color by name"
parameters:
- name: name
in: path
required: true
type: string
x-example: "blue"

generated JSON:

...
"request" : {
"urlPath" : "/color/name",
"method" : "GET"
},
...

Comment: For this case, I expected the urlPath to include the x-example value of "blue". So the urlPath would be /color/blue.


Example 2 (integer path variable):

swagger:

paths:
/color/{id}:
get:
summary: "Get color by id"
parameters:
- name: id
in: path
required: true
type: integer
x-example: 5

generated JSON:

...
"request" : {
"urlPath" : "/color/id",
"method" : "GET"
},
...

Comment: For this case, I expected the urlPath to include the x-example value of 5. So the urlPath would be /color/5.


Example 3 (integer path variable, no x-example):

swagger:

paths:
/color/{id}:
get:
summary: "Get color by id"
parameters:
- name: id
in: path
required: true
type: integer

generated JSON:

...
"request" : {
"urlPath" : "/color/id",
"method" : "GET"
},
...

Comment: For this case, I expected the urlPath to include the default integer value of 1. So the urlPath would be /color/1.


My guess is the fix would go in this section of SwaggerContractConverter.java. I see a //TODO in there, so this might be a known issue:

if (operation.getParameters() != null) {
operation.getParameters().stream()
.filter(param -> param instanceof PathParameter)
.map(AbstractSerializableParameter.class::cast)
//TODO This has to become more advanced! We need to check types so we can use 1 for int32 etc.
.forEach(param -> request.urlPath(request.getUrlPath().getClientValue().toString().replace("{" + param.getName() + "}", param.getName())));

Swagger name produces bizzare WireMock mappings

$ git clone https://github.com/SvenBayer/spring-cloud-contract-swagger-sample.git
$ cd spring-cloud-contract-swagger-sample/swagger-coffee-producer-simple
$ mvn clean install
$ ls -al target/stubs/META-INF/blog.svenbayer/swagger-coffee-producer-simple/1.0-SNAPSHOT/mappings

produces

โžœ  swagger-coffee-producer-simple git:(master) ll target/stubs/META-INF/blog.svenbayer/swagger-coffee-producer-simple/1.0-SNAPSHOT/mappings
total 8
-rw-r--r--  1 marcingrzejszczak2  staff   1.4K Jul 18 09:20 Sends a coffee rocket to a bean planet and returns the bean planet..json

this looks bad

Sends a coffee rocket to a bean planet and returns the bean planet..json

I guess the swagger name should be escaped for such chars

StackOverflow when response has a circular dependency

When a response has a circular dependency in its definition, converter fails with a StackOverflowError. Stacktrace looks like this:

java.lang.StackOverflowError
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer$TableInfo.createInitial(CharsToNameCanonicalizer.java:809)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.<init>(CharsToNameCanonicalizer.java:243)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.createRoot(CharsToNameCanonicalizer.java:300)
	at com.fasterxml.jackson.core.sym.CharsToNameCanonicalizer.createRoot(CharsToNameCanonicalizer.java:296)
	at com.fasterxml.jackson.core.JsonFactory.<init>(JsonFactory.java:191)
	at com.fasterxml.jackson.databind.MappingJsonFactory.<init>(MappingJsonFactory.java:29)
	at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:549)
	at com.fasterxml.jackson.databind.ObjectMapper.<init>(ObjectMapper.java:480)
// This is the loop start
at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.SwaggerDefinitionsRefResolverSwagger.<init>(SwaggerDefinitionsRefResolverSwagger.java:25)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.ReferenceResolverFactory.getReferenceResolver(ReferenceResolverFactory.java:32)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.ResponseHeaderValueBuilder.createResponseHeaderValue(ResponseHeaderValueBuilder.java:66)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.ResponseHeaderValueBuilder.createResponseHeaderValue(ResponseHeaderValueBuilder.java:74)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.reference.SwaggerDefinitionsRefResolverSwagger.lambda$resolveObjectDefinitionsRef$0(SwaggerDefinitionsRefResolverSwagger.java:110)
	at java.base/java.util.stream.Collectors.lambda$uniqKeysMapAccumulator$1(Collectors.java:178)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:578)
// This is the loop end

Here is the test and a json to reproduce this issue:

circular_dependency_swagger.yml

swagger: '2.0'
info:
  title: COFFEE-ROCKET-SERVICE
  description: A service that provides coffee bean rockets, bean planets, and other things the coffeeverse has to offer.
  version: '1.0'
  termsOfService: 'urn:tos'
  contact: {}
  license:
    name: Apache 2.0
    url: 'http://www.apache.org/licenses/LICENSE-2.0'
host: svenbayer.blog
schemes:
  - https
  - http
basePath: /coffee-rocket-service/v1.0
paths:
  /takeoff:
    post:
      x-ignore: false
      summary: Sends a coffee rocket to a bean planet and returns the bean planet.
      description: API endpoint to send a coffee rocket to a bean planet and returns the bean planet.
      consumes:
        - application/json
      produces:
        - '*/*'
      responses:
        '201':
          description: Created
          schema:
            $ref: '#/definitions/BeanPlanet'
definitions:
  BeanPlanet:
    type: object
    properties:
      neighbor_planets:
        type: array
        items:
          $ref: '#/definitions/BeanPlanet'
    title: BeanPlanet

CircularDependencySwaggerContractSpec

class CircularDependencySwaggerContractSpec extends Specification {

    @Subject
    SwaggerContractConverter converter = new SwaggerContractConverter()
    TestContractEquals testContractEquals = new TestContractEquals()

    def "should convert swagger with circular dependency to contract"() {
        given:
        File singleSwaggerYaml = new File(SwaggerContractConverterSpec.getResource("/swagger/circular_dependency/circular_dependency_swagger.yml").toURI())
        Contract expectedContract = Contract.make {
            label("takeoff_coffee_bean_rocket")
            name("1_takeoff_POST")
            description("API endpoint to send a coffee rocket to a bean planet and returns the bean planet.")
            priority(1)
            request {
                method(POST())
                urlPath("/coffee-rocket-service/v1.0/takeoff")
            }
            response {
                status(201)
                body("""{
  "neighbor_planets": [
    null
  ]
}""")
            }
        }
        when:
        Collection<Contract> contracts = converter.convertFrom(singleSwaggerYaml)
        then:
        testContractEquals.assertContractEquals(Collections.singleton(expectedContract), contracts)
    }
}

Swagger UI solves this issue by setting the repeated item to null. Maybe it would be useful to let just one level to be generate, to show the contract more clearly, but any solution would work.

[Question] Which status code is picked for response

Swagger is a schema definition. In your samples I don't see any examples section or sth like this, just definition of what's on the input, what's on the output. How do you pick for which request inputs a given response body and status code is applicable?

Converter fails when response is an array of known response types

When response is an array of an existing response type, the converter is not able to produce a value for the response body and crashes with this stacktrace

blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.exception.SwaggerContractConverterException: Could not parse body for response

	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.builder.ResponseBodyBuilder.createValueForResponseBody(ResponseBodyBuilder.java:46)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.SwaggerContractConverter.createResponse(SwaggerContractConverter.java:173)
	at blog.svenbayer.springframework.cloud.contract.verifier.spec.swagger.SwaggerContractConverter.createContract(SwaggerContractConverter.java:99)
...

Here is the test and a json to reproduce this issue

array/array_swagger.yml

swagger: '2.0'
info:
  title: COFFEE-ROCKET-SERVICE
  description: A service that provides coffee bean rockets, bean planets, and other things the coffeeverse has to offer.
  version: '1.0'
  termsOfService: 'urn:tos'
  contact: {}
  license:
    name: Apache 2.0
    url: 'http://www.apache.org/licenses/LICENSE-2.0'
host: svenbayer.blog
schemes:
  - https
  - http
basePath: /coffee-rocket-service/v1.0
paths:
  /takeoff:
    post:
      x-ignore: false
      summary: Sends a coffee rocket to a bean planet and returns the bean planet.
      description: API endpoint to send a coffee rocket to a bean planet and returns the bean planet.
      consumes:
        - application/json
      produces:
        - '*/*'
      responses:
        '201':
          description: Created
          schema:
            type: array
            items:
              $ref: '#/definitions/BeanPlanet'
definitions:
  BeanPlanet:
    type: object
    properties:
      name:
        type: string
      size:
        type: integer
      asteroids:
        type: array
        items:
          $ref: '#/definitions/BeanAsteroid'
    title: BeanPlanet
  BeanAsteroid:
    type: object
    properties:
      name:
        type: string
      speed:
        type: integer
      shape:
        type: string
        enum:
          - 'ROUND'
          - 'SQUARE'
          - 'BEAN'
    title: BeanAsteroids

ArraySwaggerContractSpec:

class ArraySwaggerContractSpec extends Specification {

    @Subject
    SwaggerContractConverter converter = new SwaggerContractConverter()
    TestContractEquals testContractEquals = new TestContractEquals()

    def "should convert array response swagger to contract"() {
        given:
        File singleSwaggerYaml = new File(SwaggerContractConverterSpec.getResource("/swagger/array/array_swagger.yml").toURI())
        Contract expectedContract = Contract.make {
            label("takeoff_coffee_bean_rocket")
            name("1_takeoff_POST")
            description("API endpoint to send a coffee rocket to a bean planet and returns the bean planet.")
            priority(1)
            request {
                method(POST())
                urlPath("/coffee-rocket-service/v1.0/takeoff")
            }
            response {
                status(201)
                body("""[
  {
  "size" : 1,
  "asteroids" : [ {
    "shape" : "ROUND",
    "name" : "name",
    "speed" : 1
  } ],
  "name" : "name"
  }
]""")
            }
        }
        when:
        Collection<Contract> contracts = converter.convertFrom(singleSwaggerYaml)
        then:
        testContractEquals.assertContractEquals(Collections.singleton(expectedContract), contracts)
    }
}

As I see this, the easiest and hackiest way to fix this would be to get the array type, create and use a resolver for it and then append array brackets to the start and end of the string.

By adding this clause to ResponseBodyBuilder

...
else if(response.getResponseSchema() instanceof ArrayModel && ((ArrayModel)response.getResponseSchema()).getItems() instanceof RefProperty) {
	String simpleRefName = ((RefProperty) ((ArrayModel) response.getResponseSchema()).getItems()).getSimpleRef();
	SwaggerReferenceResolver resolver = this.refFactory.getReferenceResolver(simpleRefName, response.getVendorExtensions());
	return "[" + resolver.resolveReference(definitions) + "]";
}
...

An even hackier alternative would a creation of a new array type, saving it to definitions and then pass it to the resolver, but this sounds very bad.

If this is to be fixed, it should be trivial to check if the same issues are present for request body builder and fix it.

Test Swagger in JSON format

The Swagger converter should handle YAML and JSON format. However, we need to confirm this.

Acceptance Criteria

  • A test is added that converts a JSON Swagger to a Spring Cloud contract

It should be possible to set real data for bodies

A request- and response-body should be able to have example data in the x-example field as json string so we can create requests and responses with real data.

Example of a response with real data:
responses:
'201':
description: Created
schema:
x-example: "{ id: 1, name: 'Mark Muller' }"
$ref: '#/definitions/BeanPlanet'

Acceptance criteria

  • It is verified or implemented that a x-example field can be set to a json string
  • The spring-cloud-contract-swagger-sample project is extended
  • Tests are added

External files for body specification

It should be possible to specify request and response bodies in an external json file and reference to them, so one can create complex examples of bodies outside the Swagger specification. The external json files should be validated against the Swagger definitions.

Add CI

Add Circle CI for testing.

Validate example values

We should validate example values so it is not possible to specify a floating-point value for an integer.

Add default behaviour of Swagger converter to Readme

We need to add the default behaviour of the Swagger converter to the Readme so it is clear how the converter behaves. Default values are assigned to fields. Also it picks the most top response from a list of responses. This needs to be documented to.

Add min & max values to query parameters

When setting min or max values for a query parameters, they are being ignored and the default value is set. This does not break the converter but it does not confirm to the assumed behaviour.

Acceptance Criteria

  • Min and max values for query parameters are being considered

Unify value evaluation

A Swagger specification can contain fields with pre-defined values that are set via x-example, example, or default, or in any of their children. Since the request and response parts contain different types of parameter objects, we need to find a way to move this logic in one place.

It should be possible to set leave out optional parameters for a request

As we want to create requests that are resemble real ones, we need be able to leave out optional parameters in requests.

Acceptance criteria

  • It is possible to have no values for optional attributes
  • spring-cloud-contract-swagger is extended
  • Description of usage is added to readme
  • Tests are added

Implementation hints

  • Use no value or null for x-example properties

More detailed testing 1st step

Currently, the Groovy tests with the Contract object perform very weak testing. By using the Sample Project it is possible to do some integration testing. Enable this in the CI to ensure the project works also when it is actually being used.

Number is floating-point

The type number is a floating-point value. It could be a float or a double. We need to fix this in the converter and in the samples.

More detailed testing 2nd step

Currently, the Groovy tests with the Contract object perform very weak testing. They only perform a normalised toString comparison. This is due to the Pattern objects that return different values for equals, even if they are the same. Also the Header comparison does not work at all.

Acceptance Criteria

  • Enable detailed testing of contracts in tests

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.