Skip to content

Commit 7d20fd8

Browse files
frojasgsvc-squareup-copybara
authored andcommitted
Allow binding custom StringConverters
## Description Currently params had to be one of the supported types, or expose a `valueOf` function. However at Faire, we want to offer better type validation for our parameters. For example, we have `Token<T>` where `T` could be something like `Brand` or `Product`. Each of these token types has a unique prefix, so even if we have a `valueOf` function exposed we lack the ability to validate the type is correct. With this change we can inspect the type of the parameter, and validate it's the correct type before converting the `String`. Note that this is technically a breaking change because I've changed `StringConverter` to an interface, and updated the public constructor of `FeatureBindingFactory`, but they're small enough changes that I don't think people will have any issues upgrading. ## Testing Strategy I updated `QueryParamFeatureBindingTest` to show that a custom `StringConverter` can be used to properly parse a string to a value class, which otherwise would fail. I didn't update all the other binding tests because the code path is the same, but can update them if people feel strongly. GitOrigin-RevId: ce50018634417ae91eef5dbba8d7e6d7c58c9b1f
1 parent 769c5be commit 7d20fd8

21 files changed

+192
-85
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ lib
2121
dist
2222
.hash
2323
hooks.gradle
24+
.kotlin
2425

2526
**/site/
2627
docs/0.x/*

misk/api/misk.api

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,7 +1296,11 @@ public final class misk/web/FeatureBinding$DefaultImpls {
12961296
}
12971297

12981298
public abstract interface class misk/web/FeatureBinding$Factory {
1299-
public abstract fun create (Lmisk/Action;Lmisk/web/PathPattern;Lmisk/web/FeatureBinding$Claimer;)Lmisk/web/FeatureBinding;
1299+
public abstract fun create (Lmisk/Action;Lmisk/web/PathPattern;Lmisk/web/FeatureBinding$Claimer;Ljava/util/List;)Lmisk/web/FeatureBinding;
1300+
}
1301+
1302+
public final class misk/web/FeatureBinding$Factory$DefaultImpls {
1303+
public static synthetic fun create$default (Lmisk/web/FeatureBinding$Factory;Lmisk/Action;Lmisk/web/PathPattern;Lmisk/web/FeatureBinding$Claimer;Ljava/util/List;ILjava/lang/Object;)Lmisk/web/FeatureBinding;
13001304
}
13011305

13021306
public abstract interface class misk/web/FeatureBinding$Subject {
@@ -1857,8 +1861,17 @@ public final class misk/web/extractors/RequestBodyException : java/io/IOExceptio
18571861
public fun <init> (Ljava/lang/Throwable;)V
18581862
}
18591863

1864+
public abstract interface class misk/web/extractors/StringConverter {
1865+
public abstract fun convert (Ljava/lang/String;)Ljava/lang/Object;
1866+
}
1867+
1868+
public abstract interface class misk/web/extractors/StringConverter$Factory {
1869+
public abstract fun create (Lkotlin/reflect/KType;)Lmisk/web/extractors/StringConverter;
1870+
}
1871+
18601872
public final class misk/web/extractors/StringConverterKt {
1861-
public static final fun converterFor (Lkotlin/reflect/KType;)Lkotlin/jvm/functions/Function1;
1873+
public static final fun converterFor (Lkotlin/reflect/KType;Ljava/util/List;)Lmisk/web/extractors/StringConverter;
1874+
public static synthetic fun converterFor$default (Lkotlin/reflect/KType;Ljava/util/List;ILjava/lang/Object;)Lmisk/web/extractors/StringConverter;
18621875
}
18631876

18641877
public final class misk/web/formatter/ClassNameFormatter {

misk/src/main/kotlin/misk/grpc/GrpcFeatureBinding.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import misk.web.FeatureBinding.Claimer
1515
import misk.web.FeatureBinding.Subject
1616
import misk.web.PathPattern
1717
import misk.web.WebConfig
18+
import misk.web.extractors.StringConverter
1819
import misk.web.mediatype.MediaTypes
1920
import java.lang.reflect.Type
2021
import kotlin.coroutines.CoroutineContext
@@ -137,7 +138,8 @@ internal class GrpcFeatureBinding(
137138
override fun create(
138139
action: Action,
139140
pathPattern: PathPattern,
140-
claimer: Claimer
141+
claimer: Claimer,
142+
stringConverterFactories: List<StringConverter.Factory>
141143
): FeatureBinding? {
142144
if (action.dispatchMechanism != DispatchMechanism.GRPC) return null
143145

misk/src/main/kotlin/misk/web/FeatureBinding.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package misk.web
22

33
import misk.Action
44
import misk.web.actions.WebAction
5+
import misk.web.extractors.StringConverter
56
import okio.BufferedSink
67
import okio.BufferedSource
78
import java.util.regex.Matcher
@@ -64,7 +65,8 @@ interface FeatureBinding {
6465
fun create(
6566
action: Action,
6667
pathPattern: PathPattern,
67-
claimer: Claimer
68+
claimer: Claimer,
69+
stringConverterFactories: List<StringConverter.Factory> = listOf(),
6870
): FeatureBinding?
6971
}
7072

misk/src/main/kotlin/misk/web/MiskWebModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import misk.web.extractors.RequestCookiesFeatureBinding
5858
import misk.web.extractors.RequestHeaderFeatureBinding
5959
import misk.web.extractors.RequestHeadersFeatureBinding
6060
import misk.web.extractors.ResponseBodyFeatureBinding
61+
import misk.web.extractors.StringConverter
6162
import misk.web.extractors.WebSocketFeatureBinding
6263
import misk.web.extractors.WebSocketListenerFeatureBinding
6364
import misk.web.interceptors.BeforeContentEncoding
@@ -167,6 +168,7 @@ class MiskWebModule @JvmOverloads constructor(
167168

168169
// Initialize empty sets for our multibindings.
169170
newMultibinder<NetworkInterceptor.Factory>()
171+
newMultibinder<StringConverter.Factory>()
170172
newMultibinder<ApplicationInterceptor.Factory>()
171173
newMultibinder<StaticResourceEntry>()
172174
newMultibinder<WebProxyEntry>()

misk/src/main/kotlin/misk/web/WebActionBinding.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import okio.BufferedSink
66
import okio.BufferedSource
77
import java.util.regex.Matcher
88
import jakarta.inject.Inject
9+
import misk.web.extractors.StringConverter
910
import kotlin.reflect.KParameter
1011

1112
/** HTTP binding as specified by [FeatureBinding]. */
@@ -220,15 +221,16 @@ internal class WebActionBinding @Inject constructor(
220221

221222
/** Creates feature bindings for use before and after a web call. */
222223
class Factory @Inject constructor(
223-
private val featureBindingFactories: List<FeatureBinding.Factory>
224+
private val featureBindingFactories: List<FeatureBinding.Factory>,
225+
private val stringConverterFactories: List<StringConverter.Factory>,
224226
) {
225227
fun create(
226228
action: Action,
227229
pathPattern: PathPattern
228230
): WebActionBinding {
229231
val claimer = RealClaimer(action)
230232
for (factory in featureBindingFactories) {
231-
val binding = factory.create(action, pathPattern, claimer)
233+
val binding = factory.create(action, pathPattern, claimer, stringConverterFactories)
232234
claimer.commitClaims(factory, binding)
233235
}
234236
return claimer.newWebActionBinder()

misk/src/main/kotlin/misk/web/extractors/FormAdapter.kt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,28 @@ internal class FormAdapter<T : Any> private constructor(
4040
) {
4141
fun valuesToParameter(values: Collection<String>): Any? {
4242
if (isList) {
43-
return values.map { converter?.invoke(it) }.toList()
43+
return values.map { converter?.convert(it) }.toList()
4444
} else {
4545
val first = values.firstOrNull() ?: return null
46-
return converter?.invoke(first)
46+
return converter?.convert(first)
4747
}
4848
}
4949
}
5050

5151
companion object {
5252
/** Returns an adapter for [kClass], or null if it cannot be adapted. */
53-
fun <T : Any> create(kClass: KClass<T>): FormAdapter<T>? {
53+
fun <T : Any> create(
54+
kClass: KClass<T>,
55+
stringConverterFactories: List<StringConverter.Factory>,
56+
): FormAdapter<T>? {
5457
val constructor = kClass.primaryConstructor ?: return null
55-
val fields = constructor.parameters.map { it.toField() }
58+
val fields = constructor.parameters.map { it.toField(stringConverterFactories) }
5659
return FormAdapter(constructor, fields)
5760
}
5861

59-
private fun KParameter.toField(): Field {
62+
private fun KParameter.toField(
63+
stringConverterFactories: List<StringConverter.Factory>,
64+
): Field {
6065
val annotation = findAnnotation<FormField>()
6166
val name = annotation?.name?.lowercase()
6267
?: name?.lowercase()
@@ -70,7 +75,10 @@ internal class FormAdapter<T : Any> private constructor(
7075
isOptional,
7176
type.isMarkedNullable,
7277
isList,
73-
converterFor(if (isList) type.arguments.first().type!! else type)
78+
converterFor(
79+
if (isList) type.arguments.first().type!! else type,
80+
stringConverterFactories,
81+
)
7482
)
7583
}
7684
}

misk/src/main/kotlin/misk/web/extractors/FormValueFeatureBinding.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,14 @@ internal class FormValueFeatureBinding<T : Any>(
2424
override fun create(
2525
action: Action,
2626
pathPattern: PathPattern,
27-
claimer: Claimer
27+
claimer: Claimer,
28+
stringConverterFactories: List<StringConverter.Factory>
2829
): FeatureBinding? {
2930
val parameter = action.parameterAnnotatedOrNull<FormValue>() ?: return null
3031
if (parameter.type.classifier !is KClass<*>) return null
3132

3233
val kClass = parameter.type.classifier as KClass<*>
33-
val formAdapter = FormAdapter.create(kClass) ?: return null
34+
val formAdapter = FormAdapter.create(kClass, stringConverterFactories) ?: return null
3435
claimer.claimRequestBody()
3536
claimer.claimParameter(parameter)
3637
return FormValueFeatureBinding(parameter, formAdapter)

misk/src/main/kotlin/misk/web/extractors/PathParamFeatureBinding.kt

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,20 @@ internal class PathParamFeatureBinding private constructor(
2626
) {
2727
fun bind(subject: Subject) {
2828
val pathParam = subject.pathMatcher.group(patternIndex + 1)
29-
subject.setParameter(parameter, converter.invoke(pathParam))
29+
subject.setParameter(parameter, converter.convert(pathParam))
3030
}
3131
}
3232

3333
companion object Factory : FeatureBinding.Factory {
3434
override fun create(
3535
action: Action,
3636
pathPattern: PathPattern,
37-
claimer: Claimer
37+
claimer: Claimer,
38+
stringConverterFactories: List<StringConverter.Factory>
3839
): FeatureBinding? {
39-
val bindings = action.parameters.mapNotNull { it.toParameterBinding(pathPattern) }
40+
val bindings = action.parameters.mapNotNull {
41+
it.toParameterBinding(pathPattern, stringConverterFactories)
42+
}
4043
if (bindings.isEmpty()) return null
4144

4245
for (binding in bindings) {
@@ -46,14 +49,17 @@ internal class PathParamFeatureBinding private constructor(
4649
return PathParamFeatureBinding(bindings)
4750
}
4851

49-
private fun KParameter.toParameterBinding(pathPattern: PathPattern): ParameterBinding? {
52+
private fun KParameter.toParameterBinding(
53+
pathPattern: PathPattern,
54+
stringConverterFactories: List<StringConverter.Factory>
55+
): ParameterBinding? {
5056
val annotation = findAnnotation<PathParam>() ?: return null
5157
val name = if (annotation.value.isBlank()) name else annotation.value
5258

5359
val patternIndex = pathPattern.variableNames.indexOf(name)
5460
if (patternIndex == -1) return null
5561

56-
val converter = converterFor(type)
62+
val converter = converterFor(type, stringConverterFactories)
5763
?: throw IllegalArgumentException("cannot convert path parameters to $type")
5864

5965
return ParameterBinding(patternIndex, this, converter)

misk/src/main/kotlin/misk/web/extractors/QueryParamFeatureBinding.kt

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ internal class QueryParamFeatureBinding private constructor(
4343
if (values.isEmpty()) return null
4444

4545
try {
46-
return if (isList) values.map { converter(it) }
47-
else converter(values.first())
46+
return if (isList) values.map { converter.convert(it) }
47+
else converter.convert(values.first())
4848
} catch (e: IllegalArgumentException) {
4949
throw BadRequestException("Invalid format for parameter: $name", e)
5050
}
@@ -55,9 +55,10 @@ internal class QueryParamFeatureBinding private constructor(
5555
override fun create(
5656
action: Action,
5757
pathPattern: PathPattern,
58-
claimer: Claimer
58+
claimer: Claimer,
59+
stringConverterFactories: List<StringConverter.Factory>
5960
): FeatureBinding? {
60-
val bindings = action.parameters.mapNotNull { it.toQueryBinding() }
61+
val bindings = action.parameters.mapNotNull { it.toQueryBinding(stringConverterFactories) }
6162
if (bindings.isEmpty()) return null
6263

6364
for (binding in bindings) {
@@ -67,13 +68,15 @@ internal class QueryParamFeatureBinding private constructor(
6768
return QueryParamFeatureBinding(bindings)
6869
}
6970

70-
internal fun KParameter.toQueryBinding(): ParameterBinding? {
71+
internal fun KParameter.toQueryBinding(
72+
stringConverterFactories: List<StringConverter.Factory>,
73+
): ParameterBinding? {
7174
val annotation = findAnnotation<QueryParam>() ?: return null
7275
val name = if (annotation.value.isBlank()) name!! else annotation.value
7376

7477
val isList = type.classifier?.equals(List::class) ?: false
7578
val elementType = if (isList) type.arguments.first().type!! else type
76-
val stringConverter = converterFor(elementType)
79+
val stringConverter = converterFor(elementType, stringConverterFactories)
7780
?: throw IllegalArgumentException("Unable to create converter for $name")
7881

7982
return ParameterBinding(this, isList, stringConverter, name)

0 commit comments

Comments
 (0)