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

in.specmatic.core.pattern.XMLPattern.kt Maven / Gradle / Ivy

Go to download

Turn your contracts into executable specifications. Contract Driven Development - Collaboratively Design & Independently Deploy MicroServices & MicroFrontends. Deprecation Notice for group ID "in.specmatic" ****************************************************************************************************** Updates for "specmatic-core" will no longer be available under the deprecated group ID "in.specmatic". Please update your dependencies to use the new group ID "io.specmatic". ******************************************************************************************************

There is a newer version: 1.3.39
Show newest version
package `in`.specmatic.core.pattern

import `in`.specmatic.core.*
import `in`.specmatic.core.Result.Failure
import `in`.specmatic.core.Result.Success
import `in`.specmatic.core.utilities.mapZip
import `in`.specmatic.core.utilities.parseXML
import `in`.specmatic.core.value.*
import `in`.specmatic.core.wsdl.parser.message.MULTIPLE_ATTRIBUTE_VALUE
import `in`.specmatic.core.wsdl.parser.message.OCCURS_ATTRIBUTE_NAME

const val SPECMATIC_XML_ATTRIBUTE_PREFIX = "${APPLICATION_NAME_LOWER_CASE}_"
const val TYPE_ATTRIBUTE_NAME = "specmatic_type"
const val SOAP_BODY = "body"
const val SOAP_FAULT = "fault"

fun toTypeData(node: XMLNode): XMLTypeData = XMLTypeData(node.name, node.realName, attributeTypeMap(node), nodeTypes(node))

private fun nodeTypes(node: XMLNode): List {
    return node.childNodes.map {
        it.exactMatchElseType()
    }
}

private fun attributeTypeMap(node: XMLNode): Map {
    return node.attributes.mapValues { (key, value) ->
        when {
            value.isPatternToken() -> DeferredPattern(value.trimmed().toStringLiteral(), key)
            else -> ExactValuePattern(value)
        }
    }
}

data class XMLPattern(override val pattern: XMLTypeData = XMLTypeData(realName = ""), override val typeAlias: String? = null) : Pattern, SequenceType {
    constructor(node: XMLNode, typeAlias: String? = null) : this(toTypeData(node), typeAlias)
    constructor(xmlString: String, typeAlias: String? = null) : this(toXMLNode(parseXML(xmlString)), typeAlias)

    fun toPrettyString(): String {
        return pattern.toGherkinishNode().toPrettyStringValue()
    }

    override fun matches(sampleData: List, resolver: Resolver): ConsumeResult {
        val xmlValues = sampleData.filterIsInstance()
        if (xmlValues.size != sampleData.size)
            return ConsumeResult(Failure("XMLPattern can only match XML values"))

        return when {
            pattern.isOptionalNode() -> matchesOptionalNode(xmlValues, resolver)
            pattern.isMultipleNode() -> matchesMultipleNodes(xmlValues, resolver)
            else -> matchesRequiredNode(xmlValues, sampleData, resolver)
        }
    }

    private fun matchesRequiredNode(
            xmlValues: List,
            sampleData: List,
            resolver: Resolver
    ): ConsumeResult = if (xmlValues.isEmpty())
        ConsumeResult(Failure("Didn't get enough values", breadCrumb = this.pattern.name), sampleData)
    else
        ConsumeResult(matches(xmlValues.first(), resolver), xmlValues.drop(1))

    private fun matchesMultipleNodes(
            xmlValues: List,
            resolver: Resolver
    ): ConsumeResult {
        val remainder = xmlValues.dropWhile {
            matches(it, resolver) is Success
        }

        return if (remainder.isNotEmpty() && remainder.first().let { it is XMLNode && it.name == this.pattern.name }) {
            ConsumeResult(matches(remainder.first(), resolver), remainder)
        } else if (remainder.isNotEmpty()) {
            val provisionalError = ProvisionalError(
                    matches(remainder.first(), resolver) as Failure,
                    this,
                    remainder.first()
            )
            ConsumeResult(Success(), remainder, provisionalError)
        } else {
            ConsumeResult(Success(), remainder)
        }
    }

