Skip to content

Commit a9e46e5

Browse files
ianbotsflauzadis
andauthored
chore: refactor URL types and supporting classes (#989)
Co-authored-by: Matas Lauzadis <[email protected]>
1 parent f65be85 commit a9e46e5

File tree

229 files changed

+4049
-2327
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

229 files changed

+4049
-2327
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"id": "1cd7d354-501b-439a-a01a-3e884558383a",
3+
"type": "feature",
4+
"description": "BREAKING: Overhaul URL APIs to clarify content encoding, when data is in which state, and to reduce the number of times data is encoded/decoded"
5+
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/core/RuntimeTypes.kt

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ object RuntimeTypes {
101101
val TimestampFormat = symbol("TimestampFormat", "time")
102102
val ClientException = symbol("ClientException")
103103

104+
object Collections : RuntimeTypePackage(KotlinDependency.CORE, "collections") {
105+
val Attributes = symbol("Attributes")
106+
val attributesOf = symbol("attributesOf")
107+
val AttributeKey = symbol("AttributeKey")
108+
val get = symbol("get")
109+
val mutableMultiMapOf = symbol("mutableMultiMapOf")
110+
val putIfAbsent = symbol("putIfAbsent")
111+
val putIfAbsentNotNull = symbol("putIfAbsentNotNull")
112+
val ReadThroughCache = symbol("ReadThroughCache")
113+
}
114+
104115
object Content : RuntimeTypePackage(KotlinDependency.CORE, "content") {
105116
val BigDecimal = symbol("BigDecimal")
106117
val BigInteger = symbol("BigInteger")
@@ -144,38 +155,34 @@ object RuntimeTypes {
144155
val SdkManagedGroup = symbol("SdkManagedGroup")
145156
val addIfManaged = symbol("addIfManaged", isExtension = true)
146157
}
158+
159+
object Text : RuntimeTypePackage(KotlinDependency.CORE, "text") {
160+
object Encoding : RuntimeTypePackage(KotlinDependency.CORE, "text.encoding") {
161+
val decodeBase64 = symbol("decodeBase64")
162+
val decodeBase64Bytes = symbol("decodeBase64Bytes")
163+
val encodeBase64 = symbol("encodeBase64")
164+
val encodeBase64String = symbol("encodeBase64String")
165+
val PercentEncoding = symbol("PercentEncoding")
166+
}
167+
}
168+
147169
object Utils : RuntimeTypePackage(KotlinDependency.CORE, "util") {
148-
val Attributes = symbol("Attributes")
149-
val MutableAttributes = symbol("MutableAttributes")
150-
val attributesOf = symbol("attributesOf")
151-
val AttributeKey = symbol("AttributeKey")
152-
val decodeBase64 = symbol("decodeBase64")
153-
val decodeBase64Bytes = symbol("decodeBase64Bytes")
154-
val encodeBase64 = symbol("encodeBase64")
155-
val encodeBase64String = symbol("encodeBase64String")
156170
val ExpiringValue = symbol("ExpiringValue")
157171
val flattenIfPossible = symbol("flattenIfPossible")
158-
val get = symbol("get")
159172
val LazyAsyncValue = symbol("LazyAsyncValue")
160173
val length = symbol("length")
161-
val putIfAbsent = symbol("putIfAbsent")
162-
val putIfAbsentNotNull = symbol("putIfAbsentNotNull")
163-
val ReadThroughCache = symbol("ReadThroughCache")
164174
val truthiness = symbol("truthiness")
165-
val urlEncodeComponent = symbol("urlEncodeComponent", "text")
166175
val toNumber = symbol("toNumber")
167176
val type = symbol("type")
168177
}
169178

170179
object Net : RuntimeTypePackage(KotlinDependency.CORE, "net") {
171180
val Host = symbol("Host")
172-
val parameters = symbol("parameters")
173-
val QueryParameters = symbol("QueryParameters")
174-
val QueryParametersBuilder = symbol("QueryParametersBuilder")
175-
val splitAsQueryParameters = symbol("splitAsQueryParameters")
176-
val toQueryParameters = symbol("toQueryParameters")
177-
val Url = symbol("Url")
178-
val UrlDecoding = symbol("UrlDecoding")
181+
182+
object Url : RuntimeTypePackage(KotlinDependency.CORE, "net.url") {
183+
val QueryParameters = symbol("QueryParameters")
184+
val Url = symbol("Url")
185+
}
179186
}
180187
}
181188

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/auth/AuthSchemeProviderAdapterGenerator.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ class AuthSchemeProviderAdapterGenerator {
4646
RuntimeTypes.Auth.Identity.AuthOption,
4747
) {
4848
withBlock("val params = #T {", "}", AuthSchemeParametersGenerator.getSymbol(ctx.settings)) {
49-
addImport(RuntimeTypes.Core.Utils.get)
49+
addImport(RuntimeTypes.Core.Collections.get)
5050
write("operationName = request.context[#T.OperationName]", RuntimeTypes.SmithyClient.SdkClientOption)
5151

5252
if (ctx.settings.api.enableEndpointAuthProvider) {

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/DefaultEndpointProviderGenerator.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ class DefaultEndpointProviderGenerator(
151151
private fun renderEndpointRule(rule: EndpointRule) {
152152
withConditions(rule.conditions) {
153153
writer.withBlock("return #T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) {
154-
writeInline("#T.parse(", RuntimeTypes.Core.Net.Url)
154+
writeInline("#T.parse(", RuntimeTypes.Core.Net.Url.Url)
155155
renderExpression(rule.endpoint.url)
156-
write(", #1T.DecodeAll - #1T.DecodePath),", RuntimeTypes.Core.Net.UrlDecoding)
156+
write("),")
157157

158158
if (rule.endpoint.headers.isNotEmpty()) {
159159
withBlock("headers = #T {", "},", RuntimeTypes.Http.Headers) {
@@ -168,7 +168,7 @@ class DefaultEndpointProviderGenerator(
168168
}
169169

170170
if (rule.endpoint.properties.isNotEmpty()) {
171-
withBlock("attributes = #T {", "},", RuntimeTypes.Core.Utils.attributesOf) {
171+
withBlock("attributes = #T {", "},", RuntimeTypes.Core.Collections.attributesOf) {
172172
rule.endpoint.properties.entries.forEach { (k, v) ->
173173
val kStr = k.toString()
174174

@@ -180,7 +180,7 @@ class DefaultEndpointProviderGenerator(
180180

181181
// otherwise, we just traverse the value like any other rules expression, object values will
182182
// be rendered as Documents
183-
writeInline("#T(#S) to ", RuntimeTypes.Core.Utils.AttributeKey, kStr)
183+
writeInline("#T(#S) to ", RuntimeTypes.Core.Collections.AttributeKey, kStr)
184184
renderExpression(v)
185185
ensureNewline()
186186
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/DefaultEndpointProviderTestGenerator.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,12 +106,7 @@ class DefaultEndpointProviderTestGenerator(
106106
}
107107

108108
writer.withBlock("val expected = #T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) {
109-
write(
110-
"uri = #1T.parse(#2S, #3T.DecodeAll - #3T.DecodePath),",
111-
RuntimeTypes.Core.Net.Url,
112-
endpoint.url,
113-
RuntimeTypes.Core.Net.UrlDecoding,
114-
)
109+
write("uri = #T.parse(#S),", RuntimeTypes.Core.Net.Url.Url, endpoint.url)
115110

116111
if (endpoint.headers.isNotEmpty()) {
117112
withBlock("headers = #T {", "},", RuntimeTypes.Http.Headers) {
@@ -124,14 +119,14 @@ class DefaultEndpointProviderTestGenerator(
124119
}
125120

126121
if (endpoint.properties.isNotEmpty()) {
127-
withBlock("attributes = #T {", "},", RuntimeTypes.Core.Utils.attributesOf) {
122+
withBlock("attributes = #T {", "},", RuntimeTypes.Core.Collections.attributesOf) {
128123
endpoint.properties.entries.forEach { (k, v) ->
129124
if (k in expectedPropertyRenderers) {
130125
expectedPropertyRenderers[k]!!(writer, Expression.fromNode(v), this@DefaultEndpointProviderTestGenerator)
131126
return@forEach
132127
}
133128

134-
writeInline("#T(#S) to ", RuntimeTypes.Core.Utils.AttributeKey, k)
129+
writeInline("#T(#S) to ", RuntimeTypes.Core.Collections.AttributeKey, k)
135130
renderExpression(Expression.fromNode(v))
136131
ensureNewline()
137132
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/EndpointResolverAdapterGenerator.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ class EndpointResolverAdapterGenerator(
114114
RuntimeTypes.HttpClient.Operation.ResolveEndpointRequest,
115115
EndpointParametersGenerator.getSymbol(ctx.settings),
116116
) {
117-
writer.addImport(RuntimeTypes.Core.Utils.get)
117+
writer.addImport(RuntimeTypes.Core.Collections.get)
118118
withBlock("return #T {", "}", EndpointParametersGenerator.getSymbol(ctx.settings)) {
119119
// The SEP dictates a specific source order to use when binding parameters (from most specific to least):
120120
// 1. staticContextParams (from operation shape)
@@ -164,7 +164,7 @@ class EndpointResolverAdapterGenerator(
164164
val inputContextParams = epParameterIndex.inputContextParams(op)
165165

166166
if (inputContextParams.isNotEmpty()) {
167-
writer.addImport(RuntimeTypes.Core.Utils.get)
167+
writer.addImport(RuntimeTypes.Core.Collections.get)
168168
writer.write("@Suppress(#S)", "UNCHECKED_CAST")
169169
val opInputShape = ctx.model.expectShape(op.inputShape)
170170
val inputSymbol = ctx.symbolProvider.toSymbol(opInputShape)

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/endpoints/discovery/EndpointDiscovererGenerator.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d
5151
) {
5252
write(
5353
"private val cache = #T<DiscoveryParams, #T>(10.#T, #T.System)",
54-
RuntimeTypes.Core.Utils.ReadThroughCache,
54+
RuntimeTypes.Core.Collections.ReadThroughCache,
5555
RuntimeTypes.Core.Net.Host,
5656
KotlinTypes.Time.minutes,
5757
RuntimeTypes.Core.Clock,
@@ -66,7 +66,7 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d
6666
write("")
6767
write(
6868
"""private val discoveryParamsKey = #T<DiscoveryParams>("DiscoveryParams")""",
69-
RuntimeTypes.Core.Utils.AttributeKey,
69+
RuntimeTypes.Core.Collections.AttributeKey,
7070
)
7171
write("private data class DiscoveryParams(private val region: String?, private val identity: String)")
7272
}
@@ -92,7 +92,7 @@ class EndpointDiscovererGenerator(private val ctx: CodegenContext, private val d
9292
write("")
9393
write("val originalEndpoint = delegate.resolve(request)")
9494
withBlock("#T(", ")", RuntimeTypes.SmithyClient.Endpoints.Endpoint) {
95-
write("originalEndpoint.uri.copy(host = discoveredHost),")
95+
write("originalEndpoint.uri.copy { host = discoveredHost },")
9696
write("originalEndpoint.headers,")
9797
write("originalEndpoint.attributes,")
9898
}

codegen/smithy-kotlin-codegen/src/main/kotlin/software/amazon/smithy/kotlin/codegen/rendering/protocol/HttpBindingProtocolGenerator.kt

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -314,46 +314,57 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
314314
renderNonBlankGuard(ctx, binding.member, writer)
315315
}
316316

317-
writer.openBlock("val pathSegments = listOf<String>(", ")") {
318-
httpTrait.uri.segments.forEach { segment ->
319-
if (segment.isLabel || segment.isGreedyLabel) {
320-
// spec dictates member name and label name MUST be the same
321-
val binding = pathBindings.find { binding ->
322-
binding.memberName == segment.content
323-
} ?: throw CodegenException("failed to find corresponding member for httpLabel `${segment.content}")
324-
325-
// shape must be string, number, boolean, or timestamp
326-
val targetShape = ctx.model.expectShape(binding.member.target)
327-
val memberSymbol = ctx.symbolProvider.toSymbol(binding.member)
328-
val identifier = if (targetShape.isTimestampShape) {
329-
writer.addImport(RuntimeTypes.Core.TimestampFormat)
330-
val tsFormat = resolver.determineTimestampFormat(
331-
binding.member,
332-
HttpBinding.Location.LABEL,
333-
defaultTimestampFormat,
334-
)
335-
val nullCheck = if (memberSymbol.isNullable) "?" else ""
336-
val tsLabel = formatInstant("input.${binding.member.defaultName()}$nullCheck", tsFormat, forceString = true)
337-
tsLabel
338-
} else {
339-
"input.${binding.member.defaultName()}"
340-
}
317+
if (httpTrait.uri.segments.isNotEmpty()) {
318+
writer.withBlock("path.encodedSegments {", "}") {
319+
httpTrait.uri.segments.forEach { segment ->
320+
if (segment.isLabel || segment.isGreedyLabel) {
321+
// spec dictates member name and label name MUST be the same
322+
val binding = pathBindings.find { binding ->
323+
binding.memberName == segment.content
324+
}
325+
?: throw CodegenException("failed to find corresponding member for httpLabel `${segment.content}")
326+
327+
// shape must be string, number, boolean, or timestamp
328+
val targetShape = ctx.model.expectShape(binding.member.target)
329+
val memberSymbol = ctx.symbolProvider.toSymbol(binding.member)
330+
val identifier = if (targetShape.isTimestampShape) {
331+
addImport(RuntimeTypes.Core.TimestampFormat)
332+
val tsFormat = resolver.determineTimestampFormat(
333+
binding.member,
334+
HttpBinding.Location.LABEL,
335+
defaultTimestampFormat,
336+
)
337+
val nullCheck = if (memberSymbol.isNullable) "?" else ""
338+
val tsLabel = formatInstant(
339+
"input.${binding.member.defaultName()}$nullCheck",
340+
tsFormat,
341+
forceString = true,
342+
)
343+
tsLabel
344+
} else {
345+
"input.${binding.member.defaultName()}"
346+
}
347+
348+
val encodeFn =
349+
format("#T.SmithyLabel.encode", RuntimeTypes.Core.Text.Encoding.PercentEncoding)
341350

342-
val encodeSymbol = RuntimeTypes.Http.Util.encodeLabel
343-
val encodeFn = if (segment.isGreedyLabel) {
344-
writer.format("#T(greedy = true)", encodeSymbol)
351+
if (segment.isGreedyLabel) {
352+
write("#S.split(#S).mapTo(this) { #L(it) }", "\${$identifier}", '/', encodeFn)
353+
} else {
354+
write("add(#L(#S))", encodeFn, "\${$identifier}")
355+
}
345356
} else {
346-
writer.format("#T()", encodeSymbol)
357+
// literal
358+
val encodeFn = format("#T.Path.encode", RuntimeTypes.Core.Text.Encoding.PercentEncoding)
359+
writer.write("add(#L(\"#L\"))", encodeFn, segment.content.toEscapedLiteral())
347360
}
348-
writer.write("#S.$encodeFn,", "\${$identifier}")
349-
} else {
350-
// literal
351-
writer.write("\"#L\",", segment.content.toEscapedLiteral())
352361
}
353362
}
354363
}
355364

356-
writer.write("""path = pathSegments.joinToString(separator = "/", prefix = "/")""")
365+
if (httpTrait.uri.segments.isEmpty()) {
366+
writer.write("path.trailingSlash = true")
367+
}
357368
} else {
358369
// all literals, inline directly
359370
val resolvedPath = httpTrait.uri.segments.joinToString(
@@ -363,7 +374,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
363374
it.content.toEscapedLiteral()
364375
},
365376
)
366-
writer.write("path = \"#L\"", resolvedPath)
377+
writer.write("path.encoded = \"#L\"", resolvedPath)
367378
}
368379
}
369380

@@ -384,12 +395,18 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
384395

385396
if (queryBindings.isEmpty() && queryLiterals.isEmpty() && queryMapBindings.isEmpty()) return
386397

387-
writer
388-
.withBlock("#T {", "}", RuntimeTypes.Core.Net.parameters) {
389-
queryLiterals.forEach { (key, value) ->
390-
writer.write("append(#S, #S)", key, value)
391-
}
398+
if (queryLiterals.isNotEmpty()) {
399+
writer.withBlock("parameters.decodedParameters {", "}") {
400+
queryLiterals.forEach { (key, value) -> writer.write("add(#S, #S)", key, value) }
401+
}
402+
}
392403

404+
if (queryBindings.isNotEmpty() || queryMapBindings.isNotEmpty()) {
405+
writer.withBlock(
406+
"parameters.decodedParameters(#T.SmithyLabel) {",
407+
"}",
408+
RuntimeTypes.Core.Text.Encoding.PercentEncoding,
409+
) {
393410
// render length check if applicable
394411
queryBindings.forEach { binding -> renderNonBlankGuard(ctx, binding.member, writer) }
395412

@@ -401,8 +418,8 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
401418
val target = ctx.model.expectShape<MapShape>(it.member.target)
402419
val valueTarget = ctx.model.expectShape(target.value.target)
403420
val fn = when (valueTarget.type) {
404-
ShapeType.STRING -> "append"
405-
ShapeType.LIST, ShapeType.SET -> "appendAll"
421+
ShapeType.STRING -> "add"
422+
ShapeType.LIST, ShapeType.SET -> "addAll"
406423
else -> throw CodegenException("unexpected value type for httpQueryParams map")
407424
}
408425

@@ -423,6 +440,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
423440
.dedent()
424441
}
425442
}
443+
}
426444
}
427445

428446
// shared implementation for rendering members that belong to StringValuesMap (e.g. Header or Query parameters)
@@ -764,7 +782,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
764782
)
765783
}
766784
is BlobShape -> {
767-
writer.write("builder.#L = response.headers[#S]?.#T()", memberName, headerName, RuntimeTypes.Core.Utils.decodeBase64)
785+
writer.write("builder.#L = response.headers[#S]?.#T()", memberName, headerName, RuntimeTypes.Core.Text.Encoding.decodeBase64)
768786
}
769787
is StringShape -> {
770788
when {
@@ -779,7 +797,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
779797
)
780798
}
781799
memberTarget.hasTrait<MediaTypeTrait>() -> {
782-
writer.write("builder.#L = response.headers[#S]?.#T()", memberName, headerName, RuntimeTypes.Core.Utils.decodeBase64)
800+
writer.write("builder.#L = response.headers[#S]?.#T()", memberName, headerName, RuntimeTypes.Core.Text.Encoding.decodeBase64)
783801
}
784802
else -> {
785803
writer.write("builder.#L = response.headers[#S]", memberName, headerName)
@@ -839,7 +857,7 @@ abstract class HttpBindingProtocolGenerator : ProtocolGenerator {
839857
"${enumSymbol.name}.fromValue(it)"
840858
}
841859
collectionMemberTarget.hasTrait<MediaTypeTrait>() -> {
842-
writer.addImport(RuntimeTypes.Core.Utils.decodeBase64)
860+
writer.addImport(RuntimeTypes.Core.Text.Encoding.decodeBase64)
843861
"it.decodeBase64()"
844862
}
845863
else -> ""

0 commit comments

Comments
 (0)