All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.specmatic.core.pattern.TabularPattern.kt Maven / Gradle / Ivy

Go to download

Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends.

There is a newer version: 2.0.37
Show newest version
package io.specmatic.core.pattern

import io.cucumber.messages.types.TableRow
import io.specmatic.core.Resolver
import io.specmatic.core.Result
import io.specmatic.core.UnexpectedKeyCheck
import io.specmatic.core.ValidateUnexpectedKeys
import io.specmatic.core.mismatchResult
import io.specmatic.core.pattern.config.NegativePatternConfiguration
import io.specmatic.core.utilities.Flags.Companion.MAX_TEST_REQUEST_COMBINATIONS
import io.specmatic.core.utilities.Flags.Companion.getStringValue
import io.specmatic.core.utilities.mapZip
import io.specmatic.core.utilities.stringToPatternMap
import io.specmatic.core.utilities.withNullPattern
import io.specmatic.core.value.BooleanValue
import io.specmatic.core.value.JSONArrayValue
import io.specmatic.core.value.JSONObjectValue
import io.specmatic.core.value.NullValue
import io.specmatic.core.value.NumberValue
import io.specmatic.core.value.StringValue
import io.specmatic.core.value.Value

fun toTabularPattern(jsonContent: String, typeAlias: String? = null): TabularPattern =
    toTabularPattern(stringToPatternMap(jsonContent), typeAlias)

fun toTabularPattern(map: Map, typeAlias: String? = null): TabularPattern {
    val missingKeyStrategy: UnexpectedKeyCheck = when ("...") {
        in map -> IgnoreUnexpectedKeys
        else -> ValidateUnexpectedKeys
    }

    return TabularPattern(map.minus("..."), missingKeyStrategy, typeAlias)
}

data class TabularPattern(
    override val pattern: Map,
    private val unexpectedKeyCheck: UnexpectedKeyCheck = ValidateUnexpectedKeys,
    override val typeAlias: String? = null
) : Pattern {
    override fun matches(sampleData: Value?, resolver: Resolver): Result {
        if (sampleData !is JSONObjectValue)
            return mismatchResult("JSON object", sampleData, resolver.mismatchMessages)

        val resolverWithNullType = withNullPattern(resolver)

        val keyErrors: List =
            resolverWithNullType.findKeyErrorList(pattern, sampleData.jsonObject).map {
                it.missingKeyToResult("key", resolver.mismatchMessages).breadCrumb(it.name)
            }

        val results: List =
            mapZip(pattern, sampleData.jsonObject).map { (key, patternValue, sampleValue) ->
                resolverWithNullType.matchesPattern(key, patternValue, sampleValue).breadCrumb(key)
            }.filterIsInstance()

        val failures = keyErrors.plus(results)

        return if (failures.isEmpty())
            Result.Success()
        else
            Result.Failure.fromFailures(failures)
    }

    override fun listOf(valueList: List, resolver: Resolver): Value = JSONArrayValue(valueList)

    override fun generate(resolver: Resolver): JSONObjectValue {
        val resolverWithNullType = withNullPattern(resolver)
        return JSONObjectValue(pattern.mapKeys { entry -> withoutOptionality(entry.key) }.mapValues { (key, pattern) ->
            attempt(breadCrumb = key) { resolverWithNullType.withCyclePrevention(pattern) {it.generate(key, pattern)} }
        })
    }

    override fun generateWithAll(resolver: Resolver): Value {
        return attempt(breadCrumb = "HEADERS") {
            JSONObjectValue(pattern.filterNot { it.key == "..." }.mapKeys {
                attempt(breadCrumb = it.key) {
                    withoutOptionality(it.key)
                }
            }.mapValues {
                it.value.generateWithAll(resolver)
            })
        }
    }

    override fun newBasedOn(row: Row, resolver: Resolver): Sequence> {
        val resolverWithNullType = withNullPattern(resolver)
        return allOrNothingCombinationIn(
            pattern,
            resolver.resolveRow(row),
            null,
            null, returnValues { pattern: Map ->
                newMapBasedOn(pattern, row, resolverWithNullType).map { it.value }
            }).map { it.value }.map {
            toTabularPattern(it.mapKeys { (key, _) ->
                withoutOptionality(key)
            })
        }.map { HasValue(it) }
    }

    override fun newBasedOn(resolver: Resolver): Sequence {
        val resolverWithNullType = withNullPattern(resolver)
        val allOrNothingCombinationIn =
            allOrNothingCombinationIn(
                pattern,
                Row(),
                null,
                null, returnValues { pattern: Map ->
                    newBasedOn(pattern, resolverWithNullType)
                }).map { it.value }
        return allOrNothingCombinationIn.map { toTabularPattern(it) }
    }

    override fun negativeBasedOn(row: Row, resolver: Resolver, config: NegativePatternConfiguration): Sequence> {
        return this.newBasedOn(row, resolver).map { it.value }.map { HasValue(it) }
    }

    override fun parse(value: String, resolver: Resolver): Value = parsedJSONObject(value, resolver.mismatchMessages)
    override fun encompasses(
        otherPattern: Pattern,
        thisResolver: Resolver,
        otherResolver: Resolver,
        typeStack: TypeStack
    ): Result {
        val thisResolverWithNullType = withNullPattern(thisResolver)
        val otherResolverWithNullType = withNullPattern(otherResolver)

        return when (otherPattern) {
            is ExactValuePattern -> otherPattern.fitsWithin(
                listOf(this),
                otherResolverWithNullType,
                thisResolverWithNullType,
                typeStack
            )
            is TabularPattern -> mapEncompassesMap(
                pattern,
                otherPattern.pattern,
                thisResolverWithNullType,
                otherResolverWithNullType,
                typeStack
            )
            is JSONObjectPattern -> mapEncompassesMap(
                pattern,
                otherPattern.pattern,
                thisResolverWithNullType,
                otherResolverWithNullType,
                typeStack
            )
            else -> Result.Failure("Expected json type, got ${otherPattern.typeName}")
        }
    }

    override val typeName: String = "json object"
}

