Skip to content

Commit

Permalink
Merge pull request #31 from outfoxx/feature/path-encoders
Browse files Browse the repository at this point in the history
Add support for path parameter encoders
  • Loading branch information
kdubb authored Dec 9, 2022
2 parents eb30a48 + 2c74f94 commit a7dd9ec
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 13 deletions.
52 changes: 52 additions & 0 deletions core/src/main/kotlin/io/outfoxx/sunday/PathEncoders.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2020 Outfox, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.outfoxx.sunday

import com.fasterxml.jackson.annotation.JsonProperty
import kotlin.reflect.KClass

typealias PathEncoder = (Any) -> String
typealias PathEncoderMap = Map<KClass<*>, PathEncoder>

object PathEncoders {

val default: PathEncoderMap
get() = mapOf(
Enum::class to { encodeEnum(it as Enum<*>) }
)

fun <E : Enum<E>> encodeEnum(value: Enum<E>) =
value.javaClass
.getField(value.name)
.getAnnotation(JsonProperty::class.java)
?.value
?: value.name

}

inline fun <reified T : Any> PathEncoderMap.add(
type: KClass<T>,
crossinline encoder: (T) -> String
): PathEncoderMap {
return this + mapOf(type to { encoder.invoke(it as T) })
}

inline fun <reified T : Any> PathEncoderMap.add(
crossinline encoder: (T) -> String
): PathEncoderMap {
return add(T::class, encoder)
}
1 change: 1 addition & 0 deletions core/src/main/kotlin/io/outfoxx/sunday/RequestFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ abstract class RequestFactory : Closeable {
abstract val registeredProblemTypes: Map<String, KClass<out ThrowableProblem>>
abstract val mediaTypeEncoders: MediaTypeEncoders
abstract val mediaTypeDecoders: MediaTypeDecoders
abstract val pathEncoders: Map<KClass<*>, PathEncoder>

/**
* Create a [Request].
Expand Down
26 changes: 15 additions & 11 deletions core/src/main/kotlin/io/outfoxx/sunday/URITemplate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package io.outfoxx.sunday

import com.fasterxml.jackson.annotation.JsonProperty
import com.github.hal4j.uritemplate.URIBuilder
import com.github.hal4j.uritemplate.URITemplate
import io.outfoxx.sunday.http.Parameters
import kotlin.reflect.KClass

/**
* RFC 6570 URI template and parameters.
Expand All @@ -44,9 +44,14 @@ class URITemplate(
* before parameter replacement.
* @param parameters Parameters that override the [io.outfoxx.sunday.URITemplate.parameters]
* on the template instance before parameter replacement.
* @param encoders Map of [PathEncoder]s used to convert parameters to path strings.
* @return [URIBuilder] with a fully resolved URI and replaced parameters.
*/
fun resolve(relative: String? = null, parameters: Parameters? = null): URIBuilder {
fun resolve(
relative: String? = null,
parameters: Parameters? = null,
encoders: Map<KClass<*>, PathEncoder> = mapOf()
): URIBuilder {

val template = URITemplate(join(template, relative))

Expand All @@ -57,20 +62,19 @@ class URITemplate(
this.parameters

val allStringParameters =
allParameters.mapValues { entry ->
when (val value = entry.value) {
is Enum<*> -> enumName(value)
else -> value.toString()
allParameters
.filterValues { it != null }
.mapValues { (_, value) ->
value!!
encoders.entries
.firstOrNull { it.key.isInstance(value) }
?.value?.invoke(value)
?: value.toString()
}
}

return template.expand(allStringParameters).toBuilder()
}

private fun <E : Enum<E>> enumName(value: Enum<E>) =
value.javaClass.getField(value.name).getAnnotation(JsonProperty::class.java)?.value
?: value.name

private fun join(base: String, relative: String?) =
if (relative != null) {
if (base.endsWith("/") && relative.startsWith("/")) {
Expand Down
40 changes: 40 additions & 0 deletions core/src/test/kotlin/PathEncodersTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2020 Outfox, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import io.outfoxx.sunday.PathEncoders
import io.outfoxx.sunday.add
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.jupiter.api.Test
import java.util.UUID

class PathEncodersTest {

@Test
fun `adding implicitly typed encoders`() {

val encoders = PathEncoders.default.add(UUID::toString)
assertThat(encoders, Matchers.aMapWithSize(2))
}

@Test
fun `adding explicitly typed encoders`() {

val encoders = PathEncoders.default.add(UUID::class, UUID::toString)
assertThat(encoders, Matchers.aMapWithSize(2))
}

}
67 changes: 67 additions & 0 deletions core/src/test/kotlin/URITemplateTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright 2020 Outfox, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import com.fasterxml.jackson.annotation.JsonProperty
import io.outfoxx.sunday.PathEncoder
import io.outfoxx.sunday.PathEncoders
import io.outfoxx.sunday.URITemplate
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.equalTo
import org.junit.jupiter.api.Test
import java.util.UUID
import kotlin.reflect.KClass

class URITemplateTest {

enum class TestEnum {
@JsonProperty("test-value")
TestValue
}

@Test
fun `test enum encoding`() {

val path =
URITemplate("http://example.com/{enum}", mapOf("enum" to TestEnum.TestValue))
.resolve(encoders = PathEncoders.default)
.toURI()
.toString()

assertThat(path, equalTo("http://example.com/test-value"))
}

@Test
fun `test custom encoding`() {

val encoders: Map<KClass<*>, PathEncoder> =
mapOf(
UUID::class to { (it as UUID).toString().replace("-", "") }
)

val id = UUID.randomUUID()
val path =
URITemplate("http://example.com/objects/{id}", mapOf("id" to id, "none" to null))
.resolve(encoders = encoders)
.toURI()
.toString()

assertThat(
path,
equalTo("http://example.com/objects/${id.toString().replace("-", "")}")
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,32 @@ abstract class RequestFactoryTest {
* Request Building
*/

@Test
fun `encodes path parameters`() {

createRequestFactory(URITemplate("http://example.com/{id}"))
.use { requestFactory ->

val request =
runBlocking {
requestFactory.request(
Method.Get,
"/encoded-params",
pathParameters = mapOf("id" to 123),
body = null,
contentTypes = null,
acceptTypes = null,
headers = null
)
}

assertThat(
request.uri,
equalTo(URI("http://example.com/123/encoded-params"))
)
}
}

@Test
fun `encodes query parameters`() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import io.outfoxx.sunday.EventSource
import io.outfoxx.sunday.MediaType
import io.outfoxx.sunday.MediaType.Companion.JSON
import io.outfoxx.sunday.MediaType.Companion.WWWFormUrlEncoded
import io.outfoxx.sunday.PathEncoder
import io.outfoxx.sunday.PathEncoders
import io.outfoxx.sunday.RequestFactory
import io.outfoxx.sunday.SundayError
import io.outfoxx.sunday.SundayError.Reason.EventDecodingFailed
Expand Down Expand Up @@ -68,6 +70,7 @@ class JdkRequestFactory(
private val adapter: suspend (HttpRequest) -> HttpRequest = { it },
override val mediaTypeEncoders: MediaTypeEncoders = MediaTypeEncoders.default,
override val mediaTypeDecoders: MediaTypeDecoders = MediaTypeDecoders.default,
override val pathEncoders: Map<KClass<*>, PathEncoder> = PathEncoders.default,
private val requestTimeout: Duration = requestTimeoutDefault,
private val eventRequestTimeout: Duration = EventSource.eventTimeoutDefault
) : RequestFactory(), Closeable {
Expand Down Expand Up @@ -118,7 +121,7 @@ class JdkRequestFactory(

var uri =
try {
baseURI.resolve(pathTemplate, pathParameters).toURI()
baseURI.resolve(pathTemplate, pathParameters, pathEncoders).toURI()
} catch (x: Throwable) {
throw SundayError(InvalidBaseUri, cause = x)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import io.outfoxx.sunday.EventSource
import io.outfoxx.sunday.MediaType
import io.outfoxx.sunday.MediaType.Companion.JSON
import io.outfoxx.sunday.MediaType.Companion.WWWFormUrlEncoded
import io.outfoxx.sunday.PathEncoder
import io.outfoxx.sunday.PathEncoders
import io.outfoxx.sunday.RequestFactory
import io.outfoxx.sunday.SundayError
import io.outfoxx.sunday.SundayError.Reason.EventDecodingFailed
Expand Down Expand Up @@ -63,6 +65,7 @@ class OkHttpRequestFactory(
private val eventHttpClient: OkHttpClient = httpClient.reconfiguredForEvents(),
override val mediaTypeEncoders: MediaTypeEncoders = MediaTypeEncoders.default,
override val mediaTypeDecoders: MediaTypeDecoders = MediaTypeDecoders.default,
override val pathEncoders: Map<KClass<*>, PathEncoder> = PathEncoders.default,
) : RequestFactory(), Closeable {

companion object {
Expand Down Expand Up @@ -92,7 +95,8 @@ class OkHttpRequestFactory(
logger.trace("Building request")

val urlBuilder =
baseURI.resolve(pathTemplate, pathParameters).toURI().toHttpUrlOrNull()?.newBuilder()
baseURI.resolve(pathTemplate, pathParameters, pathEncoders)
.toURI().toHttpUrlOrNull()?.newBuilder()
?: throw SundayError(InvalidBaseUri)

if (!queryParameters.isNullOrEmpty()) {
Expand Down

0 comments on commit a7dd9ec

Please sign in to comment.