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

com.microsoft.thrifty.schema.render.SchemaRendering.kt Maven / Gradle / Ivy

There is a newer version: 3.1.0
Show newest version
/*
 * 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("SchemaRendering")

package com.microsoft.thrifty.schema.render

import com.microsoft.thrifty.schema.*
import com.microsoft.thrifty.schema.NamespaceScope.JAVA
import java.io.File

/*
 * Rendering utilities for Thrifty elements. These render Thrifty elements back to well-formatted
 * spec notation.
 */

/**
 * Renders a potentially multi-file schema as a [Set] of [ThriftSpec]s. This will resolve `include`s
 * for the files while also collecting namespaces.
 *
 * @param relativizeIncludes a flag to indicate whether or not to relativize include statements.
 * Default is `true`
 * @param namespaceResolver a lambda function to result namespaces for given [UserType]s. Default
 * is to just use its Java namespace, but can be useful to configure it to look for alternate
 * namespaces (such as when performing package name preprocessing). This parameter will likely be
 * removed in the future.
 * @param minimumPrefix an optional "minimum prefix" to require if [relativizeIncludes] is true.
 * Normally when relativizing, paths are shortened to remove their combined common prefix. This can
 * be specified to ensure that a minimumPrefix is kept for reference beyond the scope of this
 * function. Example: `minPrefix = "foo/bar"` -> common prefix with `bar/baz` will be `foo/bar/baz`.
 * @return the rendered [Set] of [ThriftSpec]s.
 */
fun Schema.multiFileRender(
    relativizeIncludes: Boolean = true,
    namespaceResolver: (UserType) -> String = { it.namespaces[JAVA]!! },
    minimumPrefix: String? = null
): Set {
    // If relativizing, deduce the common prefix of all the file paths to know the "root" of their
    // directory
    val commonPathPrefix = if (relativizeIncludes) {
        elements()
            .asSequence()
            .map(UserElement::filepath)
            .reduce { currentPrefix, nextLocation ->
                currentPrefix.commonPrefixWith(nextLocation)
            }
            .let { calculatedPrefix ->
                minimumPrefix?.let { minPrefix ->
                    check(calculatedPrefix.contains(minPrefix)) {
                        "Calculated common prefix for files doesn't contain the specified minimum prefix!\nCalculated: $calculatedPrefix\nMinimum: $minPrefix"
                    }
                    calculatedPrefix.substringBefore(minPrefix)
                } ?: calculatedPrefix
            }
            .let {
                if (it.endsWith(".thrift")) {
                    // We only have one file. Back it up to the directory name for sanity
                    it.substringBeforeLast(File.separator)
                } else it
            }
    } else ""
    return elements()
        .groupBy(UserElement::filepath)
        .mapKeys { it.key.removePrefix(commonPathPrefix) }
        .mapTo(LinkedHashSet()) { (filePath, sourceElements) ->
            val elements =
                sourceElements.filter { it.filepath.removePrefix(commonPathPrefix) == filePath }
            val namespaces = elements.filterIsInstance()
                .map(UserType::namespaces)
            check(namespaces.distinct().size == 1) {
                "Multiple namespaces! $namespaces"
            }
            val realNamespaces = namespaces.first()
            val fileSchema = toBuilder()
                .exceptions(elements.filterIsInstance().filter(StructType::isException))
                .services(elements.filterIsInstance())
                .structs(elements.filterIsInstance().filter { !it.isUnion && !it.isException })
                .typedefs(elements.filterIsInstance())
                .enums(elements.filterIsInstance())
                .unions(elements.filterIsInstance().filter(StructType::isUnion))
                .build()

            val sourceFile = File(filePath)
            val includes = elements
                .flatMap { element ->
                    when (element) {
                        is StructType -> {
                            element.fields
                                .flatMap {
                                    it.type
                                        .unpack()
                                }
                        }
                        is ServiceType -> {
                            element.methods
                                .flatMap { method ->
                                    (method.run { exceptions + parameters })
                                        .flatMap {
                                            it.type
                                                .unpack()
                                        } + method.returnType.unpack()
                                }
                        }
                        is TypedefType -> element.oldType.unpack()
                        else -> emptySet()
                    }
                }
                .filterIsInstance()
                .distinctBy(UserType::filepath)
                .filter { it.filepath.removePrefix(commonPathPrefix) != filePath }
                .map { it to it.filepath.removePrefix(commonPathPrefix) }
                .run {
                    if (relativizeIncludes) {
                        map {
                            it.first to File(it.second).toRelativeString(sourceFile)
                                .removePrefix("../")
                                .run {
                                    if (startsWith("../")) {
                                        this
                                    } else {
                                        "./$this"
                                    }
                                }
                        }
                    } else this
                }
                .map {
                    Include(
                        path = it.second,
                        namespace = namespaceResolver(it.first),
                        relative = relativizeIncludes
                    )
                }

            return@mapTo ThriftSpec(
                filePath = filePath,
                namespaces = realNamespaces,
                includes = includes,
                schema = fileSchema
            )
        }
}

