io.specmatic.core.Result.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of specmatic-core Show documentation
Show all versions of specmatic-core Show documentation
Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends.
package io.specmatic.core
import io.specmatic.core.Result.Failure
import io.specmatic.core.pattern.*
import io.specmatic.core.utilities.capitalizeFirstChar
import io.specmatic.core.value.Value
sealed class Result {
var scenario: ScenarioDetailsForResult? = null
var contractPath: String? = null
companion object {
fun fromFailures(failures: List): Result {
return if(failures.isNotEmpty())
Failure.fromFailures(failures)
else
Success()
}
fun fromResults(results: List): Result {
val failures = results.filterIsInstance()
return fromFailures(failures)
}
}
fun reportString(): String {
return toReport().toText()
}
fun isFluffy(): Boolean {
return isFluffy(0)
}
fun isFluffy(acceptableFluffLevel: Int): Boolean {
return when(this) {
is Failure ->
failureReason?.let { it.fluffLevel > acceptableFluffLevel } == true || cause?.isFluffy(acceptableFluffLevel) == true
else -> false
}
}
abstract fun isAnyFluffy(acceptableFluffLevel: Int): Boolean
fun updateScenario(scenario: ScenarioDetailsForResult): Result {
this.scenario = scenario
return this
}
abstract fun isSuccess(): Boolean
abstract fun ifSuccess(function: () -> Result): Result
abstract fun withBindings(bindings: Map, response: HttpResponse): Result
abstract fun breadCrumb(breadCrumb: String): Result
abstract fun failureReason(failureReason: FailureReason?): Result
abstract fun shouldBeIgnored(): Boolean
fun updatePath(path: String): Result {
this.contractPath = path
return this
}
fun toReport(scenarioMessage: String? = null): Report {
return when (this) {
is Failure -> toFailureReport(scenarioMessage)
else -> SuccessReport
}
}
abstract fun partialSuccess(message: String): Result
abstract fun isPartialSuccess(): Boolean
abstract fun testResult(): TestResult
abstract fun withFailureReason(urlPathMisMatch: FailureReason): Result
abstract fun throwOnFailure(): Success
abstract fun toReturnValue(returnValue: T, errorMessage: String): ReturnValue
data class FailureCause(val message: String="", var cause: Failure? = null) {
fun hasReason(failureReason: FailureReason): Boolean {
return cause?.hasReason(failureReason) ?: false
}
fun filterByReason(failureReason: FailureReason): FailureCause? {
val cause = cause ?: return null
if(cause.failureReason == failureReason)
return this
val filteredCause = cause.filterByReason(failureReason)
if(filteredCause.isEmpty())
return null
return this.copy(cause = filteredCause)
}
fun hasAnyOfTheseReasons(failureReasons: List): Boolean {
return cause?.hasAnyOfTheseReasons(*failureReasons.toTypedArray()) ?: false
}
fun removeReasonsFromCauses(): FailureCause {
return this.copy(cause = cause?._removeReasonsFromCauses())
}
fun reasonIs(reasonFilter: (failureReason: FailureReason) -> Boolean): Boolean {
return (cause ?: return false).reasonIs(reasonFilter)
}
fun failureCount(): Int {
return cause?.let { it.failureCount() } ?: 1
}
}
data class Failure(val causes: List = emptyList(), val breadCrumb: String = "", val failureReason: FailureReason? = null) : Result() {
constructor(message: String="", cause: Failure? = null, breadCrumb: String = "", failureReason: FailureReason? = null): this(listOf(FailureCause(message, cause)), breadCrumb, failureReason)
companion object {
fun fromFailures(failures: List): Failure {
return Failure(failures.map {
it.toFailureCause()
})
}
}
val message = causes.firstOrNull()?.message ?: ""
val cause = causes.firstOrNull()?.cause
fun toFailureCause(): FailureCause {
return FailureCause(cause = this)
}
fun getFailureBreadCrumbs(prefix: String): List {
return causes.mapNotNull { it.cause?.getFailureBreadCrumbs("$prefix$breadCrumb.") }
.flatten()
.plus("$prefix$breadCrumb")
}
override fun ifSuccess(function: () -> Result) = this
override fun withBindings(bindings: Map, response: HttpResponse): Result {
return this
}
override fun shouldBeIgnored(): Boolean {
return this.scenario?.ignoreFailure == true
}
override fun partialSuccess(message: String): Result {
return this
}
override fun isPartialSuccess(): Boolean = false
override fun testResult(): TestResult {
if(shouldBeIgnored())
return TestResult.Error
return TestResult.Failed
}
override fun withFailureReason(failureReason: FailureReason): Result {
return copy(failureReason = failureReason)
}
override fun throwOnFailure(): Success {
throw ContractException(this.toFailureReport())
}
override fun toReturnValue(returnValue: T, errorMessage: String): ReturnValue {
return HasFailure(this)
}
fun reason(errorMessage: String) = Failure(errorMessage, this)
override fun breadCrumb(breadCrumb: String) = Failure(cause = this, breadCrumb = breadCrumb)
override fun failureReason(failureReason: FailureReason?): Result {
return this.copy(failureReason = failureReason)
}
fun toFailureReport(scenarioMessage: String? = null): FailureReport {
return FailureReport(contractPath, scenarioMessage, scenario, toMatchFailureDetailList())
}
fun toMatchFailureDetails(): MatchFailureDetails {
return (cause?.toMatchFailureDetails() ?: MatchFailureDetails()).let { reason ->
when {
message.isNotEmpty() -> reason.copy(errorMessages = listOf(message).plus(reason.errorMessages))
else -> reason
}
}.let { reason ->
when {
breadCrumb.isNotEmpty() -> reason.copy(breadCrumbs = listOf(breadCrumb).plus(reason.breadCrumbs))
else -> reason
}
}
}
fun toMatchFailureDetailList(): List {
return causes.flatMap {
(it.cause?.toMatchFailureDetailList() ?: listOf(MatchFailureDetails())).map { matchFailureDetails ->
val withReason = when {
message.isNotEmpty() -> matchFailureDetails.copy(errorMessages = listOf(message).plus(matchFailureDetails.errorMessages))
else -> matchFailureDetails
}
when {
breadCrumb.isNotEmpty() -> withReason.copy(breadCrumbs = listOf(breadCrumb).plus(withReason.breadCrumbs))
else -> withReason
}
}
}
}
override fun isAnyFluffy(acceptableFluffLevel: Int): Boolean {
return failureReason?.let { it.fluffLevel > acceptableFluffLevel } == true || causes.any { it.cause?.isAnyFluffy(acceptableFluffLevel) == true }
}
override fun isSuccess() = false
fun traverseFailureReason(): FailureReason? {
return failureReason ?: causes.asSequence().map {
it.cause?.traverseFailureReason()
}.firstOrNull()
}
fun hasReason(failureReason: FailureReason): Boolean {
return this.failureReason == failureReason || causes.any { it.hasReason(failureReason) }
}
fun hasAnyOfTheseReasons(vararg failureReasons: FailureReason): Boolean {
return this.failureReason != null && this.failureReason in failureReasons || causes.any { it.hasAnyOfTheseReasons(failureReasons.toList()) }
}
fun filterByReason(failureReason: FailureReason): Failure {
if(this.failureReason == FailureReason.DiscriminatorMismatch)
return this
val causesFilteredByReason: List = this.causes.map {
it.filterByReason(failureReason)
}.filterNotNull()
return this.copy(causes = causesFilteredByReason)
}
fun isEmpty(): Boolean {
return this.causes.isEmpty()
}
fun removeReasonsFromCauses(): Failure {
return this.copy(causes = causes.map { it.removeReasonsFromCauses() })
}
fun _removeReasonsFromCauses(): Failure {
return this.copy(causes = causes.map { it.removeReasonsFromCauses() }, failureReason = null)
}
fun reasonIs(reasonFilter: (failureReason: FailureReason) -> Boolean): Boolean {
return (failureReason?.let { reasonFilter(it) } ?: false) || causes.any { it.reasonIs(reasonFilter) }
}
fun failureCount(): Int {
return causes.sumOf { it.failureCount() }
}
}
data class Success(val variables: Map = emptyMap(), val partialSuccessMessage: String? = null) : Result() {
override fun isAnyFluffy(acceptableFluffLevel: Int): Boolean {
return false
}
override fun isSuccess() = true
override fun ifSuccess(function: () -> Result) = function()
override fun withBindings(bindings: Map, response: HttpResponse): Result {
return this.copy(variables = response.export(bindings))
}
override fun breadCrumb(breadCrumb: String): Result {
return this
}
override fun failureReason(failureReason: FailureReason?): Result {
return this
}
override fun shouldBeIgnored(): Boolean = false
override fun partialSuccess(message: String): Result {
return this.copy(partialSuccessMessage = message)
}
override fun isPartialSuccess(): Boolean = partialSuccessMessage != null
override fun testResult(): TestResult {
return TestResult.Success
}
override fun withFailureReason(urlPathMisMatch: FailureReason): Result {
return this
}
override fun throwOnFailure(): Success {
return this
}
override fun toReturnValue(returnValue: T, errorMessage: String): ReturnValue {
return HasValue(returnValue)
}
}
}
enum class TestResult {
Success,
Error,
Failed,
NotImplemented,
MissingInSpec,
NotCovered
}
enum class FailureReason(val fluffLevel: Int, val objectMatchOccurred: Boolean) {
PartNameMisMatch(0, false),
StatusMismatch(2, false),
IdentifierMismatch(1, false),
MethodMismatch(2, false),
ContentTypeMismatch(1, false),
RequestMismatchButStatusAlsoWrong(2, false),
URLPathMisMatch(2, false),
SOAPActionMismatch(2, false),
DiscriminatorMismatch(0, true),
FailedButDiscriminatorMatched(0, true),
FailedButObjectTypeMatched(0, true),
ScenarioMismatch(2, false)
}
data class MatchFailureDetails(val breadCrumbs: List = emptyList(), val errorMessages: List = emptyList(), val path: String? = null)
interface MismatchMessages {
fun mismatchMessage(expected: String, actual: String): String
fun unexpectedKey(keyLabel: String, keyName: String): String
fun expectedKeyWasMissing(keyLabel: String, keyName: String): String
fun valueMismatchFailure(expected: String, actual: Value?, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure {
return mismatchResult(expected, valueError(actual) ?: "null", mismatchMessages)
}
}
object DefaultMismatchMessages: MismatchMessages {
override fun mismatchMessage(expected: String, actual: String): String {
return "Expected $expected, actual was $actual"
}
override fun unexpectedKey(keyLabel: String, keyName: String): String {
return "${keyLabel.lowercase().capitalizeFirstChar()} named \"$keyName\" was unexpected"
}
override fun expectedKeyWasMissing(keyLabel: String, keyName: String): String {
return "Expected ${keyLabel.lowercase()} named \"$keyName\" was missing"
}
}
fun mismatchResult(expected: String, actual: String, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = Failure(mismatchMessages.mismatchMessage(expected, actual))
fun mismatchResult(expected: String, actual: Value?, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = mismatchMessages.valueMismatchFailure(expected, actual, mismatchMessages)
fun mismatchResult(expected: Value, actual: Value?, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = mismatchResult(valueError(expected) ?: "null", valueError(actual) ?: "nothing", mismatchMessages)
fun mismatchResult(expected: Pattern, actual: String, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = mismatchResult(expected.typeName, actual, mismatchMessages)
fun mismatchResult(pattern: Pattern, sampleData: Value?, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure = mismatchResult(pattern, sampleData?.toStringLiteral() ?: "null", mismatchMessages)
fun mismatchResult(thisPattern: Pattern, otherPattern: Pattern, mismatchMessages: MismatchMessages = DefaultMismatchMessages): Failure {
return mismatchResult(thisPattern.typeName, otherPattern.typeName, mismatchMessages)
}
fun valueError(value: Value?): String? {
return value?.valueErrorSnippet()
}