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

com.microsoft.thrifty.schema.SchemaFunctionalEquality.kt Maven / Gradle / Ivy

/*
 * Thrifty
 *
 * Copyright (c) Microsoft Corporation
 *
 * All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the License);
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * THIS CODE IS PROVIDED ON AN  *AS IS* BASIS, WITHOUT WARRANTIES OR
 * CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING
 * WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE,
 * FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT.
 *
 * See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.
 */
@file:JvmName("SchemaFunctionalEquality")

package com.microsoft.thrifty.schema

/*
 * Functional ABI equality checking for Thrifty elements and Schemas. This exists because we can't
 * compare raw element types together due to misc metadata that their AutoValue representations
 * would report as differing but are not relevant to the actual ABI (location, formatting, etc).
 */

/**
 * For comparing docs, we remove stars as things get weird in the parser.
 */
private fun String.cleanedDoc(): String {
    return trim().replace("*", "")
}

/**
 * A fully qualified class name of a given [UserType], used for equality checking.
 */
private inline val UserType.fqcn: String
    get() = "$javaPackage.$name"

/**
 * The java package name from the spec. We always assume its there because we don't support specs that don't.
 */
private inline val UserType.javaPackage: String
    get() = getNamespaceFor(NamespaceScope.JAVA)!!

/**
 * The java package name from the spec. We always assume its there because we don't support specs that don't.
 */
private inline val Constant.javaPackage: String
    get() {
        return getNamespaceFor(NamespaceScope.JAVA)!!
    }

/**
 * Checks that this [ThriftType] is equal to a given [other] [ThriftType].
 *
 * The following properties are checked:
 * - [ThriftType.annotations]
 * - [SetType.elementType]
 * - [ListType.elementType]
 * - [MapType.keyType]
 * - [MapType.valueType]
 * - [BuiltinType] are compared by equality
 * - [UserType] are compared by type checks, then fully qualified class name for linking. Set
 * [deepCheck] to enable deep comparisons on UserTypes too.
 *
 * @param other the other [ThriftType] to check
 * @param deepCheck a flag to signal whether or not the check should be deep. By default this is
 * `false` and UserTypes will only be compared by their fully qualified names (basically a linking
 * check).
 * @param lazyMessage a message to report if a check fails
 */
fun ThriftType.checkFunctionallyEquals(
    other: ThriftType,
    deepCheck: Boolean = false,
    lazyMessage: () -> String
) {
    check(annotations == other.annotations, lazyMessage)
    when (this) {
        is BuiltinType -> {
            check(this == other, lazyMessage)
        }
        is SetType -> {
            check(other is SetType)
            elementType.checkFunctionallyEquals((other as SetType).elementType, deepCheck, lazyMessage)
        }
        is ListType -> {
            check(other is ListType)
            elementType.checkFunctionallyEquals((other as ListType).elementType, deepCheck, lazyMessage)
        }
        is MapType -> {
            check(other is MapType)
            keyType.checkFunctionallyEquals((other as MapType).keyType, deepCheck, lazyMessage)
            valueType.checkFunctionallyEquals(other.valueType, deepCheck, lazyMessage)
        }
        is UserType -> {
            when (this) {
                is StructType -> {
                    check(other is StructType, lazyMessage)
                    check(fqcn == (other as StructType).fqcn, lazyMessage)
                    if (deepCheck) {
                        checkFunctionallyEquals(other)
                    }
                }
                is EnumType -> {
                    check(other is EnumType, lazyMessage)
                    check(fqcn == (other as EnumType).fqcn, lazyMessage)
                    if (deepCheck) {
                        checkFunctionallyEquals(other)
                    }
                }
                is TypedefType -> {
                    check(other is TypedefType, lazyMessage)
                    check(fqcn == (other as TypedefType).fqcn, lazyMessage)
                    if (deepCheck) {
                        checkFunctionallyEquals(other)
                    }
                }
                is ServiceType -> {
                    check(other is ServiceType, lazyMessage)
                    check(fqcn == (other as ServiceType).fqcn, lazyMessage)
                    if (deepCheck) {
                        checkFunctionallyEquals(other)
                    }
                }
            }
        }
    }
}

/**
 * Checks that this [Field] is equal to a given [other] [Field].
 *
 * The following properties are checked:
 * - [Field.documentation]
 * - [Field.name]
 * - [Field.annotations]
 * - [Field.type]
 * - [Field.required]
 * - [Field.optional]
 *
 * @param other the other [Field] to check
 * @param prefix a contextual prefix to use in error messaging, as [Field]s can be used in
 * [StructType.fields], [ServiceMethod.parameters], and [ServiceMethod.exceptions].
 */
