Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add description in the externalised example using discriminator metadata #1379

Merged
merged 2 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 46 additions & 18 deletions core/src/main/kotlin/io/specmatic/core/Feature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import io.cucumber.messages.IdGenerator
import io.cucumber.messages.IdGenerator.Incrementing
import io.cucumber.messages.types.*
import io.cucumber.messages.types.Examples
import io.specmatic.core.discriminator.DiscriminatorBasedItem
import io.specmatic.core.discriminator.DiscriminatorMetadata
import io.specmatic.core.utilities.*
import io.swagger.v3.oas.models.*
import io.swagger.v3.oas.models.headers.Header
Expand Down Expand Up @@ -147,38 +149,44 @@ data class Feature(
}
}

fun generateRequestResponses(scenario: Scenario): List<GeneratedRequestResponse> {
fun generateDiscriminatorBasedRequestResponseList(scenario: Scenario): List<DiscriminatorBasedRequestResponse> {
try {
val requests = scenario.generateHttpRequestV2()
val responses = scenario.generateHttpResponseV2(serverState)

val generatedRequestResponses = if(requests.size > responses.size) {
requests.map { (discriminator, request) ->
val response = if(responses.containsKey(discriminator)) responses.getValue(discriminator)
else responses.values.first()
GeneratedRequestResponse(request, response, discriminator)
val discriminatorBasedRequestResponseList = if (requests.size > responses.size) {
requests.map { (requestDiscriminator, request) ->
val (responseDiscriminator, response) = if (responses.containsDiscriminatorValueAs(requestDiscriminator.discriminatorValue))
responses.getDiscriminatorItemWith(requestDiscriminator.discriminatorValue)
else
responses.first()
DiscriminatorBasedRequestResponse(
request,
response,
requestDiscriminator,
responseDiscriminator
)
}
} else {
responses.map { (discriminator, response) ->
val request = if(requests.containsKey(discriminator)) requests.getValue(discriminator)
else requests.values.first()
GeneratedRequestResponse(request, response, discriminator)
responses.map { (responseDiscriminator, response) ->
val (requestDiscriminator, request) = if (requests.containsDiscriminatorValueAs(responseDiscriminator.discriminatorValue))
requests.getDiscriminatorItemWith(responseDiscriminator.discriminatorValue)
else requests.first()
DiscriminatorBasedRequestResponse(
request,
response,
responseDiscriminator,
requestDiscriminator
)
}
}

return generatedRequestResponses
return discriminatorBasedRequestResponseList
} finally {
serverState = emptyMap()
}
}

// Better name
data class GeneratedRequestResponse(
val request: HttpRequest,
val response: HttpResponse,
val requestKind: String
)

fun stubResponse(
httpRequest: HttpRequest,
mismatchMessages: MismatchMessages = DefaultMismatchMessages
Expand Down Expand Up @@ -1644,6 +1652,19 @@ data class Feature(
throw ContractException(errors.joinToString("${System.lineSeparator()}${System.lineSeparator()}"))
}

private fun<T> List<DiscriminatorBasedItem<T>>.containsDiscriminatorValueAs(
discriminatorValue: String
): Boolean {
return this.any { it.discriminatorValue == discriminatorValue }
}

private fun <T> List<DiscriminatorBasedItem<T>>.getDiscriminatorItemWith(
discriminatorValue: String
): DiscriminatorBasedItem<T> {
return this.first { it.discriminatorValue == discriminatorValue }
}


companion object {

private fun getTestsDirectory(contractFile: File): File? {
Expand Down Expand Up @@ -2223,3 +2244,10 @@ fun similarURLPath(baseScenario: Scenario, newScenario: Scenario): Boolean {
fun isInteger(
base: URLPathSegmentPattern
) = base.pattern is ExactValuePattern && base.pattern.pattern.toStringLiteral().toIntOrNull() != null

data class DiscriminatorBasedRequestResponse(
val request: HttpRequest,
val response: HttpResponse,
val requestDiscriminator: DiscriminatorMetadata,
val responseDiscriminator: DiscriminatorMetadata
)
31 changes: 17 additions & 14 deletions core/src/main/kotlin/io/specmatic/core/HttpRequestPattern.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import io.specmatic.core.Result.Success
import io.specmatic.core.pattern.*
import io.specmatic.core.value.StringValue
import io.ktor.util.*
import io.specmatic.core.discriminator.DiscriminatorBasedItem
import io.specmatic.core.discriminator.DiscriminatorBasedValueGenerator
import io.specmatic.core.discriminator.DiscriminatorMetadata
import io.specmatic.core.utilities.Flags
import io.specmatic.core.utilities.Flags.Companion.EXTENSIBLE_QUERY_PARAMS
import io.specmatic.core.value.JSONObjectValue
Expand Down Expand Up @@ -454,22 +457,22 @@ data class HttpRequestPattern(
}
}

fun generateV2(resolver: Resolver): Map<String, HttpRequest> {
fun generateV2(resolver: Resolver): List<DiscriminatorBasedItem<HttpRequest>> {
return attempt(breadCrumb = "REQUEST") {
if (method == null) {
throw missingParam("HTTP method")
val baseRequest = generate(resolver)

DiscriminatorBasedValueGenerator.generateDiscriminatorBasedValues(
resolver,
body
).map {
DiscriminatorBasedItem(
discriminator = DiscriminatorMetadata(
discriminatorProperty = it.discriminatorProperty,
discriminatorValue = it.discriminatorValue,
),
value = baseRequest.updateBody(it.value)
)
}
val baseRequest = HttpRequest()
.updateMethod(method)
.generateAndUpdateURL(resolver)
.generateAndUpdateHeaders(resolver)
.generateAndUpdateFormFieldsValues(resolver)
.generateAndUpdateSecuritySchemes(resolver)
.generateAndUpdateMultiPartData(resolver)

generateDiscriminatorBasedValues(resolver, body).map { (discriminatorKey, generatedBody) ->
discriminatorKey to baseRequest.updateBody(generatedBody)
}.toMap()
}
}

Expand Down
64 changes: 28 additions & 36 deletions core/src/main/kotlin/io/specmatic/core/HttpResponsePattern.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package io.specmatic.core

import io.specmatic.core.discriminator.DiscriminatorBasedItem
import io.specmatic.core.discriminator.DiscriminatorBasedValueGenerator
import io.specmatic.core.discriminator.DiscriminatorMetadata
import io.specmatic.core.pattern.*
import io.specmatic.core.value.JSONArrayValue
import io.specmatic.core.value.ListValue
import io.specmatic.core.value.StringValue
import io.specmatic.core.value.Value
import io.specmatic.stub.softCastValueToXML
Expand All @@ -19,28 +20,26 @@ data class HttpResponsePattern(
constructor(response: HttpResponse) : this(HttpHeadersPattern(response.headers.mapValues { stringToPattern(it.value, it.key) }), response.status, response.body.exactMatchElseType())

fun generateResponse(resolver: Resolver): HttpResponse {
return attempt(breadCrumb = "RESPONSE") {
val value = softCastValueToXML(resolver.withCyclePrevention(body, body::generate))
val headers = headersPattern.generate(resolver).plus(SPECMATIC_RESULT_HEADER to "success").let { headers ->
if ((headers.containsKey("Content-Type").not() && value.httpContentType.isBlank().not()))
headers.plus("Content-Type" to value.httpContentType)
else headers
}
HttpResponse(status, headers, value)
}
return generateResponseWith(
value = resolver.withCyclePrevention(body, body::generate),
resolver = resolver
)
}

fun generateResponseV2(resolver: Resolver): Map<String, HttpResponse> {
fun generateResponseV2(resolver: Resolver): List<DiscriminatorBasedItem<HttpResponse>> {
return attempt(breadCrumb = "RESPONSE") {
generateDiscriminatorBasedValues(resolver, body).map { (discriminatorKey, value) ->
val generatedBody = softCastValueToXML(value)
val headers = headersPattern.generate(resolver).plus(SPECMATIC_RESULT_HEADER to "success").let { headers ->
if ((headers.containsKey("Content-Type").not() && generatedBody.httpContentType.isBlank().not()))
headers.plus("Content-Type" to generatedBody.httpContentType)
else headers
}
discriminatorKey to HttpResponse(status, headers, generatedBody)
}.toMap()
DiscriminatorBasedValueGenerator.generateDiscriminatorBasedValues(
resolver,
body
).map {
DiscriminatorBasedItem(
discriminator = DiscriminatorMetadata(
discriminatorProperty = it.discriminatorProperty,
discriminatorValue = it.discriminatorValue,
),
value = generateResponseWith(it.value, resolver)
)
}
}
}

Expand Down Expand Up @@ -195,24 +194,17 @@ data class HttpResponsePattern(
)
}.breadCrumb("RESPONSE").value
}
}

fun generateDiscriminatorBasedValues(resolver: Resolver, pattern: Pattern): Map<String, Value> {
return resolver.withCyclePrevention(pattern) { updatedResolver ->
val resolvedPattern = resolvedHop(pattern, updatedResolver)

if(resolvedPattern is ListPattern) {
val listValuePattern = resolvedHop(resolvedPattern.pattern, updatedResolver)
if(listValuePattern is AnyPattern && listValuePattern.isDiscriminatorPresent()) {
val values = listValuePattern.generateForEveryDiscriminatorValue(updatedResolver)
return@withCyclePrevention values.mapValues { JSONArrayValue(listOf(it.value)) }
private fun generateResponseWith(value: Value, resolver: Resolver): HttpResponse {
return attempt(breadCrumb = "RESPONSE") {
val generatedBody = softCastValueToXML(value)
val headers = headersPattern.generate(resolver).plus(SPECMATIC_RESULT_HEADER to "success").let { headers ->
if ((headers.containsKey("Content-Type").not() && generatedBody.httpContentType.isBlank().not()))
headers.plus("Content-Type" to generatedBody.httpContentType)
else headers
}
HttpResponse(status, headers, generatedBody)
}

if(resolvedPattern !is AnyPattern || resolvedPattern.isDiscriminatorPresent().not()) {
return@withCyclePrevention mapOf("" to resolvedPattern.generate(updatedResolver))
}
resolvedPattern.generateForEveryDiscriminatorValue(updatedResolver)
}
}

Expand Down
8 changes: 6 additions & 2 deletions core/src/main/kotlin/io/specmatic/core/Scenario.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.specmatic.core

import io.specmatic.conversions.OpenApiSpecification
import io.specmatic.core.discriminator.DiscriminatorBasedItem
import io.specmatic.core.filters.ScenarioMetadata
import io.specmatic.core.log.logger
import io.specmatic.core.pattern.*
Expand Down Expand Up @@ -168,7 +169,10 @@ data class Scenario(
httpResponsePattern.generateResponse(resolver.copy(factStore = CheckFacts(facts), context = requestContext))
}

fun generateHttpResponseV2(actualFacts: Map<String, Value>, requestContext: Context = NoContext): Map<String, HttpResponse> =
fun generateHttpResponseV2(
actualFacts: Map<String, Value>,
requestContext: Context = NoContext
): List<DiscriminatorBasedItem<HttpResponse>> =
scenarioBreadCrumb(this) {
val facts = combineFacts(expectedFacts, actualFacts, resolver)

Expand Down Expand Up @@ -230,7 +234,7 @@ data class Scenario(
httpRequestPattern.generate(flagsBased.update(resolver.copy(factStore = CheckFacts(expectedFacts))))
}

fun generateHttpRequestV2(flagsBased: FlagsBased = DefaultStrategies): Map<String, HttpRequest> =
fun generateHttpRequestV2(flagsBased: FlagsBased = DefaultStrategies): List<DiscriminatorBasedItem<HttpRequest>> =
scenarioBreadCrumb(this) {
httpRequestPattern.generateV2(flagsBased.update(resolver.copy(factStore = CheckFacts(expectedFacts))))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.specmatic.core.discriminator

data class DiscriminatorBasedItem<T>(
val discriminator: DiscriminatorMetadata,
val value: T
) {
val discriminatorProperty get() = discriminator.discriminatorProperty
val discriminatorValue get() = discriminator.discriminatorValue
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.specmatic.core.discriminator

import io.specmatic.core.Resolver
import io.specmatic.core.pattern.AnyPattern
import io.specmatic.core.pattern.ListPattern
import io.specmatic.core.pattern.Pattern
import io.specmatic.core.pattern.resolvedHop
import io.specmatic.core.value.JSONArrayValue
import io.specmatic.core.value.Value

object DiscriminatorBasedValueGenerator {

fun generateDiscriminatorBasedValues(resolver: Resolver, pattern: Pattern): List<DiscriminatorBasedItem<Value>> {
return resolver.withCyclePrevention(pattern) { updatedResolver ->
val resolvedPattern = resolvedHop(pattern, updatedResolver)

if (resolvedPattern is ListPattern) {
val listValuePattern = resolvedHop(resolvedPattern.pattern, updatedResolver)
if (listValuePattern is AnyPattern && listValuePattern.isDiscriminatorPresent()) {
val values = listValuePattern.generateForEveryDiscriminatorValue(updatedResolver)
return@withCyclePrevention values.map {
it.copy(value = JSONArrayValue(listOf(it.value)))
}
}
}

if (resolvedPattern !is AnyPattern || resolvedPattern.isDiscriminatorPresent().not()) {
return@withCyclePrevention listOf(
DiscriminatorBasedItem(
discriminator = DiscriminatorMetadata(
discriminatorProperty = "",
discriminatorValue = "",
),
value = resolvedPattern.generate(updatedResolver)
)
)
}
resolvedPattern.generateForEveryDiscriminatorValue(updatedResolver)
}
}
}
Loading