fun newMapBasedOn(patternMap: Map, row: Row, resolver: Resolver): Sequence>> {
    val patternCollection: Map>> = patternMap.mapValues { (key, pattern) ->
        attempt(breadCrumb = withoutOptionality(key)) {
            newPatternsBasedOn(row, key, pattern, resolver).map {
                it.addDetails(breadCrumb = withoutOptionality(key), message = "")
            }
        }
    }

    return patternList(patternCollection)
}

fun newBasedOn(patternMap: Map, resolver: Resolver): Sequence> {
    val patternCollection = patternMap.mapValues { (key, pattern) ->
        attempt(breadCrumb = withoutOptionality(key)) {
            newBasedOn(key, pattern, resolver)
        }
    }

    return patternValues(patternCollection)
}

fun newPatternsBasedOn(row: Row, key: String, pattern: Pattern, resolver: Resolver): Sequence> {
    val keyWithoutOptionality = key(pattern, key)

    return when {
        row.containsField(keyWithoutOptionality) -> {
            val rowValue = row.getField(keyWithoutOptionality)

            if (isPatternToken(rowValue)) {
                val rowPattern = resolver.getPattern(rowValue)

                attempt(breadCrumb = keyWithoutOptionality) {
                    when (val result = pattern.encompasses(rowPattern, resolver, resolver)) {
                        is Result.Success -> {
                            resolver.withCyclePrevention(rowPattern, isOptional(key)) { cyclePreventedResolver ->
                                rowPattern.newBasedOn(row, cyclePreventedResolver)
                            }?:
                            // Handle cycle (represented by null value) by using empty sequence for optional properties
                            emptySequence()
                        }
                        is Result.Failure -> throw ContractException(result.toFailureReport())
                    }
                }
            } else {
                val parsedRowValue = attempt("Format error in example of \"$keyWithoutOptionality\"") {
                    resolver.parse(pattern, rowValue)
                }

                val exactValuePattern =
                    when (val matchResult = resolver.matchesPattern(null, pattern, parsedRowValue)) {
                        is Result.Failure -> throw ContractException(matchResult.toFailureReport())
                        else -> ExactValuePattern(parsedRowValue)
                    }

                val generativePatterns: Sequence> = resolver.generatedPatternsForGenerativeTests(pattern, key)

                val sequence: Sequence> =
                    sequenceOf(HasValue(exactValuePattern))

                val filteredGenerativePatterns: Sequence> = generativePatterns.filterNot { generativePatternR ->
                        generativePatternR.withDefault(false) { generativePattern ->
                            generativePattern.encompasses(exactValuePattern, resolver, resolver) is Result.Success
                        }
                    }

                sequence + filteredGenerativePatterns
            }
        }
        else -> resolver.withCyclePrevention(pattern, isOptional(key)) { cyclePreventedResolver ->
            pattern.newBasedOn(row.stepDownOneLevelInJSONHierarchy(keyWithoutOptionality), cyclePreventedResolver)
        }?:
        // Handle cycle (represented by null value) by using empty list for optional properties
        emptySequence()
    }
}