fun Field.checkFunctionallyEquals(other: Field, prefix: String) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "$prefix documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "$prefix name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(annotations == other.annotations) {
        "$prefix annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    type.checkFunctionallyEquals(other.type) {
        "$prefix type mismatch at $location. Found $type but expected ${other.type}"
    }
    check(required == other.required) {
        "$prefix required mismatch at $location. Found $required but expected ${other.required}"
    }
    check(optional == other.optional) {
        "$prefix optional mismatch at $location. Found $optional but expected ${other.optional}"
    }
}

/**
 * Checks that this [ServiceMethod] is equal to a given [other] [ServiceMethod].
 *
 * The following properties are checked:
 * - [ServiceMethod.documentation]
 * - [ServiceMethod.name]
 * - [ServiceMethod.annotations]
 * - [ServiceMethod.returnType]
 * - [ServiceMethod.parameters]
 * - [ServiceMethod.exceptions]
 *
 * @param other the other [StructType] to check
 */
fun ServiceMethod.checkFunctionallyEquals(other: ServiceMethod) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Service method documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Service method name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(annotations == other.annotations) {
        "Service method annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    returnType.checkFunctionallyEquals(other.returnType) {
        "Service method return type mismatch at $location. Found $returnType but expected ${other.returnType}"
    }
    parameters.zip(other.parameters)
        .forEach { (parameter1, parameter2) ->
            parameter1.checkFunctionallyEquals(parameter2, "Service method parameter")
        }
    exceptions.zip(other.exceptions)
        .forEach { (exception1, exception2) ->
            exception1.checkFunctionallyEquals(exception2, "Service method exception")
        }
}

/**
 * Checks that this [StructType] is equal to a given [other] [StructType].
 *
 * The following properties are checked:
 * - [StructType.documentation]
 * - [StructType.name]
 * - [StructType.namespaces]
 * - [StructType.annotations]
 * - [StructType.isUnion]
 * - [StructType.isException]
 * - [StructType.isStruct]
 * - [StructType.fields]
 *
 * @param other the other [StructType] to check
 */
fun StructType.checkFunctionallyEquals(other: StructType) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Struct documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Struct name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(namespaces == other.namespaces) {
        "Struct namespaces mismatch at $location. Found $namespaces but expected ${other.namespaces}"
    }
    check(annotations == other.annotations) {
        "Struct annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    check(isUnion == other.isUnion) {
        "Struct isUnion mismatch at $location. Found $isUnion but expected ${other.isUnion}"
    }
    check(isException == other.isException) {
        "Struct isException mismatch at $location. Found $isException but expected ${other.isException}"
    }
    check(isStruct == other.isStruct) {
        "Struct isStruct mismatch at $location. Found $isStruct but expected ${other.isStruct}"
    }
    fields.zip(other.fields)
        .forEach { (field1, field2) ->
            field1.checkFunctionallyEquals(field2, "Struct field")
        }
}

/**
 * Checks that this [ServiceType] is equal to a given [other] [ServiceType].
 *
 * The following properties are checked:
 * - [ServiceType.documentation]
 * - [ServiceType.name]
 * - [ServiceType.namespaces]
 * - [ServiceType.annotations]
 * - [ServiceType.methods]
 *
 * @param other the other [ServiceType] to check
 */
fun ServiceType.checkFunctionallyEquals(other: ServiceType) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Service documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Service name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(namespaces == other.namespaces) {
        "Service namespaces mismatch at $location. Found $namespaces but expected ${other.namespaces}"
    }
    check(annotations == other.annotations) {
        "Service annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    methods.zip(other.methods)
        .forEach { (method1, method2) ->
            method1.checkFunctionallyEquals(method2)
        }
}

/**
 * Checks that this [Constant] is equal to a given [other] [Constant].
 *
 * The following properties are checked:
 * - [Constant.documentation]
 * - [Constant.name]
 * - [Constant.namespaces]
 * - [Constant.annotations]
 * - [Constant.value]
 *
 * @param other the other [Constant] to check
 */
fun Constant.checkFunctionallyEquals(other: Constant) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Constant documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Constant name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(namespaces == other.namespaces) {
        "Constant namespaces mismatch at $location. Found $namespaces but expected ${other.namespaces}"
    }
    check(annotations == other.annotations) {
        "Constant annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    check(value == other.value) {
        "Constant value mismatch at $location. Found $value but expected ${other.value}"
    }
}

/**
 * Checks that this [EnumMember] is equal to a given [other] [EnumMember].
 *
 * The following properties are checked:
 * - [EnumMember.documentation]
 * - [EnumMember.name]
 * - [EnumMember.annotations]
 * - [EnumMember.value]
 *
 * @param other the other [EnumMember] to check
 */
fun EnumMember.checkFunctionallyEquals(other: EnumMember) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Enum member documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Enum member name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(annotations == other.annotations) {
        "Enum member annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    check(value == other.value) {
        "Enum member value mismatch at $location. Found $value but expected ${other.value}"
    }
}

/**
 * Checks that this [EnumType] is equal to a given [other] [EnumType].
 *
 * The following properties are checked:
 * - [EnumType.documentation]
 * - [EnumType.name]
 * - [EnumType.namespaces]
 * - [EnumType.annotations]
 * - [EnumType.members]
 *
 * @param other the other [EnumType] to check
 */
fun EnumType.checkFunctionallyEquals(other: EnumType) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Enum documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Enum name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(namespaces == other.namespaces) {
        "Enum namespaces mismatch at $location. Found $namespaces but expected ${other.namespaces}"
    }
    check(annotations == other.annotations) {
        "Enum annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    members.zip(other.members)
        .forEach { (member1, member2) ->
            member1.checkFunctionallyEquals(member2)
        }
}

/**
 * Checks that this [TypedefType] is equal to a given [other] [TypedefType].
 *
 * The following properties are checked:
 * - [TypedefType.documentation]
 * - [TypedefType.name]
 * - [TypedefType.namespaces]
 * - [TypedefType.annotations]
 * - [TypedefType.oldType]
 *
 * @param other the other [TypedefType] to check
 */
fun TypedefType.checkFunctionallyEquals(other: TypedefType) {
    check(documentation.cleanedDoc() == other.documentation.cleanedDoc()) {
        "Typedef documentation mismatch at $location. Found ${documentation.cleanedDoc()} but expected ${other.documentation.cleanedDoc()}"
    }
    check(name == other.name) {
        "Typedef name mismatch at $location. Found $name but expected ${other.name}"
    }
    check(namespaces == other.namespaces) {
        "Typedef namespaces mismatch at $location. Found $namespaces but expected ${other.namespaces}"
    }
    check(annotations == other.annotations) {
        "Typedef annotations mismatch at $location. Found $annotations but expected ${other.annotations}"
    }
    oldType.checkFunctionallyEquals(other.oldType) {
        "Typedef oldType mismatch at $location. Found $oldType but expected ${other.oldType}"
    }
}

/**
 * Checks that this [Schema] is equal to a given [other] [Schema]. Note that since elements are
 * effectively sets, they're sorted by their fully qualified class names and zipped with [other] for
 * consistency and matching.
 *
 * @param other the other [Schema] to check
 */
fun Schema.checkFunctionallyEquals(other: Schema) {
    structs.sortedBy(StructType::fqcn)
        .zip(other.structs.sortedBy(StructType::fqcn))
        .forEach { (struct1, struct2) ->
            struct1.checkFunctionallyEquals(struct2)
        }
    unions.sortedBy(StructType::fqcn)
        .zip(other.unions.sortedBy(StructType::fqcn))
        .forEach { (union1, union2) ->
            union1.checkFunctionallyEquals(union2)
        }
    enums.sortedBy(EnumType::fqcn)
        .zip(other.enums.sortedBy(EnumType::fqcn))
        .forEach { (enum1, enum2) ->
            enum1.checkFunctionallyEquals(enum2)
        }
    services.sortedBy(ServiceType::fqcn)
        .zip(other.services.sortedBy(ServiceType::fqcn))
        .forEach { (service1, service2) ->
            service1.checkFunctionallyEquals(service2)
        }
    typedefs.sortedBy(TypedefType::fqcn)
        .zip(other.typedefs.sortedBy(TypedefType::fqcn))
        .forEach { (typedef1, typedef2) ->
            typedef1.checkFunctionallyEquals(typedef2)
        }
    exceptions.sortedBy(StructType::fqcn)
        .zip(other.exceptions.sortedBy(StructType::fqcn))
        .forEach { (exception1, exception2) ->
            exception1.checkFunctionallyEquals(exception2)
        }
    constants.sortedBy { "${it.javaPackage}${it.name}" }
        .zip(other.constants.sortedBy { "${it.javaPackage}${it.name}" })
        .forEach { (constant1, constant2) ->
            constant1.checkFunctionallyEquals(constant2)
        }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy