annicaburns annicaburns - 5 months ago 15
iOS Question

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.

Answer

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 most projects. Its disadvantages are twofold:

  • 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.

Using configureTransformer

As of Siesta 1.0b3 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.

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….