    private fun matchesOptionalNode(
            xmlValues: List,
            resolver: Resolver
    ): ConsumeResult = if (xmlValues.isEmpty())
        ConsumeResult(Success())
    else {
        val xmlValue = xmlValues.first()
        when (val result = matches(xmlValue, resolver)) {
            is Success -> ConsumeResult(Success(), xmlValues.drop(1))
            is Failure -> when {
                xmlValue is XMLNode && xmlValue.name == this.pattern.name -> ConsumeResult(result, xmlValues)
                else -> ConsumeResult(Success(), xmlValues, ProvisionalError(result, this, xmlValue))
            }
        }
    }

    override fun matches(sampleData: Value?, resolver: Resolver): Result {
        if (sampleData !is XMLNode)
            return Failure("Expected xml, got ${sampleData?.displayableType()}").breadCrumb(pattern.name)

        if(pattern.isNillable()) {
            if(sampleData.childNodes.isEmpty())
                return Success()
        }

        val matchingType = if (this.pattern.attributes.containsKey(TYPE_ATTRIBUTE_NAME)) {
            val typeName = this.pattern.getAttributeValue(TYPE_ATTRIBUTE_NAME)
            val xmlType = (resolver.getPattern("($typeName)") as XMLPattern)
            val attributesFromReferring = this.pattern.attributes.filterKeys { it != TYPE_ATTRIBUTE_NAME }
            val attributesFromReferred = xmlType.pattern.attributes.filterKeys { it != TYPE_ATTRIBUTE_NAME }
            val attributes = attributesFromReferred + attributesFromReferring
            xmlType.copy(pattern = xmlType.pattern.copy(name = this.pattern.name, realName = this.pattern.realName, attributes = attributes))
        } else {
            this
        }

        return matchName(sampleData, resolver).ifSuccess {
            matchingType.matchNamespaces(sampleData)
        }.ifSuccess {
            matchingType.matchAttributes(sampleData, resolver)
        }.ifSuccess {
            matchingType.matchNodes(sampleData, resolver)
        }.breadCrumb(pattern.name)
    }

    private fun matchNodes(
            sampleData: XMLNode,
            resolver: Resolver
    ): Result {
        if(sampleData.name.lowercase() == SOAP_BODY && sampleData.firstNode() is XMLNode && sampleData.firstNode()?.name?.lowercase() == SOAP_FAULT)
            return Success()

        val results = pattern.nodes.scanIndexed(
                ConsumeResult(
                        Success(),
                        sampleData.childNodes
                )
        ) { index, consumeResult, type ->
            when (val resolvedType = resolvedHop(type, resolver)) {
                is ListPattern -> ConsumeResult(
                    resolvedType.matches(
                        this.listOf(
                            consumeResult.remainder.subList(index, pattern.nodes.indices.last),
                            resolver
                        ), resolver
                    ),
                    emptyList()
                )
                else -> {
                    try {
                        if (sampleData.childNodes.size == 1 && consumeResult.remainder.size == 1 && sampleData.childNodes.first() is StringValue) {
                            val childValue = when (val childNode = sampleData.childNodes[index]) {
                                is StringValue -> when {
                                    childNode.isPatternToken() -> childNode.trimmed()
                                    else -> {
                                        attempt(
                                                "Couldn't read value ${childNode.string} as type ${resolvedType.pattern}",
                                                breadCrumb = breadCrumbIfXMLTag(resolvedType)
                                        ) { resolvedType.parse(childNode.string, resolver) }
                                    }
                                }
                                else -> childNode
                            }

                            val factKey = if (childValue is XMLNode) childValue.name else null
                            ConsumeResult(resolver.matchesPattern(factKey, resolvedType, childValue), emptyList())
                        } else if (expectingEmpty(sampleData, resolvedType, resolver)) {
                            ConsumeResult(Success())
                        } else {
                            resolvedType.matches(consumeResult.remainder, resolver).cast("xml")
                        }
                    } catch (e: ContractException) {
                        ConsumeResult(e.failure(), consumeResult.remainder)
                    }
                }
            }
        }

        return failureFrom(results) ?: Success()
    }

    private fun breadCrumbIfXMLTag(resolvedType: Pattern): String {
        return when (resolvedType) {
            is XMLPattern -> resolvedType.pattern.name
            else -> ""
        }
    }

