
software.amazon.smithy.kotlin.codegen.rendering.serde.SerializeStructGenerator.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.serde
import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.kotlin.codegen.DefaultValueSerializationMode
import software.amazon.smithy.kotlin.codegen.core.*
import software.amazon.smithy.kotlin.codegen.model.*
import software.amazon.smithy.kotlin.codegen.model.targetOrSelf
import software.amazon.smithy.kotlin.codegen.rendering.protocol.ProtocolGenerator
import software.amazon.smithy.model.shapes.*
import software.amazon.smithy.model.traits.*
/**
* Generate serialization for members bound to the payload.
*
* NOTE: If the serialization order is important then [members] MUST already be sorted correctly
*
* There are some proper names Smithy models use which can lead to confusion in codegen.
* In this file, `member` refers to the lhs of a child of a structure (known as a member). It can
* be thought of as the root node from which serialization of a field begins.
*
* `target` refers to the rhs of a child in a structure, which is a reference to another Smithy type.
*
* The element of a list (which is also known as a `member` in Smithy) is referred to as an element.
*
* Example output this class generates:
* ```
* serializer.serializeStruct(OBJ_DESCRIPTOR) {
* input.field1?.let { field(FIELD1_DESCRIPTOR, it) }
* input.field2?.let { field(FIELD2_DESCRIPTOR, it) }
* }
* ```
*
* This class is open to extension for variations of member serialization; specifically Unions.
*/
open class SerializeStructGenerator(
// FIXME - refactor to just take a CodegenContext rather than the more specific protocol generator context. Serde should be protocol agnostic (ideally)
protected val ctx: ProtocolGenerator.GenerationContext,
protected val members: List,
protected val writer: KotlinWriter,
protected val defaultTimestampFormat: TimestampFormatTrait.Format,
) {
/**
* Container for serialization information for a particular shape being serialized to
*/
fun interface SerializeFunction {
/**
* Return the formatted function invocation to serialize the given [member]
* e.g.
* ```
* field(X_DESCRIPTOR, input.x)
* ```
* @param member the member shape being serialized
* @param identifier the identifier name to render the invocation with
*/
fun format(member: MemberShape, identifier: String): String
}
/**
* Returns the name to put in codegen to refer to the parent collection type.
*/
open fun parentName(defaultName: String) = defaultName
/**
* Returns the name passed to the constructor of a nested serializer.
*/
open fun valueToSerializeName(defaultName: String): String = defaultName
/**
* Iterate over all supplied [MemberShape]s to generate serializers.
*/
open fun render() {
// inline an empty object descriptor when the struct has no members
// otherwise use the one generated as part of the companion object
val objDescriptor = if (members.isNotEmpty()) {
"OBJ_DESCRIPTOR"
} else {
writer.addImport(RuntimeTypes.Serde.SdkObjectDescriptor)
"SdkObjectDescriptor.build{}"
}
writer.withBlock("serializer.#T($objDescriptor) {", "}", RuntimeTypes.Serde.serializeStruct) {
members.forEach { memberShape ->
renderMemberShape(memberShape)
}
}
}
/**
* Call appropriate serialization function based on target type of member shape.
*/
protected fun renderMemberShape(memberShape: MemberShape) {
val targetShape = ctx.model.expectShape(memberShape.target)
when (targetShape.type) {
ShapeType.LIST,
ShapeType.SET,
-> renderListMemberSerializer(memberShape, targetShape as CollectionShape)
ShapeType.MAP -> renderMapMemberSerializer(memberShape, targetShape as MapShape)
ShapeType.STRUCTURE,
ShapeType.UNION,
-> renderShapeSerializer(memberShape, serializerForStructureShape)
ShapeType.BLOB,
ShapeType.BOOLEAN,
ShapeType.STRING,
ShapeType.BYTE,
ShapeType.SHORT,
ShapeType.INTEGER,
ShapeType.LONG,
ShapeType.FLOAT,
ShapeType.DOUBLE,
ShapeType.BIG_INTEGER,
ShapeType.BIG_DECIMAL,
ShapeType.TIMESTAMP,
ShapeType.DOCUMENT,
ShapeType.ENUM,
ShapeType.INT_ENUM,
-> renderShapeSerializer(memberShape, serializerForSimpleShape)
else -> error("Unexpected shape type: ${targetShape.type}")
}
}
/**
* Produces serialization for a map member. Example:
* ```
* if (input.payload != null) {
* mapField(PAYLOAD_DESCRIPTOR) {
* ...
* }
* }
* ```
*/
open fun renderMapMemberSerializer(memberShape: MemberShape, targetShape: MapShape) {
val memberName = ctx.symbolProvider.toMemberName(memberShape)
val descriptorName = memberShape.descriptorName()
val nestingLevel = 0
val memberSymbol = ctx.symbolProvider.toSymbol(memberShape)
writer.wrapBlockIf(memberSymbol.isNullable, "if (input.$memberName != null) {", "}") {
writer.withBlock("mapField($descriptorName) {", "}") {
delegateMapSerialization(memberShape, targetShape, nestingLevel, memberName)
}
}
}
/**
* Produces serialization for a list member. Example:
* ```
* if (input.intList != null) {
* listField(INTLIST_DESCRIPTOR) {
* ...
* }
* }
* ```
*/
open fun renderListMemberSerializer(memberShape: MemberShape, targetShape: CollectionShape) {
val memberName = ctx.symbolProvider.toMemberName(memberShape)
val descriptorName = memberShape.descriptorName()
val nestingLevel = 0
val memberSymbol = ctx.symbolProvider.toSymbol(memberShape)
writer.wrapBlockIf(memberSymbol.isNullable, "if (input.$memberName != null) {", "}") {
writer.withBlock("listField($descriptorName) {", "}") {
delegateListSerialization(memberShape, targetShape, nestingLevel, memberName)
}
}
}
/**
* Delegates to other functions based on the type of value target of map.
*/
protected fun delegateMapSerialization(rootMemberShape: MemberShape, mapShape: MapShape, nestingLevel: Int, parentMemberName: String) {
val elementShape = ctx.model.expectShape(mapShape.value.target)
val isSparse = mapShape.isSparse
when (elementShape.type) {
ShapeType.BOOLEAN,
ShapeType.STRING,
ShapeType.BYTE,
ShapeType.SHORT,
ShapeType.INTEGER,
ShapeType.LONG,
ShapeType.FLOAT,
ShapeType.DOUBLE,
ShapeType.BIG_DECIMAL,
ShapeType.DOCUMENT,
ShapeType.BIG_INTEGER,
ShapeType.ENUM,
ShapeType.INT_ENUM,
-> renderPrimitiveEntry(elementShape, nestingLevel, parentMemberName)
ShapeType.BLOB -> renderBlobEntry(nestingLevel, parentMemberName)
ShapeType.TIMESTAMP -> renderTimestampEntry(mapShape.value, elementShape, nestingLevel, parentMemberName)
ShapeType.SET,
ShapeType.LIST,
-> renderListEntry(rootMemberShape, elementShape as CollectionShape, nestingLevel, isSparse, parentMemberName)
ShapeType.MAP -> renderMapEntry(rootMemberShape, elementShape as MapShape, nestingLevel, isSparse, parentMemberName)
ShapeType.UNION,
ShapeType.STRUCTURE,
-> renderNestedStructureEntry(elementShape, nestingLevel, parentMemberName, isSparse)
else -> error("Unhandled type ${elementShape.type}")
}
}
/**
* Delegates to other functions based on the type of element.
*/
protected fun delegateListSerialization(rootMemberShape: MemberShape, listShape: CollectionShape, nestingLevel: Int, parentMemberName: String) {
val elementShape = ctx.model.expectShape(listShape.member.target)
val isSparse = listShape.isSparse
when (elementShape.type) {
ShapeType.BOOLEAN,
ShapeType.STRING,
ShapeType.BYTE,
ShapeType.SHORT,
ShapeType.INTEGER,
ShapeType.LONG,
ShapeType.FLOAT,
ShapeType.DOUBLE,
ShapeType.BIG_DECIMAL,
ShapeType.DOCUMENT,
ShapeType.BIG_INTEGER,
ShapeType.ENUM,
ShapeType.INT_ENUM,
-> renderPrimitiveElement(elementShape, nestingLevel, parentMemberName, isSparse)
ShapeType.BLOB -> renderBlobElement(nestingLevel, parentMemberName)
ShapeType.TIMESTAMP -> renderTimestampElement(listShape.member, elementShape, nestingLevel, parentMemberName)
ShapeType.LIST,
ShapeType.SET,
-> renderListElement(rootMemberShape, elementShape as CollectionShape, nestingLevel, parentMemberName)
ShapeType.MAP -> renderMapElement(rootMemberShape, elementShape as MapShape, nestingLevel, parentMemberName)
ShapeType.UNION,
ShapeType.STRUCTURE,
-> renderNestedStructureElement(elementShape, nestingLevel, parentMemberName)
else -> error("Unhandled type ${elementShape.type}")
}
}
/**
* Renders the serialization of a nested Structure. Example:
*
* ```
* for(m0 in input.structList) {
* serializeSdkSerializable(NestedSerializer(m0))
* }
* ```
*/
private fun renderNestedStructureElement(structureShape: Shape, nestingLevel: Int, parentMemberName: String) {
val serializerFnName = structureShape.type.primitiveSerializerFunctionName()
val serializerTypeName = ctx.symbolProvider.toSymbol(structureShape).documentSerializerName()
val elementName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val containerName = if (nestingLevel == 0) "input." else ""
val valueToSerializeName = valueToSerializeName(elementName)
writer.withBlock("for ($elementName in $containerName$parentMemberName) {", "}") {
writer.write("$serializerFnName(#T($valueToSerializeName, ::$serializerTypeName))", RuntimeTypes.Serde.asSdkSerializable)
}
}
/**
* Renders a nested structure contained in a map. Example:
*
* ```
* input.payload.forEach { (key, value) -> entry(key, FooStructureSerializer(value)) }
* ```
*/
private fun renderNestedStructureEntry(
structureShape: Shape,
nestingLevel: Int,
parentMemberName: String,
isSparse: Boolean,
) {
val serializerTypeName = ctx.symbolProvider.toSymbol(structureShape).documentSerializerName()
val (keyName, valueName) = keyValueNames(nestingLevel)
val containerName = if (nestingLevel == 0) "input." else ""
val value = "asSdkSerializable($valueName, ::$serializerTypeName)"
when (isSparse) {
true -> writer.write("$containerName$parentMemberName.forEach { ($keyName, $valueName) -> if ($valueName != null) entry($keyName, $value) else entry($keyName, null as String?) }")
false -> writer.write("$containerName$parentMemberName.forEach { ($keyName, $valueName) -> entry($keyName, $value) }")
}
}
/**
* Renders the serialization of a list element of type map.
*
* Example:
* ```
* for (c0 in input.payload) {
* serializer.serializeMap(PAYLOAD_C0_DESCRIPTOR) {
* ...
* }
* }
* ```
*/
open fun renderMapElement(
rootMemberShape: MemberShape,
mapShape: MapShape,
nestingLevel: Int,
parentMemberName: String,
) {
val descriptorName = rootMemberShape.descriptorName(nestingLevel.nestedDescriptorName())
val elementName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val containerName = if (nestingLevel == 0) "input." else ""
val parentName = parentName(elementName)
writer.withBlock("for ($elementName in $containerName$parentMemberName) {", "}") {
writer.withBlock("serializer.#T($descriptorName) {", "}", RuntimeTypes.Serde.serializeMap) {
delegateMapSerialization(rootMemberShape, mapShape, nestingLevel + 1, parentName)
}
}
}
/**
* Render the serialization of a map entry. Example:
* ```
* input.payload.forEach { (key, value) -> mapEntry(key, PAYLOAD_M0_DESCRIPTOR) {
* if (key != null) {
* mapField(SOME_DESCRIPTOR) {
* ...
* }
* }
* }
* ```
*/
private fun renderMapEntry(
rootMemberShape: MemberShape,
mapShape: MapShape,
nestingLevel: Int,
isSparse: Boolean,
parentMemberName: String,
) {
val descriptorName = rootMemberShape.descriptorName(nestingLevel.nestedDescriptorName())
val containerName = if (nestingLevel == 0) "input." else ""
val (keyName, valueName) = keyValueNames(nestingLevel)
val parentName = parentName(valueName)
writer.withBlock("$containerName$parentMemberName.forEach { ($keyName, $valueName) ->", "}") {
writer.wrapBlockIf(isSparse, "if ($valueName != null) {", "} else entry($keyName, null as String?)") {
writer.withBlock("mapEntry($keyName, $descriptorName) {", "}") {
delegateMapSerialization(rootMemberShape, mapShape, nestingLevel + 1, parentName)
}
}
}
}
/**
* Renders a map value of type list. Example:
*
* ```
* input.payload.forEach { (key, value) -> listEntry(key, PAYLOAD_C0_DESCRIPTOR) {
* ...
* }}
* ```
*/
private fun renderListEntry(
rootMemberShape: MemberShape,
elementShape: CollectionShape,
nestingLevel: Int,
isSparse: Boolean,
parentMemberName: String,
) {
val descriptorName = rootMemberShape.descriptorName(nestingLevel.nestedDescriptorName())
val containerName = if (nestingLevel == 0) "input." else ""
val (keyName, valueName) = keyValueNames(nestingLevel)
val parentName = parentName(valueName)
writer.withBlock("$containerName$parentMemberName.forEach { ($keyName, $valueName) ->", "}") {
writer.wrapBlockIf(isSparse, "if ($valueName != null) {", "} else entry($keyName, null as String?)") {
writer.withBlock("listEntry($keyName, $descriptorName) {", "}") {
delegateListSerialization(rootMemberShape, elementShape, nestingLevel + 1, parentName)
}
}
}
}
/**
* Render a List element of type List
*
* Example:
*
* ```
* for (m0 in input.payload) {
* serializer.serializeList(PAYLOAD_M0_DESCRIPTOR) {
* ...
* }
* }
*/
private fun renderListElement(rootMemberShape: MemberShape, elementShape: CollectionShape, nestingLevel: Int, parentListMemberName: String) {
val descriptorName = rootMemberShape.descriptorName(nestingLevel.nestedDescriptorName())
val elementName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val containerName = if (nestingLevel == 0) "input." else ""
writer.withBlock("for ($elementName in $containerName$parentListMemberName) {", "}") {
writer.withBlock("serializer.#T($descriptorName) {", "}", RuntimeTypes.Serde.serializeList) {
delegateListSerialization(rootMemberShape, elementShape, nestingLevel + 1, elementName)
}
}
}
/**
* Renders the serialization of a primitive value contained by a map. Example:
*
* ```
* c0.forEach { (key1, value1) -> entry(key1, value1) }
* ```
*/
private fun renderPrimitiveEntry(elementShape: Shape, nestingLevel: Int, listMemberName: String) {
val containerName = if (nestingLevel == 0) "input." else ""
val enumPostfix = if (elementShape.isEnum) ".value" else ""
val (keyName, valueName) = keyValueNames(nestingLevel)
writer.write("$containerName$listMemberName.forEach { ($keyName, $valueName) -> entry($keyName, $valueName$enumPostfix) }")
}
/**
* Renders the serialization of a blob value contained by a map. Example:
*
* ```
* input.fooBlobMap.forEach { (key, value) -> entry(key, value.encodeBase64String()) }
* ```
*/
private fun renderBlobEntry(nestingLevel: Int, listMemberName: String) {
val containerName = if (nestingLevel == 0) "input." else ""
val (keyName, valueName) = keyValueNames(nestingLevel)
writer.write(
"$containerName$listMemberName.forEach { ($keyName, $valueName) -> entry($keyName, $valueName.#T()) }",
RuntimeTypes.Core.Text.Encoding.encodeBase64String,
)
}
/**
* Renders the serialization of a timestamp value contained by a map. Example:
*
* ```
* input.fooTimestampMap.forEach { (key, value) -> entry(key, it, TimestampFormat.EPOCH_SECONDS) }
* ```
*/
private fun renderTimestampEntry(memberShape: Shape, elementShape: Shape, nestingLevel: Int, listMemberName: String) {
writer.addImport(RuntimeTypes.Core.TimestampFormat)
// favor the member shape if it overrides the value shape trait
val shape = if (memberShape.hasTrait()) {
memberShape
} else {
elementShape
}
val tsFormat = shape
.getTrait(TimestampFormatTrait::class.java)
.map { it.format }
.orElse(defaultTimestampFormat)
.toRuntimeEnum()
val (keyName, valueName) = keyValueNames(nestingLevel)
val containerName = if (nestingLevel == 0) "input." else ""
writer.write("$containerName$listMemberName.forEach { ($keyName, $valueName) -> entry($keyName, it, $tsFormat) }")
}
/**
* Render a List element of a primitive type
*
* Example:
* ```
* for (m0 in input.payload) {
* serializeInt(m0)
* }
* ```
*/
private fun renderPrimitiveElement(
elementShape: Shape,
nestingLevel: Int,
listMemberName: String,
isSparse: Boolean,
) {
val serializerFnName = elementShape.type.primitiveSerializerFunctionName()
val iteratorName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val elementName = when (elementShape.isEnum) {
true -> "$iteratorName.value"
false -> iteratorName
}
val containerName = if (nestingLevel == 0) "input." else ""
writer.withBlock("for ($iteratorName in $containerName$listMemberName) {", "}") {
when (isSparse) {
true -> writer.write("if ($elementName != null) $serializerFnName($elementName) else serializeNull()")
false -> writer.write("$serializerFnName($elementName)")
}
}
}
/**
* Render a blob element of a list. Example:
*
* ```
* for (c0 in input.fooBlobList) {
* serializeString(c0.encodeBase64String())
* }
*/
private fun renderBlobElement(nestingLevel: Int, listMemberName: String) {
val elementName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val containerName = if (nestingLevel == 0) "input." else ""
writer.withBlock("for ($elementName in $containerName$listMemberName) {", "}") {
writer.write("serializeString($elementName.#T())", RuntimeTypes.Core.Text.Encoding.encodeBase64String)
}
}
/**
* Render a timestamp value of a list. Example:
*
* ```
* for (c0 in input.payload) {
* serializeInstant(c0, TimestampFormat.EPOCH_SECONDS)
* }
*/
private fun renderTimestampElement(memberShape: Shape, elementShape: Shape, nestingLevel: Int, listMemberName: String) {
// :test(timestamp, member > timestamp)
writer.addImport(RuntimeTypes.Core.TimestampFormat)
// favor the member shape if it overrides the value shape trait
val shape = if (memberShape.hasTrait()) {
memberShape
} else {
elementShape
}
val tsFormat = shape
.getTrait(TimestampFormatTrait::class.java)
.map { it.format }
.orElse(defaultTimestampFormat)
.toRuntimeEnum()
val elementName = nestingLevel.variableNameFor(NestedIdentifierType.ELEMENT)
val containerName = if (nestingLevel == 0) "input." else ""
writer.withBlock("for ($elementName in $containerName$listMemberName) {", "}") {
writer.write("serializeInstant($elementName, $tsFormat)")
}
}
/**
* Render code to serialize a simple shape or structure shape. Example:
*
* ```
* input.payload?.let { field(PAYLOAD_DESCRIPTOR, it) }
* ```
*
* @param memberShape [MemberShape] member shape to render serializer for
* @param serializerFn [SerializeFunction] the serializer responsible for returning the function to invoke
*/
open fun renderShapeSerializer(memberShape: MemberShape, serializerFn: SerializeFunction) {
val postfix = idempotencyTokenPostfix(memberShape)
val memberSymbol = ctx.symbolProvider.toSymbol(memberShape)
val memberName = ctx.symbolProvider.toMemberName(memberShape)
if (memberSymbol.isNullable) {
val identifier = valueToSerializeName("it")
val fn = serializerFn.format(memberShape, identifier)
writer.write("input.$memberName?.let { $fn }$postfix")
} else {
// always serialize required members, otherwise check if it's a primitive type set to it's default before serializing
val defaultValue = memberSymbol.defaultValue()
val checkDefaults = ctx.settings.api.defaultValueSerializationMode == DefaultValueSerializationMode.WHEN_DIFFERENT
val defaultCheck = if (checkDefaults && !memberShape.isRequired && memberSymbol.isNotNullable && defaultValue != null) {
"if (input.$memberName != $defaultValue) "
} else {
""
}
val fn = serializerFn.format(memberShape, "input.$memberName")
writer.write("$defaultCheck$fn$postfix")
}
}
/**
* Return string to postfix to serializer for idempotency generation
* @param memberShape shape which would have the IdempotencyTokenTrait
* @return string intended for codegen output
*/
private fun idempotencyTokenPostfix(memberShape: MemberShape): String =
if (memberShape.hasTrait()) {
writer.addImport(RuntimeTypes.SmithyClient.IdempotencyTokenProviderExt)
" ?: field(${memberShape.descriptorName()}, context.idempotencyTokenProvider.generateToken())"
} else {
""
}
/**
* Return the serializer function for a Structure or Union
*/
private val serializerForStructureShape: SerializeFunction =
SerializeFunction { member, identifier ->
// target shape type to deserialize is either the shape itself or member.target
val target = member.targetOrSelf(ctx.model)
// the Smithy type hierarchy is private such that tighter type handling at the function level isn't possible
require(target.type == ShapeType.STRUCTURE || target.type == ShapeType.UNION) { "Unexpected serializer for member: $member; target: $target" }
val symbol = ctx.symbolProvider.toSymbol(target)
val memberSerializerName = symbol.documentSerializerName()
val descriptor = member.descriptorName()
// invoke the ctor of the serializer to delegate to and pass the value
"field($descriptor, $identifier, ::$memberSerializerName)"
}
/**
* Get the serialization function and encoded value for the given [Shape], this only handles
* [simple types](https://smithy.io/2.0/spec/simple-types.html), collections should be handled separately.
*/
private val serializerForSimpleShape = SerializeFunction { member, identifier ->
// target shape type to deserialize is either the shape itself or member.target
val target = member.targetOrSelf(ctx.model)
val encoded = when {
target.type == ShapeType.BLOB -> writer.format("#L.#T()", identifier, RuntimeTypes.Core.Text.Encoding.encodeBase64String)
target.type == ShapeType.TIMESTAMP -> {
writer.addImport(RuntimeTypes.Core.TimestampFormat)
val tsFormat = member
.getTrait(TimestampFormatTrait::class.java)
.map { it.format }
.orElseGet {
target.getTrait(TimestampFormatTrait::class.java)
.map { it.format }
.orElse(defaultTimestampFormat)
}
.toRuntimeEnum()
"$identifier, $tsFormat"
}
target.isEnum -> "$identifier.value"
else -> identifier
}
val descriptor = member.descriptorName()
"field($descriptor, $encoded)"
}
/**
* Generate key and value names for iteration based on nesting level
* @param nestingLevel current level of nesting
* @return key and value as a pair of strings
*/
private fun keyValueNames(nestingLevel: Int): Pair {
val keyName = if (nestingLevel == 0) "key" else "key$nestingLevel"
val valueName = if (nestingLevel == 0) "value" else "value$nestingLevel"
return keyName to valueName
}
/**
* Get the name of the `PrimitiveSerializer` function name for the corresponding shape type
* @throws CodegenException when no known function name for the given type is known to exist
*/
private fun ShapeType.primitiveSerializerFunctionName(): String {
val suffix = when (this) {
ShapeType.BOOLEAN -> "Boolean"
ShapeType.STRING, ShapeType.ENUM -> "String"
ShapeType.BYTE -> "Byte"
ShapeType.SHORT -> "Short"
ShapeType.INTEGER, ShapeType.INT_ENUM -> "Int"
ShapeType.LONG -> "Long"
ShapeType.FLOAT -> "Float"
ShapeType.DOUBLE -> "Double"
ShapeType.DOCUMENT -> "Document"
ShapeType.STRUCTURE, ShapeType.UNION -> "SdkSerializable"
else -> throw CodegenException("$this has no primitive serialize function on the Serializer interface")
}
return "serialize$suffix"
}
}
fun TimestampFormatTrait.Format.toRuntimeEnum(): String = when (this) {
TimestampFormatTrait.Format.EPOCH_SECONDS -> "TimestampFormat.EPOCH_SECONDS"
TimestampFormatTrait.Format.DATE_TIME -> "TimestampFormat.ISO_8601"
TimestampFormatTrait.Format.HTTP_DATE -> "TimestampFormat.RFC_5322"
else -> throw CodegenException("unknown timestamp format: $this")
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy