Skip to content

Add RequestError body codable#12

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

Add RequestError body codable#12
ianpartridge merged 16 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 KituraContracts, the other is for Kitura (Kitura/Kitura#1214).

In this 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.

This PR also adds a few small tests for the new initializer and properties.

Jazzy API doc has been added for the new code.

In the Kitura 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.

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.

@codecov-io
Copy link
Copy Markdown

codecov-io commented Jan 29, 2018

Codecov Report

❗ No coverage uploaded for pull request base (master@4195ce3). Click here to learn what that means.
The diff coverage is 81.81%.

Impacted file tree graph

@@            Coverage Diff            @@
##             master      #12   +/-   ##
=========================================
  Coverage          ?   65.35%           
=========================================
  Files             ?        6           
  Lines             ?      482           
  Branches          ?        0           
=========================================
  Hits              ?      315           
  Misses            ?      167           
  Partials          ?        0
Flag Coverage Δ
#KituraContracts 65.35% <81.81%> (?)
Impacted Files Coverage Δ
Sources/KituraContracts/BodyFormat.swift 66.66% <66.66%> (ø)
Sources/KituraContracts/Contracts.swift 42.73% <85.18%> (ø)

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 4195ce3...71c8674. Read the comment docs.

@djones6 djones6 self-requested a review January 31, 2018 14:34
- Note: The code accessing this property is expected to know the
concrete type of this object and downcast, handling any
failures appropriately.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Where the client receives an error response and a RequestError is generated, how does the data off the wire get decoded into a Codable? The type is not known, so initially you have some Data. You may be able to add a function to which the user supplies the concrete type of Codable they expect to be able to decode from the error body (in lieu of making the RequestError generic).

}

/// Creates an error respresenting the given base error, with a custom
/// response body given as Data and a BodyFormat
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.

I need to add - throws to the doc here

@tunniclm tunniclm force-pushed the add_requesterror_body branch from 854ef08 to 9947ddb Compare February 2, 2018 15:30
@tunniclm tunniclm force-pushed the add_requesterror_body branch from 9947ddb to 60dc142 Compare February 2, 2018 15:32
@djones6
Copy link
Copy Markdown
Contributor

djones6 commented Feb 7, 2018

@tunniclm is this ready to merge?

@tunniclm
Copy link
Copy Markdown
Collaborator Author

tunniclm commented Feb 7, 2018

There's a few more requirements that have arisen from testing with KituraKit and talking with @EnriqueL8 / @seabaylea. I'd like to work on that a bit to work out the required changes.

return lhs.type == rhs.type
}

public static let json = BodyFormat("application/json")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we have a separate "registry" of MIME types somewhere that we could reuse?

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.

I added the type here as a convenience for a client (eg KituraKit) trying to tally the BodyFormat with a Content-Type header from a server response.

I did think about how to integrate it with a general approach for MIME/Content-types, but I don't think we have a really coherent story on that in Kitura (something I think we should fix). So I concluded that I shouldn't rathole on it in this feature/PR.

* limitations under the License.
**/

import Foundation
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this file needs to import Foundation.

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.

IIRC Foundation is required for the Data type

self.reason = reason
}

/// Creates an error respresenting the given base error, with a custom
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Typo - respresenting.

is not supported
*/
public func encodeBody(_ format: BodyFormat) throws -> Data? {
guard case .codable? = body else { return nil }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm confused. Doesn't this return nil if we're not .codable? Don't we want to return nil if we are .codable?

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.

As discussed in person, this function is for encoding the Codable body into Data, so the "source" of the body must be Codable rather than Data.
I will change the doc to "Returns the codable error body..." in the hope this clarifies the behaviour.

Side-note: I did consider a different API where the encode/decode was done transparently (ie you could just access the body as Codable or Data and it would work regardless of how it was initialized, but it didn't turn out well so I went for this less magic API instead).

- throws a `DecodingError` if decoding fails
*/
public func decodeBody<Body: Codable>(_ type: Body.Type) throws -> Body? {
guard case let .data(bodyData, format)? = body else { return nil }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same comment as before, isn't this the wrong way round?

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.

Similar response to the above, I will update the doc to "Returns the Data error body as..."


/// Representation of the error body
/// May be a type-erased Codable object or a Data (in a particular format)
public enum ErrorBody {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Did you consider using the struct approach again, just in case we ever want a third option?

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.

I thought about it, but I'm not sure it's necessary here. Unlike the BodyFormat case, the function seems fairly tightly defined and fitted to these two cases.

@ianpartridge ianpartridge merged commit 6419b27 into master Feb 21, 2018
@ianpartridge ianpartridge deleted the add_requesterror_body branch February 21, 2018 13:34
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