ebowman / api-first-hand

API-First bootstrapping tool for building RESTful web services from a Swagger/OpenAPI spec
MIT License
142 stars 22 forks source link

Routing doesn't capture requests with empty required parameters #49

Closed zyv closed 7 years ago

zyv commented 7 years ago

Hi,

Consider the following hello world schema:

  /welcome/{name}:
    parameters:
      - name: name
        in: path
        description: name of the user
        required: true
        type: string
        pattern: "[A-Za-z0-9]*"
      - name: last_name
        in: query
        description: last name of the user
        required: false
        type: string
        pattern: "[A-Za-z0-9]*"
    get:
      tags:
        - user
      operationId: getWelcomeByName
      description: Say welcome to name.
      responses:
        200:
          description: Return welcome message.
          schema:
            type: string
        default:
          description: unexpected error
          schema:
            $ref: '#/definitions/ErrorModel'

I would expect at least requests to /v1/welcome/ to be routed to getWelcomeByName and fail validation, because non-empty name is required and result in a BadRequest (400). The auto-generated tests seem to agree with me:

[info] GET /v1/welcome/{name}
[info] - should discard invalid data *** FAILED ***
[info]   Expected 400 but got 404
[info]   StatusCode = BAD_REQUEST
[info]   given 'Content-Type' [application/json], 'Accept' header [application/json] and URL: [/v1/welcome/?last_name=%E1%85%B1] given args: '(,Some(ᅱ))' did not equal (xxx:290)

In reality, this doesn't happen, because the pattern for parameters expects at least one character. I've patched it to require zero characters or more, and the test for the response code seems to pass, but then fails with a different problem.

I'm not sure that this is the right way to solve the problem and what additional consequences it might have otherwise, therefore I'm not making a pull request, but I will attach my patch to this issue.

Also, I would expect that requests to /v1/welcome get redirected to /v1/welcome/ if there is no handler explicitly defined to /v1/welcome (my understanding is that this is how Django routing works, for instance). Any opinions on that?

zyv commented 7 years ago

Proposed patch:

diff --git a/plugin/src/main/scala/de/zalando/play/generator/routes/RuleGenerator.scala b/plugin/src/main/scala/de/zalando/play/generator/routes/RuleGenerator.scala
index 2d63658..2acd78d 100644
--- a/plugin/src/main/scala/de/zalando/play/generator/routes/RuleGenerator.scala
+++ b/plugin/src/main/scala/de/zalando/play/generator/routes/RuleGenerator.scala
@@ -75,7 +75,7 @@ object RuleGenerator {
       val name = Path.strip(part)
       val (constraint, encode) = model.findParameter(Reference(name)) match {
         case Some(param) => (param.constraint, param.encode)
-        case None => ("""[^/]+""", true)
+        case None => ("""[^/]*""", true)
       }
       DynamicPart(name, constraint, encode)
     } else {
diff --git a/plugin/src/test/scala/de/zalando/play/generator/routes/RuleGeneratorTest.scala b/plugin/src/test/scala/de/zalando/play/generator/routes/RuleGeneratorTest.scala
index 8a34183..c9c9e2b 100644
--- a/plugin/src/test/scala/de/zalando/play/generator/routes/RuleGeneratorTest.scala
+++ b/plugin/src/test/scala/de/zalando/play/generator/routes/RuleGeneratorTest.scala
@@ -14,7 +14,7 @@ class RuleGeneratorTest extends FunSpec with MustMatchers {
   implicit val model = StrictModel(Nil, Map.empty, Map.empty, Map.empty, "/base/", None, Map.empty, Map.empty)

   val routes = Map(
-    "/" -> Nil, "/a/b/c/d" -> List(StaticPart("a/b/c/d")), "/a/b/c/d/" -> List(StaticPart("a/b/c/d/")), "/a/{b}/c/{d}" -> List(StaticPart("a/"), DynamicPart("b", """[^/]+""", true), StaticPart("/c/"), DynamicPart("d", """[^/]+""", true)), "/{a}/{b}/{c}/{d}/" -> List(DynamicPart("a", """[^/]+""", true), StaticPart("/"), DynamicPart("b", """[^/]+""", true), StaticPart("/"), DynamicPart("c", """[^/]+""", true), StaticPart("/"), DynamicPart("d", """[^/]+""", true), StaticPart("/")), "/{a}/b/{c}/d/" -> List(DynamicPart("a", """[^/]+""", true), StaticPart("/b/"), DynamicPart("c", """[^/]+""", true), StaticPart("/d/"))
+    "/" -> Nil, "/a/b/c/d" -> List(StaticPart("a/b/c/d")), "/a/b/c/d/" -> List(StaticPart("a/b/c/d/")), "/a/{b}/c/{d}" -> List(StaticPart("a/"), DynamicPart("b", """[^/]*""", true), StaticPart("/c/"), DynamicPart("d", """[^/]*""", true)), "/{a}/{b}/{c}/{d}/" -> List(DynamicPart("a", """[^/]*""", true), StaticPart("/"), DynamicPart("b", """[^/]*""", true), StaticPart("/"), DynamicPart("c", """[^/]*""", true), StaticPart("/"), DynamicPart("d", """[^/]*""", true), StaticPart("/")), "/{a}/b/{c}/d/" -> List(DynamicPart("a", """[^/]*""", true), StaticPart("/b/"), DynamicPart("c", """[^/]*""", true), StaticPart("/d/"))
   )

   describe("RuleGeneratorTest") {

New error message showing that at least routing seems now to work correctly:

[info] GET /v1/welcome/{name}
[info] - should discard invalid data *** FAILED ***
[info]   Expected true but got false
[info]   Contains error: error.pattern in "Bad Request: Illegal character in path at index 1: /\n"
[info]   at least one validation failing
[info]   given 'Content-Type' [application/json], 'Accept' header [application/json] and URL: [/v1/welcome/\n?] given args: '(\n,None)' did not equal (xxx:290)
LappleApple commented 7 years ago

Thanks for that suggestion, @zyv.

Hey @slavaschmidt @s12v @gipeshka @stuartmclean, what do you think of this proposed patch?

slavaschmidt commented 7 years ago

The reason for us having it like this is a play definition of the path parameters: The default matching strategy for a dynamic part is defined by the regular expression [^/]+ The proposed change might conflict with this rule in some ways or change the semantics of a path parameter to the dynamic part spanning several /.
I have not tested it, but I have a feeling that calling a play route defined like that GET /v1/welcome/:name controllers.Welcome.byName(name: String) with an empty name will end with Play error response about route not found, and not a controller call. For another framework the proposed change might be a good solution, but I'm not sure about it applied to the Play framework.

zyv commented 7 years ago

Hi Slava,

Thanks for the feedback.

I have not tested it, but I have a feeling that calling a play route defined like that GET /v1/welcome/:name controllers.Welcome.byName(name: String) with an empty name will end with Play error response about route not found, and not a controller call.

I've just checked it and you are right, if the route is defined as /v1/welcome/:name:

If the route is defined as /v1/welcome/:name/:

I have to admit that it's consistent and unopinionated in as far as trailing slashes are concerned, but it's still somewhat disappointing to me that Play routing is so bare-bones as compared to RoR / Django, where on top of that you get automatic smart routes canonicalization (redirects to pages with trailing slash, but only if they do exist in the first place) if so you wish:

https://docs.djangoproject.com/en/dev/topics/http/urls/ https://docs.djangoproject.com/en/dev/ref/settings/#append-slash

In Play, apparently the same can only be achieved with a combination of hacks:

https://github.com/mariussoutier/play-trailing-slash/blob/master/app/TrailingSlash.scala https://gist.github.com/NicolaeNMV/3d375ca2e4b43aa6014f

Anyways, this was my first foray into Play, and I can easily agree that my solution is bad / wrong and should be rejected, but the original issue still stands: auto-generated tests are failing, so I think this issue should be then re-purposed towards fixing auto-generated tests such that they correspond to the adopted routing logic, instead of changing how the routing works.

--Yury.

slavaschmidt commented 7 years ago

@zyv Yury, thank you very much for the feedback and sorry it took so long for me to react. Unfortunately, currently I can't invest time into further development of the plugin. I appreciate though your contribution and think it will help to address the issue with tests. This is exactly the reason why the issue is still open :)

I agree that other frameworks might be superior to Play, but that's another story altogether :)

zyv commented 7 years ago

Hi Slava, I'm not saying that other frameworks are superior to Play, it's just that coming from around the RoR / Django corner I honestly find it shocking what Scala infrastructure looks like, after all the hype you hear from Java folks. It almost feels like after being waterboarded by Liferay and the like, frameworks like Play are godsend to them, and their creators are to be worshiped :-)

Just the other day I was showing a Scala guy how in Django you write your class for a model, which you can use directly as DAO, and automatically generate database-independent migration code (including automatic merging of successive migrations!), and he was like... oh, we either write DAO and generate schemas from it, or the other way around (and so are coupled to a particular DB), but anyways to do migrations you need either Flyway or Forklift, and then another addon library for Slick and everything has to be written by hand...

See, as a guy who does Haskell after work, I'm completely sold on type safety and actually probably much more fanatical about controlling side effects then the Scala folks, but why everything has to be so immature, and complicated, and badly documented, and abundant of boilerplate... I honestly want to like Scala, but why it is letting me down all the time? Take your clarification from another ticket: sbt 0.13 doesn't support Scala 2.11 you say? WHUT?! It's like saying, well, you know, pip only supports Python 2.6 at the moment...

Anyways, I guess we should discuss this in another setting ;-) let me know if you end up near Köln, we could arrange some drinks.

slavaschmidt commented 7 years ago

Closing as resolved