    private fun failureFrom(results: List>): Result? {
        val nodeStructureMismatchError = results.find {
            it.result is Failure
        }?.result

        val nothingEvenCameCloseError = lazy {
            when {
                results.isNotEmpty() && results.last().remainder.isNotEmpty() -> {
                    val unexpectedValue = results.last().remainder.first()
                    unexpectedValue.matchFailure()
                }
                else -> null
            }
        }

        return (nodeStructureMismatchError ?: nothingEvenCameCloseError.value)
    }

    private fun matchNamespaces(sampleData: XMLNode): Result {
        return PLACEHOLDER_USE_GIT_BLAME_TO_FIND_RELEVANT_COMMIT(Success(), "Removed namespace validation but we should put it back")
    }

    private fun matchAttributes(sampleData: XMLNode, resolver: Resolver): Result {
        val patternAttributesWithoutXmlns = pattern.attributes.filterNot {
            it.key == "xmlns" || it.key.startsWith("xmlns:") || it.key.startsWith(SPECMATIC_XML_ATTRIBUTE_PREFIX)
        }
        val sampleAttributesWithoutXmlns = sampleData.attributes.filterNot {
            it.key == "xmlns" || it.key.startsWith("xmlns:") || it.key.startsWith(SPECMATIC_XML_ATTRIBUTE_PREFIX)
        }

        val missingKey = resolver.findKeyError(
                ignoreXMLNamespaces(patternAttributesWithoutXmlns),
                ignoreXMLNamespaces(sampleAttributesWithoutXmlns)
        )
        if (missingKey != null)
            return missingKey.missingKeyToResult("attribute", resolver.mismatchMessages)

        return matchAttributes(patternAttributesWithoutXmlns, sampleAttributesWithoutXmlns, resolver)
    }

    private fun matchName(sampleData: XMLNode, resolver: Resolver): Result {
        if (sampleData.name != pattern.name)
            return mismatchResult(pattern.name, sampleData.name, resolver.mismatchMessages)

        return Success()
    }

    private fun matchAttributes(
            patternAttributesWithoutXmlns: Map,
            sampleAttributesWithoutXmlns: Map,
            resolver: Resolver
    ): Result =
            mapZip(
                    ignoreXMLNamespaces(patternAttributesWithoutXmlns),
                    ignoreXMLNamespaces(sampleAttributesWithoutXmlns)
            ).asSequence().map { (key, patternValue, sampleValue) ->
                try {
                    val resolvedValue: Value = when {
                        sampleValue.isPatternToken() -> sampleValue.trimmed()
                        else -> patternValue.parse(sampleValue.string, resolver)
                    }
                    resolver.matchesPattern(key, patternValue, resolvedValue)
                } catch (e: ContractException) {
                    e.failure()
                }.breadCrumb(key)
            }.find { it is Failure } ?: Success()

    private fun  ignoreXMLNamespaces(attributes: Map): Map =
            attributes.filterNot { it.key.lowercase().startsWith("xmlns:") }

    private fun expectingEmpty(sampleData: XMLNode, type: Pattern, resolver: Resolver): Boolean {
        val resolvedPatternSet = type.patternSet(resolver).map { resolvedHop(it, resolver) }
        return sampleData.childNodes.isEmpty() && pattern.nodes.size == 1 && (EmptyStringPattern in resolvedPatternSet || StringPattern() in resolvedPatternSet)
    }

    override fun listOf(valueList: List, resolver: Resolver): Value {
        return XMLNode("", "", emptyMap(), valueList.map { it as XMLNode }, "", emptyMap())
    }

    override fun generate(resolver: Resolver): XMLNode {
        val name = pattern.name

        val nonSpecmaticAttributes = pattern.attributes.filterNot { it.key.startsWith(SPECMATIC_XML_ATTRIBUTE_PREFIX) }

        val newAttributes = nonSpecmaticAttributes.mapKeys { entry ->
            withoutOptionality(entry.key)
        }.mapValues { (key, pattern) ->
            attempt(breadCrumb = "$name.$key") {
                resolver.withCyclePrevention(pattern) { cyclePreventedResolver ->
                    cyclePreventedResolver.generate(key, pattern)
                }
            }
        }.mapValues {
            StringValue(it.value.toStringLiteral())
        }

        val nodes = pattern.nodes.asSequence().map {
            resolvedHop(it, resolver)
        }.map { pattern ->
            attempt(breadCrumb = name) {
                resolver.withCyclePrevention(pattern) { cyclePreventedResolver ->
                    when {
                        pattern is ListPattern -> (pattern.generate(cyclePreventedResolver) as XMLNode).childNodes
                        pattern is XMLPattern && pattern.occurMultipleTimes() ->
                            0.until(randomNumber(RANDOM_NUMBER_CEILING))
                                .map { pattern.generate(cyclePreventedResolver) }

                        else -> listOf(pattern.generate(cyclePreventedResolver))
                    }
                }
            }
        }.flatten().map {
            when (it) {
                is XMLValue -> it
                else -> StringValue(it.toStringLiteral())
            }
        }.toList()

        return XMLNode(pattern.realName, newAttributes, nodes)
    }

