diff --git a/RELEASE.md b/RELEASE.md index 6c64d185..12a0ab89 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -6,10 +6,11 @@ The main API is fairly stable now, but expect some amount of breaking changes ar - Strictness checks around accepted content types, resulting in Unsupported Media Type (415) in case of mismatch. #####v8.2.0 -- Upgraded version of Finagle that we build against to v6.27.0 +- Upgraded version of Finagle that we build against to v6.27.0. +- Bugfix for Path parameter values not being encoded correctly in clients. #####v8.1.0 -- Added native XML support as a body and a parameter type +- Added native XML support as a body and a parameter type. #####v8.0.1 - Bugfix for cannot bind custom body to a request. diff --git a/src/main/scala/io/fintrospect/parameters/Path.scala b/src/main/scala/io/fintrospect/parameters/Path.scala index b287af06..fdf16479 100644 --- a/src/main/scala/io/fintrospect/parameters/Path.scala +++ b/src/main/scala/io/fintrospect/parameters/Path.scala @@ -1,6 +1,6 @@ package io.fintrospect.parameters -import java.net.URI +import io.fintrospect.util.PathSegmentEncoderDecoder.{decode, encode} import scala.util.Try @@ -38,11 +38,9 @@ object Path extends Parameters[PathParameter, PathBindable] { override def toString() = s"{$name}" - override def unapply(str: String) = Option(str).flatMap(s => { - Try(spec.deserialize(new URI("http://localhost/" + s).getPath.substring(1))).toOption - }) + override def unapply(str: String) = Option(str).flatMap(s => Try(spec.deserialize(decode(s))).toOption) - override def -->(value: T) = Seq(new PathBinding(this, spec.serialize(value))) + override def -->(value: T) = Seq(new PathBinding(this, encode(spec.serialize(value)))) override def iterator = Option(this).iterator } diff --git a/src/main/scala/io/fintrospect/util/PathSegmentEncoderDecoder.scala b/src/main/scala/io/fintrospect/util/PathSegmentEncoderDecoder.scala new file mode 100644 index 00000000..07518482 --- /dev/null +++ b/src/main/scala/io/fintrospect/util/PathSegmentEncoderDecoder.scala @@ -0,0 +1,11 @@ +package io.fintrospect.util + +import java.net.URI + +object PathSegmentEncoderDecoder { + def encode(in: String): String = { + in.split("/").map(new URI("http", "localhost", _).getRawFragment).mkString("%2F") + } + + def decode(in: String): String = new URI("http://localhost/" + in).getPath.substring(1) +} diff --git a/src/test/scala/io/fintrospect/parameters/PathTest.scala b/src/test/scala/io/fintrospect/parameters/PathTest.scala index 64721bb5..86ffb3fe 100644 --- a/src/test/scala/io/fintrospect/parameters/PathTest.scala +++ b/src/test/scala/io/fintrospect/parameters/PathTest.scala @@ -1,6 +1,7 @@ package io.fintrospect.parameters +import org.jboss.netty.handler.codec.http.HttpMethod import org.scalatest.{FunSpec, ShouldMatchers} class PathTest extends FunSpec with ShouldMatchers { @@ -31,6 +32,10 @@ class PathTest extends FunSpec with ShouldMatchers { it("does not url decode reserved characters") { Path.string("urlEncoded").unapply(":@-._~!$&'()*+,;=") shouldEqual Option(":@-._~!$&'()*+,;=") } + + it("handles special characters when binding values") { + (Path.string("urlEncoded") --> "a path/+piece").head.apply(RequestBuild()).build(HttpMethod.GET).getUri shouldEqual "a%20path%2F+piece" + } } } \ No newline at end of file diff --git a/src/test/scala/io/fintrospect/util/PathSegmentEncoderDecoder$Test.scala b/src/test/scala/io/fintrospect/util/PathSegmentEncoderDecoder$Test.scala new file mode 100644 index 00000000..5026cb43 --- /dev/null +++ b/src/test/scala/io/fintrospect/util/PathSegmentEncoderDecoder$Test.scala @@ -0,0 +1,22 @@ +package io.fintrospect.util + +import io.fintrospect.util.PathSegmentEncoderDecoder._ +import org.scalatest.{FunSpec, ShouldMatchers} + +class PathSegmentEncoderDecoder$Test extends FunSpec with ShouldMatchers{ + + describe("encode/decode") { + it("roundtrips") { + val inputString = " :@-._~!$&'()*+,;=" + decode(encode(inputString)) shouldBe inputString + } + + it("does not url encode reserved characters") { + encode(":@-._~!$&'()*+,;=") shouldEqual ":@-._~!$&'()*+,;=" + } + + it("handles spaces and forward slashes gracefully") { + encode("a path/+piece") shouldEqual "a%20path%2F+piece" + } + } +} diff --git a/src/test/scala/presentation/_6/FakeRemoteLibrary.scala b/src/test/scala/presentation/_6/FakeRemoteLibrary.scala index 60b7c91e..517a5200 100644 --- a/src/test/scala/presentation/_6/FakeRemoteLibrary.scala +++ b/src/test/scala/presentation/_6/FakeRemoteLibrary.scala @@ -14,7 +14,6 @@ import presentation.Books class FakeRemoteLibrary(books: Books) { def search(titlePart: String) = new Service[HttpRequest, HttpResponse] { override def apply(request: HttpRequest): Future[HttpResponse] = { - println("i go this", titlePart) val results = books.titles().filter(_.toLowerCase.contains(titlePart.toLowerCase)) Future.value(PlainTextResponseBuilder.Ok(results.mkString(","))) }