How to Decorate Siesta Request with an Asynchronous Task

How to decorate Siesta request with an asynchronous task

Yes, implementing Siesta’s Request interface is no picnic. Others have had exactly the same problem — and luckily Siesta version 1.4 includes a solution.

Documentation for the new feature is still thin. To use the new API, you’ll implement the new RequestDelegate protocol, and pass your implementation to Resource.prepareRequest(using:). That will return a request that you can use in a standard Siesta request chain. The result will look something like this (WARNING – untested code):

struct MyTokenHandlerThingy: RequestDelegate {
// 3rd party SDK glue goes here
}

...

service.configure(…) {
if let authToken = self.authToken {
$0.headers["X-Auth-Token"] = authToken // authToken is an instance var or something
}

$0.decorateRequests {
self.refreshTokenOnAuthFailure(request: $1)
}
}

func refreshTokenOnAuthFailure(request: Request) -> Request {
return request.chained {
guard case .failure(let error) = $0.response, // Did request fail…
error.httpStatusCode == 401 else { // …because of expired token?
return .useThisResponse // If not, use the response we got.
}

return .passTo(
self.refreshAuthToken().chained { // If so, first request a new token, then:
if case .failure = $0.response { // If token request failed…
return .useThisResponse // …report that error.
} else {
return .passTo(request.repeated()) // We have a new token! Repeat the original request.
}
}
)
}
}

func refreshAuthToken() -> Request {
return Request.prepareRequest(using: MyTokenHandlerThingy())
.onSuccess {
self.authToken = $0.jsonDict["token"] as? String // Store the new token, then…
self.invalidateConfiguration() // …make future requests use it
}
}
}

To understand how to implement RequestDelegate, you best bet for now is to look at the new API docs directly in the code.

Since this is a brand new feature not yet released, I’d greatly appreciate a report on how it works for you and any troubles you encounter.

Swift Siesta - How to include asynchronous code into a request chain?

First off, you should rephrase the main thrust of your question so it's not Firebase-specific, along the lines of "How do I do request chaining with some arbitrary asynchronous code instead of a request?". It will be much more useful to the community that way. Then you can mention that Firebase auth is your specific use case. I'm going to answer your question accordingly.

(Edit: Having answered this question, I now see that Paul had already answered it here: How to decorate Siesta request with an asynchronous task)

Siesta's RequestDelegate does what you're looking for. To quote the docs: "This is useful for taking things that are not standard network requests, and wrapping them so they look to Siesta as if they are. To create a custom request, pass your delegate to Resource.prepareRequest(using:)."

You might use something like this as a rough starting point - it runs a closure (the auth call in your case) that either succeeds with no output or returns an error. Depending on use, you might adapt it to populate the entity with actual content.

// todo better name
class SiestaPseudoRequest: RequestDelegate {
private let op: (@escaping (Error?) -> Void) -> Void

init(op: @escaping (@escaping (Error?) -> Void) -> Void) {
self.op = op
}

func startUnderlyingOperation(passingResponseTo completionHandler: RequestCompletionHandler) {
op {
if let error = $0 {
// todo better
let reqError = RequestError(response: nil, content: nil, cause: error, userMessage: nil)
completionHandler.broadcastResponse(ResponseInfo(response: .failure(reqError)))
}
else {
// todo you might well produce output at this point
let ent = Entity<Any>(content: "", contentType: "text/plain")
completionHandler.broadcastResponse(ResponseInfo(response: .success(ent)))
}
}
}

func cancelUnderlyingOperation() {}

func repeated() -> RequestDelegate { SiestaPseudoRequest(op: op) }

// todo better
private(set) var requestDescription: String = "SiestaPseudoRequest"
}

One catch I found with this is that response transformers aren't run for such "requests" - the transformer pipeline is specific to Siesta's NetworkRequest. (This took me by surprise and I'm not sure that I like it, but Siesta seems to be generally full of good decisions, so I'm mostly taking it on faith that there's a good reason for it.)

It might be worth watching out for other non request-like behaviour.

How to override default (**) decorator for certain resource in siesta?

You can pass an arbitrary predicate to configure(whenURLMatches:), which lets you surgically exclude whatever you like:

service.configure(whenURLMatches: { url in url.path != "/auth" }) {
...
}

Or if, as in your case, you want to exclude a URL for which you have a Resource handy:

service.configure(whenURLMatches: { $0 != authRefreshResource.url }) {
...
}

Using Siesta with XML

Yes, you’re on the right track. "*" only matches a single path segment; try "**" instead.

Displaying networking error message to user in Swift

If the error messages are ViewController specific you can start with creating a function that returns the message based on the status code like this:

private func getErrorMessageFor(statusCode: Int) -> String? {
if (200...299).contains(statusCode) {
//If no error message is returned assume that the request was a success
return nil
} else if (400...499).contains(statusCode) {
return "Please make sure you filled in the all the required fields."
} else if (500...599).contains(statusCode) {
return "Sorry, couldn't reach our server."
} else if (700...).contains(statusCode) {
return "Sorry, something went wrong. Try again later."
} else {
return "Message for other errors?"
}
}

You can always move this code to a ViewController subclass to provide more generic error messages and override it later to provide more detailed errors for a specific View Controller.

class BaseViewController: UIViewController {
func getErrorMessageFor(statusCode: Int) -> String? {
//base implementation here
}
}

class OtherViewController: BaseViewController {
override func getErrorMessageFor(statusCode: Int) -> String? {
//create a new error message only for statusCode 404
if statusCode == 404 {
return "The requested resource was not found on the server. Please contact the support team"
} else {
return super.getErrorMessageFor(statusCode: statusCode)
}
}
}

Keep in mind that as your app grows you might want to create an APIClient that would handle networking and error handling for you. Take a look at https://bustoutsolutions.github.io/siesta/, it is very user friendly

ASP.NET MVC: Controlling serialization of property names with JsonResult

I wanted something a bit more baked into the framework than what Jarrett suggested, so here's what I did:

JsonDataContractActionResult:

public class JsonDataContractActionResult : ActionResult
{
public JsonDataContractActionResult(Object data)
{
this.Data = data;
}

public Object Data { get; private set; }

public override void ExecuteResult(ControllerContext context)
{
var serializer = new DataContractJsonSerializer(this.Data.GetType());
String output = String.Empty;
using (var ms = new MemoryStream())
{
serializer.WriteObject(ms, this.Data);
output = Encoding.Default.GetString(ms.ToArray());
}
context.HttpContext.Response.ContentType = "application/json";
context.HttpContext.Response.Write(output);
}
}

JsonContract() method, added to my base controller class:

    public ActionResult JsonContract(Object data)
{
return new JsonDataContractActionResult(data);
}

Sample Usage:

    public ActionResult Update(String id, [Bind(Exclude="Id")] Advertiser advertiser)
{
Int32 advertiserId;
if (Int32.TryParse(id, out advertiserId))
{
// update
}
else
{
// insert
}

return JsonContract(advertiser);
}

Note: If you're looking for something more performant than JsonDataContractSerializer, you can do the same thing using JSON.NET instead. While JSON.NET doesn't appear to utilize DataMemberAttribute, it does have its own JsonPropertyAttribute which can be used to accomplish the same thing.



Related Topics



Leave a reply



Submit