Skip to content

Commit f55d438

Browse files
authored
airframe-http: Cross-platform Request and Response classes (#982)
* Add HTTP header constants * Add HttpMultiMap * Use HttpMultiMap for adapter interfaces * Changed HttpMethod to String constants for Scala.js interoperability * Fix Finagle/OkHttp backend adapters
1 parent 16fd48f commit f55d438

File tree

24 files changed

+902
-140
lines changed

24 files changed

+902
-140
lines changed

airframe-http-finagle/src/main/scala/wvlet/airframe/http/finagle/FinagleClient.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class FinagleClient(address: ServerAddress, config: FinagleClientConfig)
164164
/**
165165
* Create a new Request
166166
*/
167-
protected def newRequest(method: HttpMethod, path: String): Request = {
167+
protected def newRequest(method: String, path: String): Request = {
168168
Request(toFinagleHttpMethod(method), path)
169169
}
170170

airframe-http-finagle/src/main/scala/wvlet/airframe/http/finagle/package.scala

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,25 @@ package object finagle {
8080
}
8181

8282
implicit object FinagleHttpRequestAdapter extends HttpRequestAdapter[http.Request] {
83-
override def methodOf(request: Request): HttpMethod = toHttpMethod(request.method)
84-
override def pathOf(request: Request): String = request.path
85-
override def headerOf(request: Request): Map[String, String] = request.headerMap.toMap
86-
override def queryOf(request: Request): Map[String, String] = request.params
87-
override def contentStringOf(request: Request): String = request.contentString
83+
override def methodOf(request: Request): String = toHttpMethod(request.method)
84+
override def pathOf(request: Request): String = request.path
85+
override def headerOf(request: Request): HttpMultiMap = {
86+
val h = request.headerMap
87+
var m = HttpMultiMap.empty
88+
for (k <- h.keys) {
89+
h.getAll(k).map { v => m = m.add(k, v) }
90+
}
91+
m
92+
}
93+
override def queryOf(request: Request): HttpMultiMap = {
94+
val p = request.params
95+
var m = HttpMultiMap.empty
96+
for (k <- p.keys) {
97+
p.getAll(k).map { v => m = m.add(k, v) }
98+
}
99+
m
100+
}
101+
override def contentStringOf(request: Request): String = request.contentString
88102
override def contentBytesOf(request: Request): Array[Byte] = {
89103
val content = request.content
90104
val size = content.length
@@ -132,11 +146,11 @@ package object finagle {
132146
)
133147
private val httpMethodMappingReverse = httpMethodMapping.map(x => x._2 -> x._1).toMap
134148

135-
private[finagle] def toHttpMethod(method: http.Method): HttpMethod = {
149+
private[finagle] def toHttpMethod(method: http.Method): String = {
136150
httpMethodMappingReverse.getOrElse(method, throw new IllegalArgumentException(s"Unsupported method: ${method}"))
137151
}
138152

139-
private[finagle] def toFinagleHttpMethod(method: HttpMethod): http.Method = {
153+
private[finagle] def toFinagleHttpMethod(method: String): http.Method = {
140154
httpMethodMapping.getOrElse(method, throw new IllegalArgumentException(s"Unsupported method: ${method}"))
141155
}
142156
}

airframe-http-finagle/src/test/scala/wvlet/airframe/http/finagle/FinagleTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import java.nio.charset.StandardCharsets
1717

1818
import com.twitter.finagle.http
1919
import com.twitter.finagle.http.Status
20-
import wvlet.airframe.http.HttpStatus
20+
import wvlet.airframe.http.{HttpMultiMap, HttpStatus}
2121
import wvlet.airspec.AirSpec
2222

2323
/**
@@ -43,7 +43,7 @@ class FinagleTest extends AirSpec {
4343
val r = req.toHttpRequest
4444
r.method shouldBe toHttpMethod(m)
4545
r.path shouldBe "/hello"
46-
r.query shouldBe Map.empty
46+
r.query shouldBe HttpMultiMap.empty
4747
r.contentString shouldBe "hello finagle"
4848
r.contentBytes shouldBe "hello finagle".getBytes(StandardCharsets.UTF_8)
4949
r.contentType shouldBe Some("application/json;charset=utf-8")

airframe-http-okhttp/src/main/scala/wvlet/airframe/http/okhttp/package.scala

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@ package object okhttp {
1515
}
1616

1717
implicit object OkHttpRequestAdapter extends HttpRequestAdapter[Request] {
18-
override def methodOf(request: Request): HttpMethod = toHttpMethod(request.method())
19-
override def pathOf(request: Request): String = request.url().encodedPath()
20-
override def headerOf(request: Request): Map[String, String] = {
21-
request.headers().toMultimap.asScala.toMap.map { case (k, v) => k -> v.get(0) }
18+
override def methodOf(request: Request): String = toHttpMethod(request.method())
19+
override def pathOf(request: Request): String = request.url().encodedPath()
20+
override def headerOf(request: Request): HttpMultiMap = {
21+
val m = HttpMultiMap.newBuilder
22+
for ((k, lst) <- request.headers().toMultimap.asScala; v <- lst.asScala) {
23+
m += k -> v
24+
}
25+
m.result()
2226
}
23-
override def queryOf(request: Request): Map[String, String] = {
27+
override def queryOf(request: Request): HttpMultiMap = {
28+
val m = HttpMultiMap.newBuilder
2429
(0 until request.url().querySize()).map { i =>
25-
request.url().queryParameterName(i) -> request.url().queryParameterValue(i)
26-
}.toMap
30+
m += request.url().queryParameterName(i) -> request.url().queryParameterValue(i)
31+
}
32+
m.result()
2733
}
2834
override def contentStringOf(request: Request): String = {
2935
val sink: BufferedSink = new Buffer()
@@ -53,7 +59,7 @@ package object okhttp {
5359
override def httpResponseOf(resp: Response): HttpResponse[Response] = OkHttpResponse(resp)
5460
}
5561

56-
private[okhttp] def toHttpMethod(method: String): HttpMethod = method match {
62+
private[okhttp] def toHttpMethod(method: String): String = method match {
5763
case "GET" => HttpMethod.GET
5864
case "POST" => HttpMethod.POST
5965
case "PUT" => HttpMethod.PUT

airframe-http-okhttp/src/test/scala/wvlet/airframe/http/okhttp/OkHttpTest.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import java.nio.charset.StandardCharsets
44

55
import okhttp3.internal.http.HttpMethod
66
import okhttp3.{Protocol, Request, RequestBody, Response, ResponseBody}
7-
import wvlet.airframe.http.HttpStatus
7+
import wvlet.airframe.http.{HttpMultiMap, HttpStatus}
88
import wvlet.airspec.AirSpec
99

1010
class OkHttpTest extends AirSpec {
@@ -26,7 +26,7 @@ class OkHttpTest extends AirSpec {
2626
val r = req.toHttpRequest
2727
r.method shouldBe toHttpMethod(req.method())
2828
r.path shouldBe "/hello"
29-
r.query shouldBe Map.empty
29+
r.query shouldBe HttpMultiMap.empty
3030
if (HttpMethod.permitsRequestBody(req.method())) {
3131
r.contentString shouldBe "hello okhttp"
3232
r.contentBytes shouldBe "hello okhttp".getBytes(StandardCharsets.UTF_8)

airframe-http/.js/src/main/scala/wvlet/airframe/http/js/HttpClient.scala renamed to airframe-http/.js/src/main/scala/wvlet/airframe/http/js/JSHttpClient.scala

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,35 +14,84 @@
1414
package wvlet.airframe.http.js
1515
import java.nio.ByteBuffer
1616

17-
import org.scalajs.dom.XMLHttpRequest
18-
import org.scalajs.dom.ext.Ajax
17+
import org.scalajs.dom
18+
import org.scalajs.dom.{XMLHttpRequest, window}
19+
import org.scalajs.dom.ext.{Ajax, AjaxException}
1920
import org.scalajs.dom.ext.Ajax.InputData
2021
import wvlet.airframe.codec.MessageCodec
22+
import wvlet.airframe.http.HttpMessage.Request
2123
import wvlet.airframe.json.JSON.{JSONArray, JSONObject}
2224
import wvlet.airframe.surface.Surface
23-
import wvlet.log.LogSupport
2425

25-
import scala.concurrent.Future
26+
import scala.concurrent.{Future, Promise}
2627
import scala.scalajs.js.typedarray.{ArrayBuffer, TypedArrayBuffer}
2728

2829
/**
2930
* HttpClient utilities for Scala.js
3031
*/
31-
object HttpClient extends LogSupport {
32-
// Import a queue for callling AJAX call immediately
32+
object JSHttpClient {
3333
import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
3434

35+
def sendRaw[Response](
36+
request: Request,
37+
responseCodec: MessageCodec[Response],
38+
requestFilter: Request => Request = identity
39+
): Future[Response] = {
40+
41+
val protocol = window.location.protocol
42+
val hostname = window.location.hostname
43+
val port = window.location.port
44+
val fullUri = s"${protocol}//${hostname}${if (port.isEmpty) "" else ":" + port}${request.path}"
45+
46+
val xhr = new dom.XMLHttpRequest()
47+
val promise = Promise[dom.XMLHttpRequest]()
48+
49+
xhr.onreadystatechange = { (e: dom.Event) =>
50+
if (xhr.readyState == 4) {
51+
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304)
52+
promise.success(xhr)
53+
else
54+
promise.failure(AjaxException(xhr))
55+
}
56+
}
57+
xhr.open(request.method.toString, fullUri)
58+
xhr.responseType = "arraybuffer"
59+
xhr.timeout = 0
60+
xhr.withCredentials = false
61+
request.header.entries.foreach { x => xhr.setRequestHeader(x.key, x.value) }
62+
val data: Array[Byte] = request.contentBytes
63+
if (data.isEmpty) {
64+
xhr.send()
65+
} else {
66+
val input: InputData = ByteBuffer.wrap(data)
67+
xhr.send(input)
68+
}
69+
70+
val future = promise.future
71+
future.map { xhr: XMLHttpRequest =>
72+
val arrayBuffer = xhr.response.asInstanceOf[ArrayBuffer]
73+
val dst = new Array[Byte](arrayBuffer.byteLength)
74+
TypedArrayBuffer.wrap(arrayBuffer).get(dst, 0, arrayBuffer.byteLength)
75+
responseCodec.fromMsgPack(dst)
76+
}
77+
}
78+
3579
def send[Response](
3680
method: String,
3781
path: String,
3882
data: InputData = null,
3983
responseCodec: MessageCodec[Response],
4084
headers: Map[String, String] = Map.empty
4185
): Future[Response] = {
86+
val protocol = window.location.protocol
87+
val hostname = window.location.hostname
88+
val port = window.location.port
89+
val fullUrl = s"${protocol}//${hostname}${if (port.isEmpty) "" else ":" + port}${path}"
90+
4291
val future =
4392
Ajax(
4493
method = method,
45-
url = path,
94+
url = fullUrl,
4695
data = data,
4796
headers = Map(
4897
// Use MessagePack RPC

airframe-http/.js/src/test/scala/wvlet/airframe/http/js/HttpClientTest.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* limitations under the License.
1313
*/
1414
package wvlet.airframe.http.js
15+
import wvlet.airframe.http.Http
1516
import wvlet.airframe.surface.Surface
1617
import wvlet.airspec.AirSpec
1718

@@ -26,10 +27,14 @@ object HttpClientTest extends AirSpec {
2627
test("create http client") {
2728
ignore("ignore server interaction tests")
2829
val s = Surface.of[Person]
29-
HttpClient.getOps[Person, Person]("/v1/info", Person(1, "leo"), s, s).recover {
30+
JSHttpClient.getOps[Person, Person]("/v1/info", Person(1, "leo"), s, s).recover {
3031
case e: Throwable =>
3132
logger.warn(e)
3233
1
3334
}
3435
}
36+
37+
test("request") {
38+
val req = Http.request("/v1/info")
39+
}
3540
}

airframe-http/.jvm/src/main/scala/wvlet/airframe/http/codegen/HttpClientIR.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ object HttpClientIR extends LogSupport {
5151
case class ClientClassDef(clsName: String, services: Seq[ClientServiceDef]) extends ClientCodeIR
5252
case class ClientServiceDef(serviceName: String, methods: Seq[ClientMethodDef]) extends ClientCodeIR
5353
case class ClientMethodDef(
54-
httpMethod: HttpMethod,
54+
httpMethod: String,
5555
isOpsRequest: Boolean,
5656
name: String,
5757
typeArgs: Seq[Surface],

airframe-http/.jvm/src/main/scala/wvlet/airframe/http/codegen/client/ScalaHttpClient.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ object ScalaJSClient extends HttpClientType {
157157
|
158158
|import scala.concurrent.Future
159159
|import wvlet.airframe.surface.Surface
160-
|import wvlet.airframe.http.js.HttpClient
160+
|import wvlet.airframe.http.js.JSHttpClient
161161
|${src.imports.map(x => s"import ${x.rawType.getName}").mkString("\n")}
162162
|
163163
|${cls}""".stripMargin
@@ -195,7 +195,7 @@ object ScalaJSClient extends HttpClientType {
195195
sendRequestArgs += "headers = headers"
196196

197197
s"""def ${m.name}(${inputArgs.mkString(", ")}): Future[${m.returnType.name}] = {
198-
| HttpClient.${httpClientMethodName}[${m.typeArgs.map(_.name).mkString(", ")}](${sendRequestArgs.result
198+
| JSHttpClient.${httpClientMethodName}[${m.typeArgs.map(_.name).mkString(", ")}](${sendRequestArgs.result
199199
.mkString(", ")})
200200
|}""".stripMargin
201201
}.mkString("\n")

airframe-http/.jvm/src/main/scala/wvlet/airframe/http/router/HttpRequestMapper.scala

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,20 @@ package wvlet.airframe.http.router
1515

1616
import wvlet.airframe.codec.PrimitiveCodec.StringCodec
1717
import wvlet.airframe.codec.{JSONCodec, MessageCodec, MessageCodecFactory}
18-
import wvlet.airframe.http.{HttpContext, HttpMethod, HttpRequest, HttpRequestAdapter}
18+
import wvlet.airframe.http.{HttpContext, HttpMethod, HttpMultiMap, HttpMultiMapCodec, HttpRequest, HttpRequestAdapter}
1919
import wvlet.airframe.json.JSON
2020
import wvlet.airframe.msgpack.spi.MessagePack
2121
import wvlet.airframe.surface.reflect.ReflectMethodSurface
2222
import wvlet.airframe.surface.{OptionSurface, Zero}
2323
import wvlet.log.LogSupport
24-
import scala.language.higherKinds
2524

25+
import scala.language.higherKinds
2626
import scala.util.Try
2727

2828
/**
2929
* Mapping HTTP requests to method call arguments
3030
*/
3131
object HttpRequestMapper extends LogSupport {
32-
private val stringMapCodec = MessageCodec.of[Map[String, String]]
33-
3432
def buildControllerMethodArgs[Req, Resp, F[_]](
3533
// This instance is necessary to retrieve the default method argument values
3634
controller: Any,
@@ -44,8 +42,8 @@ object HttpRequestMapper extends LogSupport {
4442
implicit adapter: HttpRequestAdapter[Req]
4543
): Seq[Any] = {
4644
// Collect URL query parameters and other parameters embedded inside URL.
47-
val requestParams: Map[String, String] = adapter.queryOf(request) ++ params
48-
lazy val queryParamMsgpack = stringMapCodec.toMsgPack(requestParams)
45+
val requestParams: HttpMultiMap = adapter.queryOf(request) ++ params
46+
lazy val queryParamMsgpack = HttpMultiMapCodec.toMsgPack(requestParams)
4947

5048
// Build the function arguments
5149
val methodArgs: Seq[Any] =

0 commit comments

Comments
 (0)