Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

airframe-http: #1100 Support unary and n-ary functions in RPC #1126

Merged
merged 23 commits into from Jun 15, 2020

Conversation

xerial
Copy link
Member

@xerial xerial commented Jun 10, 2020

Addresses #1100

  • Support an arbitrary number of function arguments for RPC calls.
  • sbt-airframe: If RPC call requires multiple arguments, wrap them into a single case class and send it as JSON or MsgPack.
  • Changed to throw IllegalArgumentException and 400 (BadRequest) response when incompatible type data is used in HTTP requests. Note this change still doesn't cover cases described in airframe-http: Validation for unnecessary attributes #978 (unnecessary attributes are given)
  • Properly handle application/octet-stream requests so as not to bind uploaded file contents to RPC arguments

@codecov
Copy link

codecov bot commented Jun 10, 2020

Codecov Report

Merging #1126 into master will decrease coverage by 0.14%.
The diff coverage is 76.92%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1126      +/-   ##
==========================================
- Coverage   82.58%   82.43%   -0.15%     
==========================================
  Files         272      276       +4     
  Lines       10427    10634     +207     
  Branches      684      686       +2     
==========================================
+ Hits         8611     8766     +155     
- Misses       1816     1868      +52     
Impacted Files Coverage Δ
...airframe/http/codegen/client/ScalaHttpClient.scala 91.89% <ø> (ø)
...t/airframe/http/router/HttpRequestDispatcher.scala 90.90% <ø> (ø)
...rc/main/scala/wvlet/airframe/http/HttpClient.scala 94.23% <ø> (ø)
...c/main/scala/wvlet/airframe/http/HttpContext.scala 73.33% <ø> (ø)
...ala/wvlet/airframe/http/codegen/HttpClientIR.scala 70.11% <36.84%> (-23.54%) ⬇️
...n/scala/wvlet/airframe/codec/CollectionCodec.scala 86.31% <64.70%> (-5.15%) ⬇️
.../main/scala/wvlet/airframe/http/router/Route.scala 83.33% <75.00%> (-4.17%) ⬇️
...wvlet/airframe/http/router/HttpRequestMapper.scala 97.50% <97.43%> (+0.53%) ⬆️
...cala/wvlet/airframe/codec/ScalaStandardCodec.scala 96.57% <100.00%> (+0.04%) ⬆️
.../main/scala/wvlet/airframe/msgpack/spi/Value.scala 81.42% <100.00%> (+2.44%) ⬆️
... and 19 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4e7611f...3b057f1. Read the comment docs.

@xerial xerial changed the title airframe-http: #1100 Support nested JSON in RPC mapping airframe-http: #1100 Support unary and n-ary functions in RPC Jun 10, 2020
@xerial xerial requested review from shimamoto and takezoe June 11, 2020 01:26
@xerial
Copy link
Member Author

xerial commented Jun 11, 2020

@takezoe @shimamoto I revised airframe-http Endpoint/RPC mapping so that we can support any type of functions including unary, n-ary functions even though primitive type values are involved.

I know this PR is fairly complicated, but this change will affect all airframe-http applications, so it's better to have your look.

Some(argCodec.fromMsgPack(paramValue.toMsgpack))
case None =>
// When the target parameter is not found in the MapValue, try mapping the content body as a whole
argCodec.unpackMsgPack(msgpack)
Copy link
Member

@takezoe takezoe Jun 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I know this is an original behavior of the parameter mapping in airframe-http, this behavior is sometimes convenient to take whole request body as a string, however, it might be a little dangerous because this happens unintentionally when we give a wrong parameter name.

def rpc1(name: String) = {}

{"name": "hello"} => "hello"
{"meme": "hello"} => """{"meme": "hello"}"""

If we need to do this intentionally, it would be better to use HttpRequest injection than relying on this fallback behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. That's an issue and I'm worrying the compatibility of existing applications using airframe-http. For applications using sbt-airframe (e.g., Conductor), we can fix both of the client and server code at the same time, so we don't need to worry about the compatibility.

An idea is totally deprecating the whole content body mapping as you suggested, and ask users to use Request object.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed this as #1129. At least for RPC, which uses generated clients, we don't need to support the whole body mapping


@RPC
trait MyApi {
def rpc1(p1: String): Unit = {}
Copy link
Member

@takezoe takezoe Jun 11, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way, how does airframe-http work if we define the same name methods as follows?

def rpc1(p1: String): Unit = {}
def rpc1(p1: String, p1: Int): Unit = {}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will throw a runtime exception when building a Router because these methods will be detected as non-unique mappings.

I think we can generate different HTTP path mappings for the same name methods. I'll file a ticket.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filed #1128

@takezoe
Copy link
Member

takezoe commented Jun 11, 2020

I saw unintuitive cases in array (and map) conversion.

def test(p1: Int) = {}
{"p1": [1]} => Seq(null)

def test(p1: Option[Int]) = {}
{"p1": [1]} => Seq(Some(null))

I think it's better to throw an exception in these cases, or use the default value (0 in the first case and None in the second case) at least.

@takezoe
Copy link
Member

takezoe commented Jun 11, 2020

In the mapping from the query string, Seq doesn't work for unary parameter while it works for a nested object. Is this a limitation in the unary parameter?

// Query string: ?name=aaaa&name=bbbb

// This doesn't work
def test(name: Seq[String]) = {}

// This works
case class NestedObject(name: Seq[String])
def test(p1: NestedObject) = {}

Even in the second case, if the query string contains a single parameter as ?name=aaaa, it doesn't work. I hope that Seq("aaaa") is generated in this case.

@xerial
Copy link
Member Author

xerial commented Jun 11, 2020

@takezoe Query string: ?name=aaaa&name=bbbb should be mapped to Seq[String]. I'll add test cases as well as the cases for

def test(p1: Int) = {}
{"p1": [1]} => Seq(null)  <-- throw an Exception for incompatible types

def test(p1: Option[Int]) = {}
{"p1": [1]} => Seq(Some(null)) <-- throw an Excecption for incompatible types  

@xerial
Copy link
Member Author

xerial commented Jun 12, 2020

@takezoe Fixed the corner cases mentioned in the above and filed #1128 and #1129 as future work

@takezoe
Copy link
Member

takezoe commented Jun 15, 2020

Thanks for fixing! Please allow me to ask about another corner case.

How should Seq mapping from the query string work if the query string has no corresponding parameter?

def endpoint3(p1: Seq[String]): Unit = {} // => IllegalArgumentException
def endpoint3(p1: Option[Seq[String]]): Unit = {} // => IllegalArgumentException

It would be nice if the first case will be Nil and the second case will be None as same as the mapping from JSON.

@xerial
Copy link
Member Author

xerial commented Jun 15, 2020

@takezoe Right. It should bind Seq.empty and None respectively. Added test cases and found how to properly handle these cases (primitive, Seq[primitive], Option[primitive], Option[Seq[primitive]]). Thanks for the comment.

Copy link
Member

@takezoe takezoe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great thanks! I have no other cases which don't work expected for now.

@xerial
Copy link
Member Author

xerial commented Jun 15, 2020

Awesome. Your review was really helpful. I appreciate it.

@xerial xerial merged commit 0d2f280 into wvlet:master Jun 15, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants