Giter Club home page Giter Club logo

spring-cloud-contract-swagger's Introduction

Maven Central Javadocs Project Stats

[Unit Tests CircleCI [Integration Tests CircleCI

codecov [SonarQube]

Spring Cloud Contract Swagger

Converts Swagger files to contracts for Spring Cloud Contract

This project enables Spring Cloud Contract to parse Swagger API 2.0 specifications as Spring Cloud Contracts.

Usage

You can check out spring-cloud-contract-swagger-sample for examples.

Producer

To convert a Swagger file to a Spring Cloud Contract and to execute it against a Producer, add the spring-cloud-contract-maven-plugin as plugin and the converter spring-cloud-contract-swagger as plugin-dependency.

Consumer

For the consumer, add as dependencies the spring-cloud-starter-contract-stub-runner and the converter spring-cloud-contract-swagger.

Default Behaviour

Currently, Spring Cloud Contract Swagger generates default values for a Swagger document’s fields.

  • Boolean: true
  • Number: 1
  • Float/Double: 1.1
  • String: (the name of the String)

Custom Values

To set your own default values, you can use the x-example field in the parameters and responses section. In the definitions section, you can also use the x-example or the supported example field. You should avoid the default field, since the current Swagger parser (1.0.36) interprets numerical values of default fields as String. Regarding the order, the converter will first evaluate example, then x-example, and then default fields. If it does not find a predefined value, it will go all the way down to the primitive fields. The converter will only use the first response of a Swagger method entry.

Optional Parameters

If you do not want to pass an optional parameter (required: false) in a request, you can add x-ignore: true to this field.

Patterns

Pattern only work for request parameters. Your provided example values will be checked against the pattern.

Custom JSON Body

You can set the request and response body with a json string. For this, you have to place an x-example field next (on the same level) to the schema field. Use single ticks for your json string so you do not have to escape any quotes.

Also, you can reference external json files for the request and response by using the x-ref field. Just place the x-ref field next (on the same level) to the schema field. Use single ticks for the path, relative to the Swagger file.

Recommendation

If you are working in a project that restricts you to define your API with Swagger, this library is for you. Instead of defining your API and Groovy/JSON/YAML contracts, maintaining two separate documents, and risking inconsistency, you can maintain your API specification and contract in one document. This enables you to take at least partly benefit of CDC. The features of this are more restricted then by using proper contracts. However, if possible, you should go the way to define your API with contracts and let the Swagger documentation be generated by your Maven project. See more details at https://github.com/spring-cloud/spring-cloud-contract

If you decide to do contract testing with this project, you might have the choice to move gradually to Spring Cloud Contract with the Swagger Request Validator since Spring Cloud Contract generates WireMock stubs. This would enable you to add Groovy/JSON/YAML contracts and therefore test move scenarios. Thanks to Marcin Grzejszczak for this tip.

Further Information

Read the blog about Consumer Driven Contracts with Swagger for more information.

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

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

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.

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.

Validate example values

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

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

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.

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 CI

Add Circle CI for testing.

[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?

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.

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 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

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.

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

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.

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

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())));

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.