Skip to content

Add RequestError body codable#1214

Merged
ianpartridge merged 14 commits intomasterfrom
add_requesterror_body
Feb 21, 2018
Merged

Add RequestError body codable#1214
ianpartridge merged 14 commits intomasterfrom
add_requesterror_body

Conversation

@tunniclm
Copy link
Copy Markdown
Collaborator

@tunniclm tunniclm commented Jan 29, 2018

Add the capability to provide a Codable object to RequestError that will be encoded and sent by the Router as the response body when passed to the completion handler of a codable route.

Description

This PR is one part of a pair, this one is for Kitura, the other is for KituraContracts (Kitura/KituraContracts#12).

In the KituraContracts PR we add a new generic constructor to RequestError that can specify a "base" request error and a body Codable, for example: RequestError(.unavailable, body: health.status).

This will assign the object to a new stored property body as a (type-erased) Codable. The fact the type is erased allow the RequestError struct to remain non-generic (changing it to be generic would be a breaking change to the API).

However, this means that the real type of the body is no longer know. In order for the Router to encode the body into Data in some format (currently we only support JSON, but in the future there may be other formats) we need to be able to access the real type to pass to the encoder.

To achieve this we also store a closure in a second new stored property bodyData at the same time (from within the generic init where we still have access to the real type of the body codable). This closure accepts an argument describing the format into which you want to encode the body and returns a Data containing the encoded form of the body.

A new struct BodyFormat has been added to enumerate the different supported format. Currently only JSON is supported. This was made as a struct rather than an enum to allow new formats to be added without breaking the API (enums are exhaustive and adding a case would break any switch statements relying on that.)

The bodyData closure performs a switch on the BodyFormat provided and calls the appropriate encoder on the body to convert it to Data. If the BodyFormat is not recognised, then an UnsupportedBodyFormatError (another new type added in this change) will be thrown. Since BodyFormat only has a private initializer, no other code should be able to create them, and provided the generic initializer of RequestError and BodyFormat are kept in sync, then UnsupportedBodyFormatError should never be thrown.

In this PR the Router has been updated so that codable routes will check for the existence of a bodyData in any RequestError returned by a codable route handler, and if it is non-nil then will use it to get a Data to send as the body of the response.

This PR also adds a number of tests, and to prevent excessive code duplication a new test builder API has been added to allow construction of concise tests in a declarative style.

Motivation and Context

This change allows codable route handlers to customise the body of error responses. One example of where this feature is needed is the health route generated by the Swift Server Generator and kitura-cli.

This route should send HTTP 200 OK when the application is healthy and HTTP 503 Service Unavailable when the application is not working. It should also provide a response body that is a JSON object describing the status of the application and each dependent service. This body should be sent both when the application is healthy and when it is not, and is arguably more important in the latter case as it will give information about which service is not available.

Without a custom body for RequestError it is not possible to send a response body if the status code is not 2xx (usually HTTP 200 OK, but it varies depending on the HTTP method, eg: delete uses HTTP 204 No content).

How Has This Been Tested?

New tests have been added, tested with swift test.

Checklist:

  • If applicable, I have updated the documentation accordingly.
  • If applicable, I have added tests to cover my changes.

@tunniclm
Copy link
Copy Markdown
Collaborator Author

Working through rebasing the code on master, there are some conflicts to resolve.

@tunniclm tunniclm force-pushed the add_requesterror_body branch from 2dd2a0b to 5a26833 Compare January 30, 2018 14:07
@tunniclm
Copy link
Copy Markdown
Collaborator Author

Conflicts now resolved but this code requires the KituraContracts changes before it will compile.

@codecov-io
Copy link
Copy Markdown

codecov-io commented Jan 30, 2018

Codecov Report

Merging #1214 into master will increase coverage by 0.99%.
The diff coverage is 86.48%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #1214      +/-   ##
==========================================
+ Coverage   86.94%   87.93%   +0.99%     
==========================================
  Files          38       38              
  Lines        2367     2404      +37     
==========================================
+ Hits         2058     2114      +56     
+ Misses        309      290      -19
Flag Coverage Δ
#Kitura 87.93% <86.48%> (+0.99%) ⬆️
Impacted Files Coverage Δ
Sources/Kitura/CodableRouter.swift 74.27% <86.48%> (+8.28%) ⬆️

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 4fbe57d...96ac814. Read the comment docs.

@djones6 djones6 self-requested a review January 31, 2018 14:35
Copy link
Copy Markdown
Collaborator

@djones6 djones6 left a comment

Choose a reason for hiding this comment

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

The changes look good (in particular, the refactoring of the tests looks great), I made a few minor suggestions.

})
}
.request("post", path: "/error/users", data: user)
.hasStatus(.conflict)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Did you want to assert .hasNoData() here?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks correct to me, the "/error/users" route responds with nil.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

It wasn't obvious from the diff, but Dave was pointing out that .hasNoData() wasn't being called here. Then I added it in, but his original comment remains and now it reads like he's questioning if the .hasNoData() should be there!

I think we all agree it should be. :)

})
}
.request("post", path: "/error/users", data: user)
.hasStatus(.conflict)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(same comment as above)

}
router.post("/bodyerror/users") { (user: User, respondWith: (User?, RequestError?) -> Void) in
print("POST on /bodyerror/users for user \(user)")
respondWith(user, RequestError(.conflict, body: Conflict(on: "id")))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think this shouldn't be trying to respond with a user as well as an error. (We should probably have separate tests that check that the error overrides the body, both when the error does / does not have its own body)

return
}
.request("get", path: "/error/status")
.hasStatus(.serviceUnavailable)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

(same comment as above)

@tunniclm
Copy link
Copy Markdown
Collaborator Author

tunniclm commented Feb 1, 2018

@djones6 I think I have addressed everything in the review on this PR, can you re-review for me? Thanks

@tunniclm tunniclm force-pushed the add_requesterror_body branch from 4760018 to 5b613d4 Compare February 1, 2018 17:35
@djones6
Copy link
Copy Markdown
Collaborator

djones6 commented Feb 2, 2018

LGTM, thanks for adding those extra tests.

Package.swift Outdated
.package(url: "https://github.com/IBM-Swift/Kitura-net.git", .upToNextMinor(from: "2.0.0")),
.package(url: "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", .upToNextMinor(from: "1.7.0")),
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", .upToNextMinor(from: "0.0.17"))
.package(url: "https://github.com/IBM-Swift/KituraContracts.git", .branch("add_requesterror_body"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remember to revert before this is merged :)

response.status(status)
do {
if let bodyData = try err.encodeBody(.json) {
response.headers.setType("json")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Indentation is off here.

respondWith(Status("GOOD"), nil)
}
router.get("/error/status") { (respondWith: (Status?, RequestError?) -> Void) in
print("GET on /status")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should be print("GET on /error/status")

respondWith(nil, .serviceUnavailable)
}
router.get("/bodyerror/status") { (respondWith: (Status?, RequestError?) -> Void) in
print("GET on /status")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

print("GET on /bodyerror/status")

})
}
.request("post", path: "/error/users", data: user)
.hasStatus(.conflict)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This looks correct to me, the "/error/users" route responds with nil.

buildServerTest(router, timeout: 30)
.request("get", path: "/status/1")
.hasStatus(.notFound)
.hasData("Cannot GET /status/1.")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not sure if testing the precise error message is wise - 404 seems enough.

@ianpartridge ianpartridge merged commit 0e1f01e into master Feb 21, 2018
@ianpartridge ianpartridge deleted the add_requesterror_body branch February 21, 2018 14:04
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.

4 participants