Skip to content

Commit

Permalink
Merge branch 'main' into schema_example_gen
Browse files Browse the repository at this point in the history
  • Loading branch information
joelrosario committed Nov 12, 2024
2 parents 98858c8 + a9bc5d5 commit fbc4645
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1161,12 +1161,12 @@ class OpenApiSpecification(
private fun toSpecmaticPattern(mediaType: MediaType, section: String, jsonInFormData: Boolean = false): Pattern =
toSpecmaticPattern(mediaType.schema ?: throw ContractException("${section.capitalizeFirstChar()} body definition is missing"), emptyList(), jsonInFormData = jsonInFormData)

private fun resolveDeepAllOfs(schema: Schema<Any>, discriminatorDetails: DiscriminatorDetails, typeStack: Set<String>): Pair<List<Schema<Any>>, DiscriminatorDetails> {
private fun resolveDeepAllOfs(schema: Schema<Any>, discriminatorDetails: DiscriminatorDetails, typeStack: Set<String>, topLevel: Boolean): Pair<List<Schema<Any>>, DiscriminatorDetails> {
if (schema.allOf == null)
return listOf(schema) to discriminatorDetails

// Pair<String [property name], Map<String [possible value], Pair<String [Schema name derived from the ref], Schema<Any> [reffed schema]>>>
val newDiscriminatorDetailsDetails: Triple<String, Map<String, Pair<String, List<Schema<Any>>>>, DiscriminatorDetails>? = schema.discriminator?.let { rawDiscriminator ->
val newDiscriminatorDetailsDetails: Triple<String, Map<String, Pair<String, List<Schema<Any>>>>, DiscriminatorDetails>? = if (!topLevel) null else schema.discriminator?.let { rawDiscriminator ->
rawDiscriminator.propertyName?.let { propertyName ->
val mapping = rawDiscriminator.mapping ?: emptyMap()

Expand All @@ -1178,7 +1178,8 @@ class OpenApiSpecification(
val value = mappedSchemaName to resolveDeepAllOfs(
mappedSchema,
discriminatorDetails,
typeStack + mappedComponentName
typeStack + mappedComponentName,
topLevel = false
)
discriminatorValue to value
} else {
Expand Down Expand Up @@ -1212,7 +1213,8 @@ class OpenApiSpecification(
resolveDeepAllOfs(
referredSchema,
discriminatorDetails.plus(newDiscriminatorDetailsDetails),
typeStack + componentName
typeStack + componentName,
topLevel = false
)
} else
null
Expand Down Expand Up @@ -1299,7 +1301,7 @@ class OpenApiSpecification(

is ComposedSchema -> {
if (schema.allOf != null) {
val (deepListOfAllOfs, allDiscriminators) = resolveDeepAllOfs(schema, DiscriminatorDetails(), emptySet())
val (deepListOfAllOfs, allDiscriminators) = resolveDeepAllOfs(schema, DiscriminatorDetails(), emptySet(), topLevel = true)

val explodedDiscriminators = allDiscriminators.explode()
val topLevelRequired = schema.required.orEmpty()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9352,6 +9352,40 @@ paths:
assertThat(resolvedBodyPattern.pattern.keys).doesNotContain("id?")
}

@Test
fun `should not resolve deep discriminators in an allOf schema`() {
val specFile = "src/test/resources/openapi/vehicle_deep_allof.yaml"
val feature = OpenApiSpecification.fromFile(specFile).toFeature()

assertThat(feature.scenarios).allSatisfy { scenario ->
val responseBodyPattern = resolvedHop(scenario.httpResponsePattern.body, scenario.resolver).let {
when (it) {
is ListPattern -> resolvedHop(it.pattern, scenario.resolver)
else -> it
}
}
val requestBodyPattern = scenario.httpRequestPattern.body.takeIf { it !is NoBodyPattern }?.let {
resolvedHop(scenario.httpRequestPattern.body, scenario.resolver)
}

assertThat(responseBodyPattern).isNotInstanceOf(AnyPattern::class.java)
if (requestBodyPattern != null) {
when (scenario.method) {
"POST" -> {
assertThat(requestBodyPattern).isInstanceOf(AnyPattern::class.java)
(requestBodyPattern as AnyPattern).let {
assertThat(it.pattern).hasSize(2)
assertThat(it.discriminator!!.property).isEqualTo("type")
assertThat(it.discriminator!!.values).containsExactlyInAnyOrder("car", "truck")
}
}
"PATCH" -> assertThat(requestBodyPattern).isNotInstanceOf(AnyPattern::class.java)
else -> Exception("Unexpected method: ${scenario.method}")
}
}
}
}

@Test
fun `should apply top-level required fields to properties in resolved allOf schema`() {
val specContent = """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import io.specmatic.core.value.JSONObjectValue
import io.specmatic.core.value.NumberValue
import io.specmatic.core.value.StringValue
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.io.File

class ExamplesInteractiveServerTest {
@BeforeEach
fun resetCounter() {
ExamplesInteractiveServer.resetExampleFileNameCounter()
}

@Nested
inner class ExternaliseInlineExamplesTests {

Expand Down Expand Up @@ -65,4 +72,56 @@ class ExamplesInteractiveServerTest {
}
}
}

@Nested
inner class DiscriminatorExamplesGenerationTests {
private val specFile = File("src/test/resources/openapi/vehicle_deep_allof.yaml")
private val examplesDir = specFile.parentFile.resolve("vehicle_deep_allof_examples")

@AfterEach
fun cleanUp() {
if (examplesDir.exists()) {
examplesDir.listFiles()?.forEach { it.delete() }
examplesDir.delete()
}
}

@Test
fun `should generate multiple examples for top level discriminator`() {
val generatedExamples = ExamplesInteractiveServer.generate(
contractFile = specFile,
method = "POST", path = "/vehicles", responseStatusCode = 201,
bulkMode = false, allowOnlyMandatoryKeysInJSONObject = false
)

assertThat(generatedExamples).hasSize(2)
assertThat(generatedExamples).allSatisfy {
assertThat(it.created).isTrue()
assertThat(it.path).satisfiesAnyOf(
{ path -> assertThat(path).contains("car") }, { path -> assertThat(path).contains("truck") }
)
}
}

@Test
fun `should not generate multiple examples for deep nested discriminator`() {
val generatedGetExamples = ExamplesInteractiveServer.generate(
contractFile = specFile,
method = "GET", path = "/vehicles", responseStatusCode = 200,
bulkMode = false, allowOnlyMandatoryKeysInJSONObject = false
)

assertThat(generatedGetExamples).hasSize(1)
assertThat(generatedGetExamples.first().path).contains("GET")

val generatedPatchExamples = ExamplesInteractiveServer.generate(
contractFile = specFile,
method = "PATCH", path = "/vehicles", responseStatusCode = 200,
bulkMode = false, allowOnlyMandatoryKeysInJSONObject = false
)

assertThat(generatedPatchExamples).hasSize(1)
assertThat(generatedPatchExamples.first().path).contains("PATCH")
}
}
}
82 changes: 82 additions & 0 deletions core/src/test/resources/openapi/vehicle_deep_allof.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
openapi: 3.0.3
info:
title: Vehicle Inventory API
description: An API to manage a catalog of vehicles available for sale
version: 1.0.0
paths:
/vehicles:
get:
responses:
'200':
description: A list of vehicles
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/VehicleResponse'
post:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Vehicle'
responses:
'201':
description: Vehicle created
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleResponse'
patch:
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleResponse'
responses:
'200':
description: Vehicle updated
content:
application/json:
schema:
$ref: '#/components/schemas/VehicleResponse'
components:
schemas:
VehicleResponse:
allOf:
- $ref: '#/components/schemas/Vehicle'
required:
- id
BaseVehicle:
properties:
id:
type: string
type:
type: string
required:
- type
Vehicle:
allOf:
- $ref: '#/components/schemas/BaseVehicle'
discriminator:
propertyName: type
mapping:
car: '#/components/schemas/Car'
truck: '#/components/schemas/Truck'
Car:
allOf:
- $ref: '#/components/schemas/BaseVehicle'
- type: object
properties:
seatingCapacity:
type: integer
Truck:
allOf:
- $ref: '#/components/schemas/BaseVehicle'
- type: object
properties:
trailer:
type: boolean

0 comments on commit fbc4645

Please sign in to comment.