Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the best way to map REST URL Patterns to Model Objects for the Siesta framework?

I'd like to use a ResponseTransformer (or a series of them) to automatically map my object model classes to the responses coming back from a Siesta service so that my Siesta resources are instances of my model classes. I have a working implementation for one class, but I'd like to know if there is a safer, smarter or more efficient way to do this before I build a separate ResponseTransformer for each type of resource (model).

Here is a sample model class:

import SwiftyJSON

class Challenge {
    var id:String?
    var name:String?

    init(fromDictionary:JSON) {
        if let challengeId = fromDictionary["id"].int {
            self.id = String(challengeId)
        }
        self.name = fromDictionary["name"].string
    }
}

extension Challenge {

    class func parseChallengeList(fromJSON:JSON) -> [Challenge] {
        var list = [Challenge]()

        switch fromJSON.type {
        case .Array:
            for itemDictionary in fromJSON.array! {
                let item = Challenge(fromDictionary: itemDictionary)
                list.append(item)
            }
        case .Dictionary:
            list.append(Challenge(fromDictionary: fromJSON))
        default: break
        }

        return list
    }
}

And here is the ResponseTransformer I built to map the response from any endpoint that returns either a collection of this model type or a single instance of this model type:

public func ChallengeListTransformer(transformErrors: Bool = true) -> ResponseTransformer {
    return ResponseContentTransformer(transformErrors: transformErrors)
        {
            (content: NSJSONConvertible, entity: Entity) throws -> [Challenge] in        
            let itemJSON = JSON(content)        
            return Challenge.parseChallengeList(itemJSON)
    }
}

And, finally, here is the URL Pattern mapping I am doing when I configure the Siesta service:

class _GFSFAPI: Service {

    ...

    configure("/Challenge/*")    { $0.config.responseTransformers.add(ChallengeListTransformer()) }
}

I am planning to build a separate ResponseTransformer for each model type, and then individually map each URL Pattern to that transformer. Is that the best approach? By the way, I am super excited about the new Siesta framework. I love the idea of a resource-oriented REST Networking Library.

like image 754
annicaburns Avatar asked Nov 05 '15 20:11

annicaburns


1 Answers

Your approach is solid! You’ve basically got it. There are a few things you can do to simplify the transformers.

Big Picture

Sounds like you already grasp this tradeoff, but for others who find this answer … you have two general approaches to choose from:

  1. construct your model object in your Siesta observer, or
  2. construct your model object in a transformer.

Option 1 is easier to set up — just make the model on the spot, and you’re done!

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = Challenge.parseChallengeList(
        JSON(resource.latestData?.jsonDict))
    ...
}

This works well for many projects. It has drawbacks, however:

  • Option 1 instantiates a new model object for every event multiplied by every observer; option 2 only instantiates the model object per “new data” response.
  • There’s no central place in option 1 that keeps track of which routes map to which model objects.
  • Option 2 gives better errors if the server doesn’t return the content type you expect.

I prefer option 1 if (and only if) the project is small and the models are lightweight.

You will find extensive documentation on option 2 in the Pipeline section of the user guide. Here is a quick overview.

Using configureTransformer

You can simplify your ChallengeListTransformer by using configureTransformer(...):

configureTransformer("/Challenge/*") {
    (content: NSJSONConvertible, entity: Entity) throws -> [Challenge] in        
    let itemJSON = JSON(content)        
    return Challenge.parseChallengeList(itemJSON)
}

But wait, there’s more! Watch as Swift’s amazing type inference slices and dices for you:

configureTransformer("/Challenge/*") {
    Challenge.parseChallengeList(
        JSON($0.content as NSJSONConvertible))
}

(Note that configureTransformer sets transformErrors to false. That is almost certainly what you want … unless your server sends a JSON “challenge” model as the body of an error response! The transformErrors option is typically only for general-purpose transformers like text and JSON parsing that are associated with a content-type, and not ones that are attached to a route.)

Global SwiftyJSON transformer

If you’re using SwiftyJSON (which I like too, BTW), then you can apply it en masse to all JSON responses:

private let SwiftyJSONTransformer =
    ResponseContentTransformer(skipWhenEntityMatchesOutputType: false)
        { JSON($0.content as AnyObject) }

…and then:

service.configure {
    $0.config.responseTransformers.add(
        SwiftyJSONTransformer, contentTypes: ["*/json"])
}

…which further simplifies each route’s content transformer:

configureTransformer("/Challenge/*") {
    Challenge.parseChallengeList($0.content)
}

Note that Swift’s type inference tells Siesta that this transformer expects a JSON struct as input, and Siesta uses that to flag it as an error if it didn’t come out of the transformer pipeline that way. The JSON-related transformers are all attached to */json content types, so if the server returns anything unexpected, your observers see a nice tidy “Hey, that’s not JSON!” error.

See the user guide for more in-depth info on all this.

Getting the Model from a Resource

As the Siesta API currently stands, you need to downcast the model’s content:

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = resource.latestData?.content as? [Challenge]
    ...
}

Alternatively, you can use the TypedContentAccessors protocol extension methods to simultaneously do the cast and grab a default value if either the data is not yet present or the cast fails. For example, this code defaults to an empty array if there are no challenges:

func resourceChanged(resource: Resource, event: ResourceEvent) {
    let challenges = resource.typedContent(ifNone: [Challenge]())
    ...
}

Siesta does not currently provide a statically typed way of tying a model type to a resource; you have to do the cast. This is because limitations of Swift’s type system prevent something a genericized resource type (e.g. Resource<[Challenge]>) from being workable in practice. Hopefully Swift 3 addresses those issues so that some future version of Siesta can provide that. Update: The necessary generics improvements are out for Swift 3, so hopefully in Swift 4….

like image 79
Paul Cantrell Avatar answered Sep 24 '22 06:09

Paul Cantrell