/**
 * @return the rendered form of this [Schema].
 */
fun Schema.render() = renderTo(StringBuilder()).toString()

/**
 * Renders this [Schema] into a given [buffer].
 *
 * @return the [buffer] for chaining convenience.
 */
@Suppress("RemoveExplicitTypeArguments") // False positive
fun  Schema.renderTo(buffer: A) = buffer.apply {
    if (typedefs.isNotEmpty()) {
        typedefs.sortedBy(TypedefType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, typedef ->
                typedef.renderTo(buffer)
            }
    }
    if (enums.isNotEmpty()) {
        enums.sortedBy(EnumType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, enum ->
                enum.renderTo(buffer)
            }
    }
    if (structs.isNotEmpty()) {
        structs.sortedBy(StructType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, struct ->
                struct.renderTo(buffer)
            }
    }
    if (unions.isNotEmpty()) {
        unions.sortedBy(StructType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, struct ->
                struct.renderTo(buffer)
            }
    }
    if (exceptions.isNotEmpty()) {
        exceptions.sortedBy(StructType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, struct ->
                struct.renderTo(buffer)
            }
    }
    if (services.isNotEmpty()) {
        services.sortedBy(ServiceType::name)
            .joinEachTo(
                buffer = buffer,
                separator = DOUBLE_NEWLINE,
                postfix = DOUBLE_NEWLINE
            ) { _, service ->
                service.renderTo(buffer)
            }
    }

}

/**
 * @return the rendered form of this [UserElement].
 */
fun UserElement.renderElement(indent: String = "  ") =
    renderElementTo(StringBuilder(), indent).toString()

/**
 * Renders this [UserElement] into a given [buffer].
 *
 * @return the [buffer] for chaining convenience.
 */
fun  UserElement.renderElementTo(buffer: A, indent: String = "  "): A {
    @Suppress("RemoveExplicitTypeArguments") // False positive
    when (this) {
        is UserType -> renderTo(buffer)
        is Field -> renderTo(buffer, indent)
        is ServiceMethod -> renderTo(buffer, indent)
        is EnumMember -> renderTo(buffer, indent)
        is Constant -> renderTo(buffer)
        else -> throw IllegalArgumentException("Unsupported UserElement type: $this")
    }
    return buffer
}

/**
 * @return the rendered form of this [UserType].
 */
fun UserType.render(): String = renderTo(StringBuilder()).toString()

/**
 * Renders this [UserType] into a given [buffer].
 *
 * @return the [buffer] for chaining convenience.
 */
fun  UserType.renderTo(buffer: A): A {
    // Doesn't follow the usual buffer.apply function body pattern because type checking falls over
    @Suppress("RemoveExplicitTypeArguments") // False positive
    when (this) {
        is TypedefType -> renderTo(buffer)
        is EnumType -> renderTo(buffer)
        is StructType -> renderTo(buffer)
        is ServiceType -> renderTo(buffer)
        else -> throw IllegalArgumentException("Unrecognized UserType: $this")
    }
    return buffer
}

private fun  TypedefType.renderTo(buffer: A) = buffer.apply {
    renderJavadocTo(buffer)
    append("typedef ")
    oldType.renderTypeTo(buffer, location)
    oldType.annotations
        .renderTo(buffer)
    append(" ", name)
    renderAnnotationsTo(buffer, indent = " ")
}

private fun  StructType.renderTo(buffer: A) = buffer.apply {
    renderJavadocTo(buffer)
    val type = when {
        isException -> "exception"
        isUnion -> "union"
        else -> "struct"
    }
    append(type, " ", name, " {")
    appendln()
    fields
        .joinEachTo(buffer, NEWLINE) { _, field ->
            field.renderElementTo(buffer)
        }
    appendln()
    append("}")
    renderAnnotationsTo(buffer)
}

private fun  EnumType.renderTo(buffer: A) = buffer.apply {
    renderJavadocTo(buffer)
    append("enum ", name, " {")
    appendln()
    members.joinEachTo(buffer, ",$NEWLINE") { _, member ->
        member.renderElementTo(buffer)
    }
    appendln()
    append("")
    append("}")
    renderAnnotationsTo(buffer)
}

private fun  ServiceType.renderTo(buffer: A) = buffer.apply {
    renderJavadocTo(buffer)
    append("service ", name, " {")
    appendln()
    methods.joinEachTo(buffer = buffer, separator = DOUBLE_NEWLINE) { _, method ->
        method.renderElementTo(buffer)
    }
    appendln()
    append("}")
    renderAnnotationsTo(buffer)
}

private fun  Field.renderTo(buffer: A, indent: String = "  ") = buffer.apply {
    renderJavadocTo(buffer, indent)
    append(indent, id.toString(), ":", requiredness, " ")
    type.renderTypeTo(buffer, location)
    if (type !is UserType) type.annotations.renderTo(buffer)
    append(" ", name)
    renderAnnotationsTo(buffer, indent)
}

private fun  ServiceMethod.renderTo(buffer: A, indent: String = "  ") =
    buffer.apply {
        renderJavadocTo(buffer, indent)
        append(indent)
        returnType.renderTypeTo(buffer, location)
        append(" ", name)
        if (parameters.isEmpty()) {
            append("()")
        } else {
            parameters
                .joinEachTo(
                    buffer = buffer,
                    separator = ",$NEWLINE",
                    prefix = "($NEWLINE",
                    postfix = "$NEWLINE$indent)"
                ) { _, param ->
                    param.renderTo(buffer, "$indent  ")
                }
        }
        if (exceptions.isNotEmpty()) {
            appendln(" throws (")
            exceptions
                .joinEachTo(buffer = buffer, separator = ",$NEWLINE") { _, param ->
                    param.renderTo(buffer, "$indent  ")
                }
            appendln()
            append(indent, ")")
        }
        renderAnnotationsTo(buffer, indent)
    }

private fun  EnumMember.renderTo(buffer: A, indent: String = "  ") = buffer.apply {
    renderJavadocTo(buffer, indent)
    append(indent, name, " = ", value.toString())
    renderAnnotationsTo(buffer)
}

private fun  Constant.renderTo(buffer: A) = buffer.apply {
    renderJavadocTo(buffer)
    append("const ")
    type.renderTypeTo(buffer, location)
    append(" ")
    type.annotations
        .renderTo(buffer)
    append(" ", name, " = ", value.thriftText)
    renderAnnotationsTo(buffer)
}

/**
 * Renders a thrift type by its name, possibly prefixing with the program name if not the same
 * location as [source].
 */
private fun  ThriftType.renderTypeTo(buffer: A, source: Location): A {
    // Doesn't follow the usual buffer.apply function body pattern because type checking falls over
    when {
        this is UserType && source.filepath != location.filepath -> {
            buffer.apply {
                append(location.programName)
                append(".")
                append(name)
            }
        }
        this is SetType -> {
            buffer.apply {
                append("set<")
                elementType.renderTypeTo(buffer, source)
                append(">")
            }
        }
        this is ListType -> {
            buffer.apply {
                append("list<")
                elementType.renderTypeTo(buffer, source)
                append(">")
            }
        }
        this is MapType -> {
            buffer.apply {
                append("map<")
                keyType.renderTypeTo(buffer, source)
                append(",")
                valueType.renderTypeTo(buffer, source)
                append(">")
            }
        }
        else -> buffer.append(name)
    }
    return buffer
}

private fun  UserElement.renderJavadocTo(buffer: A, indent: String = "") =
    buffer.apply {
        if (hasJavadoc) {
            val docLines = documentation.trim()
                .trim(Character::isSpaceChar)
                .lines()
            val isSingleLine = docLines.size == 1
            if (isSingleLine) {
                append(indent)
                append("/* ")
                append(docLines[0])
                appendln(" */")
            } else {
                docLines.joinTo(
                    buffer = buffer,
                    separator = NEWLINE,
                    prefix = "$indent/**$NEWLINE",
                    postfix = "$NEWLINE$indent */$NEWLINE"
                ) {
                    "$indent * $it"
                }
            }
        }
    }

private fun  UserElement.renderAnnotationsTo(
    buffer: A,
    indent: String = "",
    prefix: String = " "
) = buffer.apply {
    annotations.renderTo(buffer, indent, prefix)
}

private fun  Map.renderTo(
    buffer: A,
    indent: String = "",
    prefix: String = " "
) = buffer.apply {
    when {
        size == 1 -> {
            append(prefix)
            append("(")
            val (key, value) = entries.first()
            append(key)
            append(" = ")
            append("\"")
            append(value.replace("\"", "\\\""))
            append("\"")
            append(")")
        }
        size > 1 -> {
            append(prefix)
            appendln("(")
            entries
                .sortedBy(Map.Entry::key)
                .joinTo(buffer = buffer, separator = ",$NEWLINE") { (key, value) ->
                    "$indent  $key = \"${value.replace("\"", "\\\"")}\""
                }
            appendln()
            append(indent, ")")
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy