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

software.amazon.smithy.kotlin.codegen.rendering.protocol.HttpStringValuesMapSerializer.kt Maven / Gradle / Ivy

/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0.
 */

package software.amazon.smithy.kotlin.codegen.rendering.protocol

import software.amazon.smithy.codegen.core.SymbolProvider
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.model.*
import software.amazon.smithy.kotlin.codegen.rendering.serde.formatInstant
import software.amazon.smithy.model.Model
import software.amazon.smithy.model.knowledge.HttpBinding
import software.amazon.smithy.model.shapes.*
import software.amazon.smithy.model.traits.*

/**
 * Shared implementation to generate serialization for members bound to HTTP query parameters or headers
 * (both of which are implemented using `StringValuesMap`).
 *
 * This is a partial generator, the entry point for rendering from this component is an open block where the current
 * value of `this` is a `StringValuesMapBuilder`.
 *
 * Example output this class generates:
 * ```
 * if (input.field1 != null) append("X-Foo", input.field1)
 * if (input.field2?.isNotEmpty() == true) appendAll("X-Foo", input.field2!!.map { it.value })
 * ```
 */
class HttpStringValuesMapSerializer(
    private val model: Model,
    private val symbolProvider: SymbolProvider,
    private val bindings: List,
    private val resolver: HttpBindingResolver,
    private val defaultTimestampFormat: TimestampFormatTrait.Format,
) {
    constructor(
        ctx: ProtocolGenerator.GenerationContext,
        bindings: List,
        resolver: HttpBindingResolver,
        defaultTimestampFormat: TimestampFormatTrait.Format,
    ) : this(ctx.model, ctx.symbolProvider, bindings, resolver, defaultTimestampFormat)

    fun render(
        writer: KotlinWriter
    ) {
        bindings.sortedBy(HttpBindingDescriptor::memberName).forEach {
            val memberName = symbolProvider.toMemberName(it.member)
            val memberTarget = model.expectShape(it.member.target)
            val paramName = it.locationName
            val location = it.location
            val member = it.member
            when (memberTarget) {
                is CollectionShape -> renderCollectionShape(it, memberTarget, writer)
                is TimestampShape -> {
                    val tsFormat = resolver.determineTimestampFormat(member, location, defaultTimestampFormat)
                    // headers/query params need to be a string
                    val formatted = formatInstant("input.$memberName", tsFormat, forceString = true)
                    writer.write("if (input.#1L != null) append(\"#2L\", #3L)", memberName, paramName, formatted)
                    writer.addImport(RuntimeTypes.Core.TimestampFormat)
                }
                is BlobShape -> {
                    writer.addImport("encodeBase64String", KotlinDependency.UTILS)
                    writer.write(
                        "if (input.#1L?.isNotEmpty() == true) append(\"#2L\", input.#1L.encodeBase64String())",
                        memberName,
                        paramName
                    )
                }
                is StringShape -> renderStringShape(it, memberTarget, writer)
                else -> {
                    // encode to string
                    val encodedValue = "\"\${input.$memberName}\""

                    val targetSymbol = symbolProvider.toSymbol(member)
                    val defaultValue = targetSymbol.defaultValue()
                    if ((memberTarget.isNumberShape || memberTarget.isBooleanShape) && targetSymbol.isNotBoxed && defaultValue != null) {
                        // unboxed primitive with a default value
                        if (member.hasTrait()) {
                            // always serialize a required member even if it's the default
                            writer.write("append(#S, #L)", paramName, encodedValue)
                        } else {
                            writer.write("if (input.#1L != $defaultValue) append(#2S, #3L)", memberName, paramName, encodedValue)
                        }
                    } else {
                        writer.write("if (input.#1L != null) append(#2S, #3L)", memberName, paramName, encodedValue)
                    }
                }
            }
        }
    }

    private fun renderCollectionShape(binding: HttpBindingDescriptor, memberTarget: CollectionShape, writer: KotlinWriter) {
        val collectionMemberTarget = model.expectShape(memberTarget.member.target)
        val mapFnContents = when (collectionMemberTarget.type) {
            ShapeType.TIMESTAMP -> {
                // special case of timestamp list
                val tsFormat = resolver.determineTimestampFormat(binding.member, binding.location, defaultTimestampFormat)
                writer.addImport(RuntimeTypes.Core.TimestampFormat)
                // headers/query params need to be a string
                formatInstant("it", tsFormat, forceString = true)
            }
            ShapeType.STRING -> {
                if (collectionMemberTarget.isEnum) {
                    // collections of enums should be mapped to the raw values
                    "it.value"
                } else {
                    // collections of string doesn't need mapped to anything
                    ""
                }
            }
            // default to "toString"
            else -> "\"\$it\""
        }

        val memberName = symbolProvider.toMemberName(binding.member)
        val paramName = binding.locationName
        // appendAll collection parameter 2
        val param2 = if (mapFnContents.isEmpty()) "input.$memberName" else "input.$memberName.map { $mapFnContents }"
        writer.write(
            "if (input.#1L?.isNotEmpty() == true) appendAll(\"#2L\", #3L)",
            memberName,
            paramName,
            param2
        )
    }

    private fun renderStringShape(binding: HttpBindingDescriptor, memberTarget: StringShape, writer: KotlinWriter) {
        val memberName = symbolProvider.toMemberName(binding.member)
        val location = binding.location
        val paramName = binding.locationName

        // NOTE: query parameters are allowed to be empty, whereas headers should omit empty string
        // values from serde
        if ((location == HttpBinding.Location.QUERY || location == HttpBinding.Location.HEADER) && binding.member.hasTrait()) {
            // Call the idempotency token function if no supplied value.
            writer.addImport(RuntimeTypes.Core.IdempotencyTokenProviderExt)
            writer.write("append(\"#L\", (input.$memberName ?: context.idempotencyTokenProvider.generateToken()))", paramName)
        } else {
            val cond =
                if (location == HttpBinding.Location.QUERY || memberTarget.hasTrait()) {
                    "input.$memberName != null"
                } else {
                    "input.$memberName?.isNotEmpty() == true"
                }

            val suffix = when {
                memberTarget.hasTrait() -> {
                    ".value"
                }
                memberTarget.hasTrait() -> {
                    writer.addImport("encodeBase64", KotlinDependency.UTILS)
                    ".encodeBase64()"
                }
                else -> ""
            }

            writer.write("if (#1L) append(\"#2L\", #3L)", cond, paramName, "input.${memberName}$suffix")
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy