svenbayer / spring-cloud-contract-swagger Goto Github PK
View Code? Open in Web Editor NEWConverts Swagger files to contracts for Spring Cloud Contract
License: Apache License 2.0
Converts Swagger files to contracts for Spring Cloud Contract
License: Apache License 2.0
We should validate example values so it is not possible to specify a floating-point value for an integer.
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
As we want to create requests that are resemble real ones, we need be able to leave out optional parameters in requests.
Acceptance criteria
Implementation hints
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.
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.
Our current test coverage is 69%. We need to increase it!
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.
The Swagger converter should handle YAML and JSON format. However, we need to confirm this.
Acceptance Criteria
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?
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())));
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.
$ 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
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.
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
Add Circle CI for testing.
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.
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
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.
We need to add spring-cloud-contract 2.1.0.RELEASE as soon as it is available so we can use standard yml-file extensions for swagger files. Currently, we have to rename the extension of swagger files so they get picked up by the SwaggerContractConverter.
https://github.com/spring-cloud/spring-cloud-contract/milestone/38
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.