    override fun newBasedOn(row: Row, resolver: Resolver): Sequence {
        return forEachKeyCombinationIn(pattern.attributes, row) { attributePattern ->
            attempt(breadCrumb = this.pattern.name) {
                newBasedOn(attributePattern, row, resolver).map {
                    it.mapKeys { entry -> withoutOptionality(entry.key) }
                }
            }
        }.flatMap { newAttributes ->
            val newNodesList = when {
                row.containsField(pattern.name) -> {
                    attempt(breadCrumb = pattern.name) {
                        val dereferenced = dereferenceType(resolver)
                        if (dereferenced.pattern.nodes.isEmpty())
                            throw ContractException("Node ${pattern.name} is empty but an example with this name exists")

                        val parsedData =
                                dereferenced.pattern.nodes[0].parse(row.getField(dereferenced.pattern.name), resolver)
                        val matchResult = dereferenced.pattern.nodes[0].matches(parsedData, resolver)

                        if (matchResult is Failure)
                            throw ContractException(matchResult.toFailureReport())

                        sequenceOf(listOf(ExactValuePattern(parsedData)))
                    }
                }
                else -> {
                    listCombinations(pattern.nodes.map { childPattern ->
                        attempt(breadCrumb = this.pattern.name) {
                            when (childPattern) {
                                is XMLPattern -> {
                                    val dereferenced: XMLPattern = childPattern.dereferenceType(resolver)

                                    resolver.withCyclePrevention(dereferenced) { cyclePreventedResolver ->
                                        when {
                                            dereferenced.occurMultipleTimes() -> {
                                                dereferenced.newBasedOn(row, cyclePreventedResolver)
                                            }

                                            dereferenced.isOptional() -> {
                                                dereferenced.newBasedOn(row, cyclePreventedResolver).plus(null)
                                            }

                                            else -> dereferenced.newBasedOn(row, cyclePreventedResolver)
                                        }
                                    }
                                }
                                else -> resolver.withCyclePrevention(childPattern) { cyclePreventedResolver ->
                                    childPattern.newBasedOn(row, cyclePreventedResolver)
                                }
                            }
                        }
                    })
                }
            }

            newNodesList.map { newNodes ->
                XMLPattern(XMLTypeData(pattern.name, pattern.realName, newAttributes, newNodes))
            }
        }
    }

    override fun newBasedOn(resolver: Resolver): Sequence {
        return allOrNothingCombinationIn(pattern.attributes) { attributePattern ->
            attempt(breadCrumb = this.pattern.name) {
                newBasedOn(attributePattern, resolver).map {
                    it.mapKeys { entry -> withoutOptionality(entry.key) }
                }
            }
        }.flatMap { newAttributes ->
            val newNodesList = allOrNothingListCombinations(pattern.nodes.map { childPattern ->
                attempt(breadCrumb = this.pattern.name) {
                    when (childPattern) {
                        is XMLPattern -> {
                            val dereferenced: XMLPattern = childPattern.dereferenceType(resolver)

                            resolver.withCyclePrevention(dereferenced) { cyclePreventedResolver ->
                                when {
                                    dereferenced.occurMultipleTimes() -> {
                                        dereferenced.newBasedOn(cyclePreventedResolver)
                                    }

                                    dereferenced.isOptional() -> {
                                        dereferenced.newBasedOn(cyclePreventedResolver).plus(null)
                                    }

                                    else -> dereferenced.newBasedOn(cyclePreventedResolver)
                                }
                            }
                        }
                        else -> resolver.withCyclePrevention(childPattern) { cyclePreventedResolver ->
                            childPattern.newBasedOn(cyclePreventedResolver)
                        }
                    }
                }
            }
        )

            newNodesList.map { newNodes ->
                XMLPattern(XMLTypeData(pattern.name, pattern.realName, newAttributes, newNodes.toList()))
            }
        }
    }

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