fun newBasedOn(key: String, pattern: Pattern, resolver: Resolver): Sequence {
    return resolver.withCyclePrevention(pattern, isOptional(key)) { cyclePreventedResolver ->
        pattern.newBasedOn(cyclePreventedResolver)
    }?:
    // Handle cycle (represented by null value) by using empty list for optional properties
    emptySequence()
}

fun key(pattern: Pattern, key: String): String {
    return withoutOptionality(
        when (pattern) {
            is Keyed -> pattern.key ?: key
            else -> key
        }
    )
}

fun  patternList(patternCollection: Map>>): Sequence>> {
    if (patternCollection.isEmpty())
        return sequenceOf(HasValue(emptyMap()))

    val maxTestRequestCombinations = getStringValue(MAX_TEST_REQUEST_COMBINATIONS)?.toInt() ?: Int.MAX_VALUE
    val spec = CombinationSpec(patternCollection, maxTestRequestCombinations)
    return spec.selectedCombinations
}

fun  patternValues(patternCollection: Map>): Sequence> {
    if (patternCollection.isEmpty())
        return sequenceOf(emptyMap())

    val first = mutableMapOf()
    val ranOut = first.mapValues { false }.toMutableMap()

    val iterators = patternCollection.mapValues {
        it.value.iterator()
    }.filter {
        it.value.hasNext()
    }

    return sequence {
        while (true) {
            val nextValue = iterators.mapValues { (key, iterator) ->
                val nextValueFromIterator = if (iterator.hasNext()) {
                    val value = iterator.next()

                    first.putIfAbsent(key, value)

                    value
                } else {
                    ranOut[key] = true
                    first.getValue(key)
                }

                nextValueFromIterator
            }

            if (ranOut.size == iterators.size && ranOut.all { it.value }) {
                break
            }

            yield(nextValue)
        }
    }
}

private fun  keyCombinations(
    valuePatternOptions: Map>,
    optionalSelector: (String, List) -> Pair
): Map {
    return valuePatternOptions
        .filterValues { it.isNotEmpty() }
        .map { (key, value) ->
            optionalSelector(key, value)
        }.toMap()
}

fun  forEachKeyCombinationGivenRowIn(
    patternMap: Map,
    row: Row,
    resolver: Resolver,
    creator: (Map) -> Sequence>>
): Sequence>> =
    keySets(patternMap.keys.toList(), row, resolver).map { keySet ->
        patternMap.filterKeys { key -> key in keySet }
    }.map { newPattern ->
        creator(newPattern)
    }.flatten()

fun  forEachKeyCombinationIn(
    patternMap: Map,
    row: Row,
    creator: (Map) -> Sequence>>
): Sequence>> =
    keySets(patternMap.keys.toList(), row).map { keySet ->
        patternMap.filterKeys { key -> key in keySet }
    }.map { newPattern ->
        creator(newPattern)
    }.flatten()

fun  returnValues(function: (Map) -> Sequence>): (Map) -> Sequence>> {
    val wrappedFunction: (Map) -> Sequence>> = { map ->
        function(map).map { HasValue(it) }
    }

    return wrappedFunction
}

