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.
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!
func resourceChanged(resource: Resource, event: ResourceEvent) {
let challenges = Challenge.parseChallengeList(
JSON(resource.latestData?.jsonDict))
...
}
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.
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.)
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.
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….
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With