    private fun dereferenceType(resolver: Resolver): XMLPattern {
        if (!hasType()) {
            return this
        }

        val specmaticType = pattern.attributes[TYPE_ATTRIBUTE_NAME]
        val resolved = resolver.getPattern("($specmaticType)") as XMLPattern
        return resolved.copy(
                pattern = resolved.pattern.copy(
                        name = this.pattern.name,
                        realName = this.pattern.realName,
                        attributes = resolved.pattern.attributes.plus(this.pattern.attributes)
                )
        )
    }

    private fun hasType(): Boolean = pattern.attributes.containsKey(TYPE_ATTRIBUTE_NAME)

    fun occurMultipleTimes(): Boolean = pattern.getNodeOccurrence() == NodeOccurrence.Multiple

    private fun isOptional(): Boolean = pattern.getNodeOccurrence() == NodeOccurrence.Optional

    override fun parse(value: String, resolver: Resolver): Value {
        return toXMLNode(parseXML(value))
    }

    override fun encompasses(
            otherPattern: Pattern,
            thisResolver: Resolver,
            otherResolver: Resolver,
            typeStack: TypeStack
    ): Result {
        val otherResolvedPattern = resolvedHop(otherPattern, otherResolver)

        return when (otherResolvedPattern) {
            is ExactValuePattern -> otherResolvedPattern.fitsWithin(
                    listOf(this),
                    otherResolver,
                    thisResolver,
                    typeStack
            )
            is XMLPattern -> nodeNamesShouldBeEqual(otherResolvedPattern).ifSuccess {
                mapEncompassesMap(
                        pattern.attributes.filterNot { it.key.startsWith(SPECMATIC_XML_ATTRIBUTE_PREFIX) || it.key.startsWith("xmlns:") },
                        otherResolvedPattern.pattern.attributes.filterNot { it.key.startsWith(SPECMATIC_XML_ATTRIBUTE_PREFIX) || it.key.startsWith("xmlns:") },
                        thisResolver,
                        otherResolver
                )
            }.ifSuccess {
                thisOccurrenceEncompassesTheOther(this, otherResolvedPattern)
            }.ifSuccess {
                val theseMembers = this.memberList
                val otherMembers = otherResolvedPattern.memberList

                val others = otherMembers.getEncompassables(otherResolver)
                val these = adapt(adaptFromList(theseMembers.getEncompassables(thisResolver), thisResolver), thisResolver)

                val adaptedOthers = adapt(others, otherResolver)

                val results =
                        these.runningFold(ConsumeResult(adaptedOthers)) { consumeResult, thisOne ->
                            val unmatched = dropOneIfMatching(consumeResult.remainder) { otherOne ->
                                encompassResult(thisOne, otherOne, thisResolver, otherResolver, typeStack)
                            }

                            if (unmatched.size == consumeResult.remainder.size) {
                                val failure = encompassResult(
                                        thisOne,
                                        unmatched.first(),
                                        thisResolver,
                                        otherResolver,
                                        typeStack
                                ) as Failure
                                ConsumeResult(failure, unmatched)
                            } else {
                                ConsumeResult(Success(), unmatched)
                            }
                        }

                val failureResult = results.find { it.result is Failure }?.result

                failureResult ?: provisionalFailure(results) ?: Success()
            }
            else -> mismatchResult(this, otherResolvedPattern, thisResolver.mismatchMessages)
        }.breadCrumb(this.pattern.name)
    }

    private fun provisionalFailure(results: List>): Failure? {
        if (results.isEmpty() || results.last().remainder.isEmpty())
            return null

        val provisionalError = results.map { it.provisionalError?.result }.filterIsInstance().firstOrNull()

        return provisionalError ?: genericProvisionalFailure(results)
    }