fun  allOrNothingCombinationIn(
    patternMap: Map,
    row: Row = Row(),
    minPropertiesOrNull: Int? = null,
    maxPropertiesOrNull: Int? = null,
    creator: (Map) -> Sequence>>
): Sequence>> {
    val keyLists = if (patternMap.keys.any { isOptional(it) }) {
        val nothingList: Set =
            patternMap.keys.filter { k -> !isOptional(k) || row.containsField(withoutOptionality(k)) }.toSet()
                .let { propertyNames ->
                    minPropertiesOrNull?.let { minProperties ->
                        if (propertyNames.size >= minProperties)
                            propertyNames
                        else {
                            val remainingPropertyNames = patternMap.keys.minus(propertyNames)
                            propertyNames + remainingPropertyNames.shuffled().toList()
                                .take(minProperties - propertyNames.size).toSet()
                        }
                    } ?: propertyNames
                }

        val allList: Set = patternMap.keys.let { propertyNames ->
            maxPropertiesOrNull?.let { maxProperties ->
                if (propertyNames.size <= maxProperties)
                    propertyNames
                else {
                    val remainingPropertyNames = patternMap.keys.minus(nothingList)
                    nothingList + remainingPropertyNames.shuffled().toList().take(maxProperties - nothingList.size)
                        .toSet()
                }
            } ?: propertyNames
        }

        sequenceOf(allList, nothingList).distinct()
    } else {
        sequenceOf(patternMap.keys)
    }

    val keySets: Sequence> = keyLists.map { keySet ->
        patternMap.filterKeys { key -> key in keySet }
    }.asSequence()

    val keySetValues = keySets.map { newPattern ->
        creator(newPattern)
    }

    return keySetValues.flatten()
}

internal fun keySets(listOfKeys: List, row: Row, resolver: Resolver): Sequence> {
    if (listOfKeys.isEmpty())
        return sequenceOf(listOfKeys)

    val key = listOfKeys.last()
    val subLists = keySets(listOfKeys.dropLast(1), row)

    return subLists.flatMap { subList ->
        when {
            row.containsField(withoutOptionality(key)) ->
                resolver.generateKeySubLists(key, subList)
            isOptional(key) -> sequenceOf(subList, subList + key)
            else -> sequenceOf(subList + key)
        }
    }
}

internal fun keySets(listOfKeys: List, row: Row): Sequence> {
    if (listOfKeys.isEmpty())
        return sequenceOf(listOfKeys)

    val key = listOfKeys.last()
    val subLists = keySets(listOfKeys.dropLast(1), row)

    return subLists.flatMap { subList ->
        when {
            row.containsField(withoutOptionality(key)) -> listOf(subList + key)
            isOptional(key) -> listOf(subList, subList + key)
            else -> listOf(subList + key)
        }
    }
}

fun rowsToTabularPattern(rows: List, typeAlias: String? = null) =
    toTabularPattern(rows.map { it.cells }.associate { (key, value) ->
        key.value to toJSONPattern(value.value)
    }, typeAlias)

fun toJSONPattern(value: String): Pattern {
    return value.trim().let {
        val asNumber: Number? = try {
            convertToNumber(value)
        } catch (e: Throwable) {
            null
        }

        when {
            asNumber != null -> ExactValuePattern(NumberValue(asNumber))
            it.startsWith("\"") && it.endsWith("\"") ->
                ExactValuePattern(StringValue(it.removeSurrounding("\"")))
            it == "null" -> ExactValuePattern(NullValue)
            it == "true" -> ExactValuePattern(BooleanValue(true))
            it == "false" -> ExactValuePattern(BooleanValue(false))
            else -> parsedPattern(value)
        }
    }
}

fun isNumber(value: StringValue): Boolean {
    return try {
        convertToNumber(value.string)
        true
    } catch (e: ContractException) {
        false
    }
}

fun convertToNumber(value: String): Number = value.trim().let {
    stringToInt(it) ?: stringToLong(it) ?: stringToFloat(it) ?: stringToDouble(it)
    ?: throw ContractException("""Expected number, actual was "$value"""")
}

internal fun stringToInt(value: String): Int? = try {
    value.toInt()
} catch (e: Throwable) {
    null
}

internal fun stringToLong(value: String): Long? = try {
    value.toLong()
} catch (e: Throwable) {
    null
}

internal fun stringToFloat(value: String): Float? = try {
    value.toFloat()
} catch (e: Throwable) {
    null
}

internal fun stringToDouble(value: String): Double? = try {
    value.toDouble()
} catch (e: Throwable) {
    null
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy