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.
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:
Option 1 is easier to set up — just make the model on the spot, and you’re done!
This works well for many projects. It has drawbacks, however:
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
configureTransformerYou can simplify your
ChallengeListTransformerby usingconfigureTransformer(...):But wait, there’s more! Watch as Swift’s amazing type inference slices and dices for you:
(Note that
configureTransformersetstransformErrorsto false. That is almost certainly what you want … unless your server sends a JSON “challenge” model as the body of an error response! ThetransformErrorsoption 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:
…and then:
…which further simplifies each route’s content transformer:
Note that Swift’s type inference tells Siesta that this transformer expects a
JSONstruct 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*/jsoncontent 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:
Alternatively, you can use the
TypedContentAccessorsprotocol 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: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….