    private fun genericProvisionalFailure(results: List>): Failure {
        val unmatched = results.last().remainder

        val nodeDescriptor = when (unmatched.size) {
            1 -> "Node"
            else -> "Nodes"
        }

        val names = unmatched.joinToString(", ") {
            when (it) {
                is XMLPattern -> it.pattern.name
                else -> it.typeName
            }
        }

        return Failure("$nodeDescriptor named $names were not matched")
    }

    private fun dropOneIfMatching(remainder: List, test: (Pattern) -> Result): List {
        if(remainder.isEmpty())
            return remainder

        val first = remainder.first()

        return when(test(first)) {
            is Success -> remainder.drop(1)
            is Failure -> remainder
        }
    }

    private fun encompassResult(
            thisOne: Pattern,
            otherOne: Pattern,
            thisResolver: Resolver,
            otherResolver: Resolver,
            typeStack: TypeStack
    ): Result {
        return try {
            val otherOneAdjustedForExactValue = when {
                otherOne is ExactValuePattern && otherOne.pattern is StringValue -> ExactValuePattern(
                        thisOne.parse(
                                otherOne.pattern.string,
                                thisResolver
                        )
                )
                else -> otherOne
            }

            thisOne.encompasses(otherOneAdjustedForExactValue, thisResolver, otherResolver, typeStack)
        } catch (e: Throwable) {
            Failure(e.message ?: e.javaClass.name)
        }

    }

    private fun getNodeOccurrence(): NodeOccurrence {
        return pattern.getNodeOccurrence()
    }

    private fun thisOccurrenceEncompassesTheOther(thisOne: XMLPattern, otherOne: XMLPattern): Result {
        val thisTypeOccurrence = thisOne.pattern.getAttributeValue(OCCURS_ATTRIBUTE_NAME)
        val otherTypeOccurrence = otherOne.pattern.getAttributeValue(OCCURS_ATTRIBUTE_NAME)

        return when (thisTypeOccurrence) {
            otherTypeOccurrence -> Success()
            else -> thisOne.getNodeOccurrence().encompasses(otherOne.getNodeOccurrence())
        }
    }

    private fun nodeNamesShouldBeEqual(otherResolvedPattern: XMLPattern) = when {
        pattern.name != otherResolvedPattern.pattern.name ->
            Failure("Expected a node named ${pattern.name}, but got ${otherResolvedPattern.pattern.name} instead.")
        else -> Success()
    }

    private fun adapt(types: List, resolver: Resolver): List {
        if (types.isEmpty())
            return listOf(EmptyStringPattern)

        return adaptFromList(types, resolver)
    }

    private fun adaptFromList(types: List, resolver: Resolver): List {
        val first = types.firstOrNull()

        if (first is ListPattern) {
            val typeName = first.pattern.pattern.toString().removeSurrounding("(", ")")
            val type = resolver.getPattern("($typeName)")
            if (type !is XMLPattern)
                throw ContractException("A list specification inside an XML definition must refer to an XML type. But $typeName is not an XML type.")

            val multipleOccursAttribute =
                    mapOf(OCCURS_ATTRIBUTE_NAME to ExactValuePattern(StringValue(MULTIPLE_ATTRIBUTE_VALUE)))

            return listOf(
                    type.copy(
                            pattern = type.pattern.copy(
                                    attributes = type.pattern.attributes.plus(
                                            multipleOccursAttribute
                                    )
                            )
                    )
            )
        }

        return types
    }

    val referredType: String?
        get() {
            return pattern.attributes[TYPE_ATTRIBUTE_NAME]?.let {
                it as ExactValuePattern
                it.pattern.toStringLiteral()
            }
        }

    override val memberList: MemberList
        get() = MemberList(pattern.nodes, null)

    override val typeName: String = "xml"

    // TODO not sure if this is still needed
    fun toGherkinString(additionalIndent: String = "", indent: String = ""): String {
        return pattern.toGherkinString(additionalIndent, indent)
    }

    fun toGherkinXMLNode(): XMLNode {
        return pattern.toGherkinishNode()
    }

    fun toGherkinStatement(specmaticTypeName: String): String {
        val typeString = this.toGherkinXMLNode().toPrettyStringValue().trim()
        return "And type $specmaticTypeName\n\"\"\"\n$typeString\n\"\"\""
    }
}

private fun  PLACEHOLDER_USE_GIT_BLAME_TO_FIND_RELEVANT_COMMIT (value: T, s: String): T {
    return value
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy