Skip to content

Commit

Permalink
Added URLSession feed reader
Browse files Browse the repository at this point in the history
  • Loading branch information
Wes Billman committed May 20, 2017
1 parent 933472a commit f88df37
Show file tree
Hide file tree
Showing 5 changed files with 231 additions and 1 deletion.
26 changes: 25 additions & 1 deletion JSONFeed.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
CB3BE7F51ED0AF73008D884F /* timetable.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F41ED0AF73008D884F /* timetable.json */; };
CB3BE7F71ED0B569008D884F /* simple.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F61ED0B569008D884F /* simple.json */; };
CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */; };
CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */; };
CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */; };
CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */; };
CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -61,6 +65,10 @@
CB3BE7F41ED0AF73008D884F /* timetable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = timetable.json; sourceTree = "<group>"; };
CB3BE7F61ED0B569008D884F /* simple.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = simple.json; sourceTree = "<group>"; };
CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleFeedTests.swift; sourceTree = "<group>"; };
CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReader.swift; sourceTree = "<group>"; };
CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReaderTests.swift; sourceTree = "<group>"; };
CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -111,6 +119,7 @@
CB3BE7E91ECFB7DC008D884F /* Item.swift */,
CB3BE7DA1ECF593B008D884F /* JSONFeed.swift */,
CB3BE7DB1ECF593B008D884F /* JSONFeedError.swift */,
CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */,
CB3BE7D71ECF593B008D884F /* Extensions */,
);
path = JSONFeed;
Expand All @@ -119,14 +128,16 @@
CB3BE7C91ECF58E9008D884F /* JSONFeedTests */ = {
isa = PBXGroup;
children = (
CB3BE7F11ED0AB1C008D884F /* Feeds */,
CB3BE7CC1ECF58E9008D884F /* Info.plist */,
CB3BE7ED1ECFBF9E008D884F /* AttachmentTests.swift */,
CB3BE7E41ECFA4C8008D884F /* AuthorTests.swift */,
CB3BE7E21ECFA409008D884F /* HubTests.swift */,
CB3BE7EF1ECFC124008D884F /* ItemTests.swift */,
CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */,
CB3BE7CA1ECF58E9008D884F /* JSONFeedTests.swift */,
CB3BE7E61ECFAA43008D884F /* Extensions */,
CB3BE7F11ED0AB1C008D884F /* Feeds */,
CB3BE8001ED0BFF3008D884F /* Mocks */,
);
path = JSONFeedTests;
sourceTree = "<group>";
Expand Down Expand Up @@ -158,6 +169,15 @@
path = Feeds;
sourceTree = "<group>";
};
CB3BE8001ED0BFF3008D884F /* Mocks */ = {
isa = PBXGroup;
children = (
CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */,
CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */,
);
path = Mocks;
sourceTree = "<group>";
};
/* End PBXGroup section */

/* Begin PBXHeadersBuildPhase section */
Expand Down Expand Up @@ -289,6 +309,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */,
CB3BE7DC1ECF593B008D884F /* Author.swift in Sources */,
CB3BE7EC1ECFB8EB008D884F /* Attachment.swift in Sources */,
CB3BE7DD1ECF593B008D884F /* URLExtensions.swift in Sources */,
Expand All @@ -304,12 +325,15 @@
buildActionMask = 2147483647;
files = (
CB3BE7CB1ECF58E9008D884F /* JSONFeedTests.swift in Sources */,
CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */,
CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */,
CB3BE7E81ECFAA53008D884F /* URLExtensionsTests.swift in Sources */,
CB3BE7F01ECFC124008D884F /* ItemTests.swift in Sources */,
CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */,
CB3BE7F31ED0ADB5008D884F /* TimetableFeedTests.swift in Sources */,
CB3BE7E31ECFA409008D884F /* HubTests.swift in Sources */,
CB3BE7E51ECFA4C8008D884F /* AuthorTests.swift in Sources */,
CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */,
CB3BE7EE1ECFBF9E008D884F /* AttachmentTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
47 changes: 47 additions & 0 deletions JSONFeed/JSONFeedReader.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// Created by Wes Billman on 5/20/17.
// Copyright © 2017 wesbillman. All rights reserved.
//

import Foundation

public enum JSONFeedReaderError: Error {
case invalidRequestString
case emptyResponseData
}

public class JSONFeedReader {
private let session: URLSession

init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default)) {
self.session = session
}

public func read(url: URL, complete: @escaping (JSONFeed?, Error?) -> Void) {
let task = session.dataTask(with: URLRequest(url: url)) { (data, _, error) in
if let error = error {
complete(nil, error)
return
}
guard let data = data else {
complete(nil, JSONFeedReaderError.emptyResponseData)
return
}
do {
let feed = try JSONFeed(data: data)
complete(feed, nil)
} catch {
complete(nil, error)
}
}
task.resume()
}

public func read(string: String, complete: @escaping (JSONFeed?, Error?) -> Void) {
guard let url = URL(string: string) else {
complete(nil, JSONFeedReaderError.invalidRequestString)
return
}
read(url: url, complete: complete)
}
}
128 changes: 128 additions & 0 deletions JSONFeedTests/JSONFeedReaderTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
//
// Created by Wes Billman on 5/20/17.
// Copyright © 2017 wesbillman. All rights reserved.
//

import XCTest
@testable import JSONFeed

class JSONFeedReaderTests: XCTestCase {
enum TestError: Error {
case sessionError
}

private let url = "https://jsonfeed.org/version/1"
private let text = "Some Text"

private var session: MockURLSession!
private var subject: JSONFeedReader!

var validData: Data {
let json = ["version": url, "title": text]
guard let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) else {
return Data()
}
return data
}

override func setUp() {
super.setUp()
session = MockURLSession()
subject = JSONFeedReader(session: session)
}

func testInvalidURLString() {
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil
let nilString: String? = nil

subject.read(string: "bogus\\\(String(describing: nilString))") { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(!session.task.resumed)
XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.invalidRequestString)
XCTAssertNil(resultFeed)
}

func testSessionError() {
session.error = TestError.sessionError
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil

subject.read(string: "example.com") { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(session.task.resumed)
XCTAssertEqual(resultError as? TestError, TestError.sessionError)
XCTAssertNil(resultFeed)
}

func testNilData() {
session.data = nil
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil

subject.read(string: "example.com") { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(session.task.resumed)
XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.emptyResponseData)
XCTAssertNil(resultFeed)
}

func testInvalidData() {
guard let data = "bogus".data(using: .utf8) else {
XCTFail()
return
}
session.data = data
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil

subject.read(string: "example.com") { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(session.task.resumed)
XCTAssertEqual(resultError as? JSONFeedError, JSONFeedError.invalidData)
XCTAssertNil(resultFeed)
}

func testReadingFeedURL() {
session.data = validData
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil

guard let feedUrl = URL(string: "example.com") else {
XCTFail("unable to create url")
return
}

subject.read(url: feedUrl) { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(session.task.resumed)
XCTAssertEqual(resultFeed?.version.absoluteString, url)
XCTAssertEqual(resultFeed?.title, text)
XCTAssertNil(resultError)
}

func testReadingFeedURLString() {
session.data = validData
var resultFeed: JSONFeed? = nil
var resultError: Error? = nil

subject.read(string: "example.com") { (feed, error) in
resultFeed = feed
resultError = error
}
XCTAssert(session.task.resumed)
XCTAssertEqual(resultFeed?.version.absoluteString, url)
XCTAssertEqual(resultFeed?.title, text)
XCTAssertNil(resultError)
}
}
18 changes: 18 additions & 0 deletions JSONFeedTests/Mocks/MockURLSession.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//
// Created by Wes Billman on 5/20/17.
// Copyright © 2017 wesbillman. All rights reserved.
//

import Foundation

class MockURLSession: URLSession {
let task = MockURLSessionDataTask()
var data: Data?
var response: URLResponse?
var error: Error?

override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
completionHandler(data, response, error)
return task
}
}
13 changes: 13 additions & 0 deletions JSONFeedTests/Mocks/MockURLSessionDataTask.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Created by Wes Billman on 5/20/17.
// Copyright © 2017 wesbillman. All rights reserved.
//

import Foundation

class MockURLSessionDataTask: URLSessionDataTask {
private(set) var resumed = false
override func resume() {
resumed = true
}
}

0 comments on commit f88df37

Please sign in to comment.