In this episode we start writing an application-specific API Client. We use Argo to decode our JSON response into an Episode model, including some nested object decoding, date formatting, and wrap it up by testing the implementation to make sure it works.
Episode Links Source Code Argo Creating an Episode Model Our Episode model will be a simple Swift class that has properties that map from the API response. class Episode { var episodeId: Int? var title: String? var number: Int? var duration: Int? var dominantColor: String? var largeImageUrl: NSURL? var mediumImageUrl: NSURL? var thumbnailImageUrl: NSURL? var videoUrl: NSURL? var hlsUrl: NSURL? var episodeDescription: String? var episodeType: EpisodeType? var publishedAt: NSDate? var showNotes: String? } This is pretty straightforward. Here I'm allowing any of these parameters to be nil, but you might choose to consider a model invalid if any required parameters are nil. We also need to define the EpisodeType type: enum EpisodeType : String { case Free = "free" case Paid = "paid" } There might be other episode types in the future, and I might also choose to add some logic related to the type, so an enum is a good choice here, rather than just a string. Adding Argo Support Next we need to add Argo support for our model. I prefer to do this in another file and extend the type that way, so it's easy to read, and if I want to change it later it will be all contained in one place. I'll create a new file called Episode+Decodable.swift and define the parsing logic there. For now we'll just return the simplest thing that we can, which is a simple decode error indicating that something went wrong. Argo uses this return type to better communicate why an object cannot be parsed, which is extremely helpful in diagnosing problems. import Foundation import Argo extension Episode : Decodable { typealias DecodedType = Episode static func decode(json: JSON) -> Decoded<Episode> { return Decoded.Failure(DecodeError.Custom("not implemented")) } } Implementing our own Api Client Now we can turn our attention to the API client specific to our application. We'll start with a simple case, fetching an episode by ID: class NSScreencastApiClient : ApiClient { func fetchEpisode(id: Int, completion: ApiClientResult<Episode> -> Void) { completion(ApiClientResult.NotFound) } } Now that we have the most minimal (albeit broken) implementation, we can write a test to see if we get the expected error. import Foundation import XCTest @testable import nsscreencast_tvdemo class NSScreencastApiClientTests : XCTestCase { var apiClient: NSScreencastApiClient! override func setUp() { apiClient = NSScreencastApiClient(configuration: NSURLSessionConfiguration.defaultSessionConfiguration()) } func testFetchSingleEpisode() { let expectation = expectationWithDescription("response received") apiClient.fetchEpisode(1) { result in switch result { case .Success(_): break default: XCTFail() } } waitForExpectationsWithTimeout(3, handler: nil) } } We also want to set up our own sesison configuration so we can supply common request headers to be sent with our requests. For this I'll create a singleton property we can access from anywhere. static var sharedApiClient: NSScreencastApiClient = { let config = NSURLSessionConfiguration.defaultSessionConfiguration() config.HTTPAdditionalHeaders = [ "Content-Type" : "application/json", "Accept" : "application/json" ] return NSScreencastApiClient(configuration: config) }() Decodable Implementation for Episode Going back to Episode+Decodable.swift we can write our implementation: extension Episode : Decodable { public static func decode(json: JSON) -> Decoded<Episode> { let episode = Episode() episode.episodeId = (json <| "id").value episode.title = (json <| "title").value episode.number = (json <| "episode_number").value episode.dominantColor = (json <| "dominant_color").value episode.largeImageUrl = (json <| "large_artwork_url").value episode.mediumImageUrl = (json <| "medium_artwork_url").value episode.thumbnailImageUrl = (json <| "small_artwork_url").value episode.videoUrl = (json <| "video_url").value episode.hlsUrl = (json <| "hls_url").value episode.episodeType = (json <| "episode_type").value episode.episodeDescription = (json <| "description").value episode.showNotes = (json <| "show_notes").value episode.duration = (json <| "duration").value return .Success(episode) } } Parsing NSURLs Parsing an NSURL is= easy, but since no decodable implementation is builtin, we can provide our own: extension NSURL: Decodable { public typealias DecodedType = NSURL public class func decode(j: JSON) -> Decoded<NSURL> { switch j { case .String(let urlString): if let url = NSURL(string: urlString) { return .Success(url) } else { return .typeMismatch("URL", actual: j) } default: return .typeMismatch("URL", actual: j) } } } Note that the if let ... else could be simplified with some functional voodoo: return NSURL(string: urlString).map(pure) ?? .typeMismatch("URL", actual: j) The idea here is that you're mapping an optional value onto the pure function, which wraps the value in a Success. If the value is nil, then map returns nil, in which case the right side of the ?? operator is returned. While a neat one-liner, I find the former a bit more readable. Supporting ISO8601 Format to NSDate Parsing a string into an NSDate isn't as straightforward as NSURL. We could provide a Decodable implementation for it, but that would assume a single date format. Instead we'll have a function we can use to do the parsing for us: class DateHelper { static var dateFormatterISO8601: NSDateFormatter = { let formatter = NSDateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ" return formatter }() class func parseDateISO8601(dateString: String) -> Decoded<NSDate> { if let date = dateFormatterISO8601.dateFromString(dateString) { return Decoded.Success(date) } return Decoded.Success(NSDate()) } } Then we can use it when decoding as needed: episode.publishedAt = (json <| "published_at" >>- DateHelper.parseDateISO8601).value Working inside out here, parseDateISO8601 takes a String and returns a Decoded<NSDate>, so that gives the <| operator the information it needs to treat the value as a string first, then apply it to the provided function. the >>- operator does this for us. With this in hand we can easily use multiple date formats without being tied down to just one. Back to the API Client Now, back on the API client, we can write our request & send it off to complete the example and run our test. func fetchEpisodeDetail(id: Int, completion: ApiClientResult<Episode> -> ()) { let url = ApiEndpoints.Episode(id).url() let request = buildRequest("GET", url: url, params: nil) fetchResource(request, rootKey: "episode", completion: completion) } Here we're using the enum technique for modeling endpoints we looked at in Episode 194. enum Endpoints { case Episode(Int) var url: NSURL { let path: String switch self { case .Episode(let id): path = "/episodes/\(id)" } return NSURL(string: NSScreencastApiClient.baseURL + path)! } } Adding some Assertions Finally, we can go back to our test and add a couple assertions to make sure that things are working as expected. func testFetchSingleEpisode() { let expectation = expectationWithDescription("api response received") apiClient.fetchEpisode(1) { result in expectation.fulfill() switch result { case .Success(let episode): XCTAssertEqual(1, episode.episodeId) XCTAssertEqual(1, episode.number) break default: XCTFail() } } waitForExpectationsWithTimeout(3, handler: nil) } We run the test, and it passes! Yay!