Skip to content

Commit

Permalink
DDB mapper filter expressions codegen
Browse files Browse the repository at this point in the history
  • Loading branch information
ianbotsf committed Sep 29, 2024
1 parent 4240741 commit 7a1d42d
Show file tree
Hide file tree
Showing 40 changed files with 1,719 additions and 191 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public object MapperPkg {
public val CollectionValues: String = "$Values.collections"
public val ScalarValues: String = "$Values.scalars"
public val SmithyTypeValues: String = "$Values.smithytypes"

@InternalSdkApi
public object Expressions {
public val Base: String = "${Hl.Base}.expressions"
public val Internal: String = "$Base.internal"
}
}

@InternalSdkApi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ public object MapperTypes {
public val ManualPagination: TypeRef = TypeRef(MapperPkg.Hl.Annotations, "ManualPagination")
}

public object Expressions {
public val BooleanExpr: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "BooleanExpr")
public val Filter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "Filter")
public val KeyFilter: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Base, "KeyFilter")

public object Internal {
public val FilterImpl: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "FilterImpl")
public val ParameterizingExpressionVisitor: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "ParameterizingExpressionVisitor")
public val toExpression: TypeRef = TypeRef(MapperPkg.Hl.Expressions.Internal, "toExpression")
}
}

public object Items {
public fun itemSchema(typeVar: String): TypeRef =
TypeRef(MapperPkg.Hl.Items, "ItemSchema", genericArgs = listOf(TypeVar(typeVar)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,12 @@
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.Operation
import aws.sdk.kotlin.hll.codegen.model.Structure
import aws.smithy.kotlin.runtime.collections.AttributeKey

/**
* Defines [AttributeKey] instances that relate to the data model of low-level to high-level codegen
*/
internal object ModelAttributes {
/**
* For a given high-level [Operation], this attribute key identifies the associated low-level [Operation]
*/
val LowLevelOperation: AttributeKey<Operation> = AttributeKey("aws.sdk.kotlin.ddbmapper#LowLevelOperation")

/**
* For a given high-level [Structure], this attribute key identifies the associated low-level [Structure]
*/
val LowLevelStructure: AttributeKey<Structure> = AttributeKey("aws.sdk.kotlin.ddbmapper#LowLevelStructure")

internal object MapperAttributes {
/**
* For a given [Operation], this attribute key contains relevant pagination members (if applicable) in the request
* and response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.Member
import aws.sdk.kotlin.hll.codegen.model.TypeRef
import aws.sdk.kotlin.hll.codegen.model.Types
import aws.sdk.kotlin.hll.codegen.model.nullable
import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperPkg
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes

private val attrMapTypes = setOf(MapperTypes.AttributeMap, MapperTypes.AttributeMap.nullable())
private val attrMapListTypes = Types.Kotlin.list(MapperTypes.AttributeMap).let { setOf(it, it.nullable()) }
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionArgumentsType.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.ExpressionLiteralType.*
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model.MemberCodegenBehavior.*

/**
* Describes a behavior to apply for a given [Member] in a low-level structure when generating code for an equivalent
Expand Down Expand Up @@ -60,56 +57,105 @@ internal sealed interface MemberCodegenBehavior {
* structure).
*/
data object Hoist : MemberCodegenBehavior

/**
* Indicates that a member is a string expression parameter which should be replaced by an expression DSL
* @param type The type of expression this member models
*/
data class ExpressionLiteral(val type: ExpressionLiteralType) : MemberCodegenBehavior

/**
* Indicates that a member is a map of expression arguments which should be automatically handled by an expression
* DSL
* @param type The type of expression arguments this member models
*/
data class ExpressionArguments(val type: ExpressionArgumentsType) : MemberCodegenBehavior
}

/**
* Identifies a type of expression literal supported by DynamoDB APIs
*/
internal enum class ExpressionLiteralType {
Condition,
Filter,
KeyCondition,
Projection,
Update,
}

/**
* Identifies a type of expression arguments supported by DynamoDB APIs
*/
internal enum class ExpressionArgumentsType {
AttributeNames,
AttributeValues,
}

/**
* Identifies a [MemberCodegenBehavior] for this [Member] by way of various heuristics
*/
internal val Member.codegenBehavior: MemberCodegenBehavior
get() = when {
this in unsupportedMembers -> MemberCodegenBehavior.Drop
type in attrMapTypes -> if (name == "key") MemberCodegenBehavior.MapKeys else MemberCodegenBehavior.MapAll
type in attrMapListTypes -> MemberCodegenBehavior.ListMapAll
isTableName || isIndexName -> MemberCodegenBehavior.Hoist
else -> MemberCodegenBehavior.PassThrough
}
get() = rules.firstNotNullOfOrNull { it.matchedBehaviorOrNull(this) } ?: PassThrough

private val Member.isTableName: Boolean
get() = name == "tableName" && type == Types.Kotlin.StringNullable
private fun llType(name: String) = TypeRef(MapperPkg.Ll.Model, name)

private val Member.isIndexName: Boolean
get() = name == "indexName" && type == Types.Kotlin.StringNullable
private data class Rule(
val namePredicate: (String) -> Boolean,
val typePredicate: (TypeRef) -> Boolean,
val behavior: MemberCodegenBehavior,
) {
constructor(name: String, type: TypeRef, behavior: MemberCodegenBehavior) :
this(name::equals, type::isEquivalentTo, behavior)

private fun llType(name: String) = TypeRef(MapperPkg.Ll.Model, name)
constructor(name: Regex, type: TypeRef, behavior: MemberCodegenBehavior) :
this(name::matches, type::isEquivalentTo, behavior)

private val unsupportedMembers = listOf(
// superseded by ConditionExpression
Member("conditionalOperator", llType("ConditionalOperator")),
Member("expected", Types.Kotlin.stringMap(llType("ExpectedAttributeValue"))),

// superseded by FilterExpression
Member("queryFilter", Types.Kotlin.stringMap(llType("Condition"))),
Member("scanFilter", Types.Kotlin.stringMap(llType("Condition"))),

// superseded by KeyConditionExpression
Member("keyConditions", Types.Kotlin.stringMap(llType("Condition"))),

// superseded by ProjectionExpression
Member("attributesToGet", Types.Kotlin.list(Types.Kotlin.String)),

// superseded by UpdateExpression
Member("attributeUpdates", Types.Kotlin.stringMap(llType("AttributeValueUpdate"))),

// TODO add support for expressions
Member("expressionAttributeNames", Types.Kotlin.stringMap(Types.Kotlin.String)),
Member("expressionAttributeValues", MapperTypes.AttributeMap),
Member("conditionExpression", Types.Kotlin.String),
Member("projectionExpression", Types.Kotlin.String),
Member("updateExpression", Types.Kotlin.String),
).map { member ->
if (member.type is TypeRef) {
member.copy(type = member.type.nullable())
} else {
member
}
}.toSet()
fun matchedBehaviorOrNull(member: Member) = if (matches(member)) behavior else null
fun matches(member: Member) = namePredicate(member.name) && typePredicate(member.type as TypeRef)
}

private fun Type.isEquivalentTo(other: Type): Boolean = when (this) {
is TypeVar -> other is TypeVar && shortName == other.shortName
is TypeRef ->
other is TypeRef &&
fullName == other.fullName &&
genericArgs.size == other.genericArgs.size &&
genericArgs.zip(other.genericArgs).all { (thisArg, otherArg) -> thisArg.isEquivalentTo(otherArg) }
}

/**
* Priority-ordered list of dispositions to apply to members found in structures. The first element from this list that
* successfully matches with a member will be chosen.
*/
private val rules = listOf(
// Deprecated expression members not to be carried forward into HLL
Rule("conditionalOperator", llType("ConditionalOperator"), Drop),
Rule("expected", Types.Kotlin.stringMap(llType("ExpectedAttributeValue")), Drop),
Rule("queryFilter", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("scanFilter", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("keyConditions", Types.Kotlin.stringMap(llType("Condition")), Drop),
Rule("attributesToGet", Types.Kotlin.list(Types.Kotlin.String), Drop),
Rule("attributeUpdates", Types.Kotlin.stringMap(llType("AttributeValueUpdate")), Drop),

// Hoisted members
Rule("tableName", Types.Kotlin.String, Hoist),
Rule("indexName", Types.Kotlin.String, Hoist),

// Expression literals
Rule("keyConditionExpression", Types.Kotlin.String, ExpressionLiteral(KeyCondition)),
Rule("filterExpression", Types.Kotlin.String, ExpressionLiteral(Filter)),

// TODO add support for remaining expression types
Rule("conditionExpression", Types.Kotlin.String, Drop),
Rule("projectionExpression", Types.Kotlin.String, Drop),
Rule("updateExpression", Types.Kotlin.String, Drop),

// Expression arguments
Rule("expressionAttributeNames", Types.Kotlin.stringMap(Types.Kotlin.String), ExpressionArguments(AttributeNames)),
Rule("expressionAttributeValues", MapperTypes.AttributeMap, ExpressionArguments(AttributeValues)),

// Mappable members
Rule(".*".toRegex(), Types.Kotlin.list(MapperTypes.AttributeMap), ListMapAll),
Rule("key", MapperTypes.AttributeMap, MapKeys),
Rule(".*".toRegex(), MapperTypes.AttributeMap, MapAll),
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
*/
package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.ModelAttributes
import aws.sdk.kotlin.hll.codegen.model.Operation
import aws.sdk.kotlin.hll.codegen.util.plus
import aws.smithy.kotlin.runtime.collections.get

/**
* Gets the low-level [Operation] equivalent for this high-level operation
*/
internal val Operation.lowLevel: Operation
get() = attributes[ModelAttributes.LowLevelOperation]

/**
* Derives a high-level [Operation] equivalent for this low-level operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,15 @@ internal data class PaginationMembers(
* property returns `null`.
*/
internal val Operation.paginationInfo: PaginationMembers?
get() = attributes.getOrNull(ModelAttributes.PaginationInfo)
get() = attributes.getOrNull(MapperAttributes.PaginationInfo)

/**
* A codegen plugin that adds DDB-specific pagination info to operations
*/
internal class DdbPaginationPlugin : ModelParsingPlugin {
override fun postProcessOperation(operation: Operation): Operation {
val paginationMembers = PaginationMembers.forOperationOrNull(operation) ?: return operation
val newAttributes = operation.attributes + (ModelAttributes.PaginationInfo to paginationMembers)
val newAttributes = operation.attributes + (MapperAttributes.PaginationInfo to paginationMembers)
return operation.copy(attributes = newAttributes)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.model

import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.codegen.util.plus
import aws.smithy.kotlin.runtime.collections.get

/**
* Gets the low-level [Structure] equivalent for this high-level structure
*/
internal val Structure.lowLevel: Structure
get() = attributes[ModelAttributes.LowLevelStructure]
import aws.sdk.kotlin.hll.dynamodbmapper.codegen.model.MapperTypes

/**
* Derives a high-level [Structure] equivalent for this low-level structure
Expand All @@ -24,20 +18,54 @@ internal fun Structure.toHighLevel(pkg: String): Structure {
val hlType = TypeRef(pkg, llStructure.type.shortName, listOf(TypeVar("T")))

val hlMembers = llStructure.members.mapNotNull { llMember ->
when (llMember.codegenBehavior) {
MemberCodegenBehavior.PassThrough -> llMember
val nullable = llMember.type.nullable

val hlMember = when (val behavior = llMember.codegenBehavior) {
MemberCodegenBehavior.PassThrough, is MemberCodegenBehavior.ExpressionArguments -> llMember

MemberCodegenBehavior.MapAll, MemberCodegenBehavior.MapKeys ->
llMember.copy(type = TypeVar("T", llMember.type.nullable))
llMember.copy(type = TypeVar("T", nullable))

MemberCodegenBehavior.ListMapAll -> {
val llListType = llMember.type as? TypeRef ?: error("`ListMapAll` member is required to be a TypeRef")
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")), nullable = llListType.nullable)
val hlListType = llListType.copy(genericArgs = listOf(TypeVar("T")))
llMember.copy(type = hlListType)
}

is MemberCodegenBehavior.ExpressionLiteral -> {
val expressionType = when (behavior.type) {
ExpressionLiteralType.Filter -> MapperTypes.Expressions.BooleanExpr
ExpressionLiteralType.KeyCondition -> MapperTypes.Expressions.KeyFilter

// TODO add support for other expression types
else -> return@mapNotNull null
}.nullable(nullable)

val dslInfo = when (behavior.type) {
ExpressionLiteralType.Filter -> DslInfo(
interfaceType = MapperTypes.Expressions.Filter,
implType = MapperTypes.Expressions.Internal.FilterImpl,
implSingleton = true,
)

// KeyCondition doesn't use a top-level DSL (SortKeyCondition is nested)
ExpressionLiteralType.KeyCondition -> null

// TODO add support for other expression types
else -> return@mapNotNull null
}

llMember.copy(
name = llMember.name.removeSuffix("Expression"),
type = expressionType,
attributes = llMember.attributes + (ModelAttributes.DslInfo to dslInfo),
)
}

else -> null
}

hlMember?.copy(attributes = hlMember.attributes + (ModelAttributes.LowLevelMember to llMember))
}.toSet()

val hlAttributes = llStructure.attributes + (ModelAttributes.LowLevelStructure to llStructure)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ package aws.sdk.kotlin.hll.dynamodbmapper.codegen.operations.rendering

import aws.sdk.kotlin.hll.codegen.core.CodeGenerator
import aws.sdk.kotlin.hll.codegen.model.*
import aws.sdk.kotlin.hll.codegen.rendering.BuilderRenderer
import aws.sdk.kotlin.hll.codegen.rendering.RenderContext
import aws.sdk.kotlin.hll.codegen.rendering.RenderOptions
import aws.sdk.kotlin.hll.codegen.rendering.Visibility
import aws.sdk.kotlin.hll.codegen.rendering.*
import aws.sdk.kotlin.hll.codegen.util.plus

/**
Expand Down
Loading

0 comments on commit 7a1d42d

Please sign in to comment.