Skip to content

Commit f88df37

Browse files
author
Wes Billman
committed
Added URLSession feed reader
1 parent 933472a commit f88df37

File tree

5 files changed

+231
-1
lines changed

5 files changed

+231
-1
lines changed

JSONFeed.xcodeproj/project.pbxproj

+25-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
CB3BE7F51ED0AF73008D884F /* timetable.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F41ED0AF73008D884F /* timetable.json */; };
2727
CB3BE7F71ED0B569008D884F /* simple.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F61ED0B569008D884F /* simple.json */; };
2828
CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */; };
29+
CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */; };
30+
CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */; };
31+
CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */; };
32+
CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */; };
2933
/* End PBXBuildFile section */
3034

3135
/* Begin PBXContainerItemProxy section */
@@ -61,6 +65,10 @@
6165
CB3BE7F41ED0AF73008D884F /* timetable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = timetable.json; sourceTree = "<group>"; };
6266
CB3BE7F61ED0B569008D884F /* simple.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = simple.json; sourceTree = "<group>"; };
6367
CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleFeedTests.swift; sourceTree = "<group>"; };
68+
CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReader.swift; sourceTree = "<group>"; };
69+
CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReaderTests.swift; sourceTree = "<group>"; };
70+
CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
71+
CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = "<group>"; };
6472
/* End PBXFileReference section */
6573

6674
/* Begin PBXFrameworksBuildPhase section */
@@ -111,6 +119,7 @@
111119
CB3BE7E91ECFB7DC008D884F /* Item.swift */,
112120
CB3BE7DA1ECF593B008D884F /* JSONFeed.swift */,
113121
CB3BE7DB1ECF593B008D884F /* JSONFeedError.swift */,
122+
CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */,
114123
CB3BE7D71ECF593B008D884F /* Extensions */,
115124
);
116125
path = JSONFeed;
@@ -119,14 +128,16 @@
119128
CB3BE7C91ECF58E9008D884F /* JSONFeedTests */ = {
120129
isa = PBXGroup;
121130
children = (
122-
CB3BE7F11ED0AB1C008D884F /* Feeds */,
123131
CB3BE7CC1ECF58E9008D884F /* Info.plist */,
124132
CB3BE7ED1ECFBF9E008D884F /* AttachmentTests.swift */,
125133
CB3BE7E41ECFA4C8008D884F /* AuthorTests.swift */,
126134
CB3BE7E21ECFA409008D884F /* HubTests.swift */,
127135
CB3BE7EF1ECFC124008D884F /* ItemTests.swift */,
136+
CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */,
128137
CB3BE7CA1ECF58E9008D884F /* JSONFeedTests.swift */,
129138
CB3BE7E61ECFAA43008D884F /* Extensions */,
139+
CB3BE7F11ED0AB1C008D884F /* Feeds */,
140+
CB3BE8001ED0BFF3008D884F /* Mocks */,
130141
);
131142
path = JSONFeedTests;
132143
sourceTree = "<group>";
@@ -158,6 +169,15 @@
158169
path = Feeds;
159170
sourceTree = "<group>";
160171
};
172+
CB3BE8001ED0BFF3008D884F /* Mocks */ = {
173+
isa = PBXGroup;
174+
children = (
175+
CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */,
176+
CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */,
177+
);
178+
path = Mocks;
179+
sourceTree = "<group>";
180+
};
161181
/* End PBXGroup section */
162182

163183
/* Begin PBXHeadersBuildPhase section */
@@ -289,6 +309,7 @@
289309
isa = PBXSourcesBuildPhase;
290310
buildActionMask = 2147483647;
291311
files = (
312+
CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */,
292313
CB3BE7DC1ECF593B008D884F /* Author.swift in Sources */,
293314
CB3BE7EC1ECFB8EB008D884F /* Attachment.swift in Sources */,
294315
CB3BE7DD1ECF593B008D884F /* URLExtensions.swift in Sources */,
@@ -304,12 +325,15 @@
304325
buildActionMask = 2147483647;
305326
files = (
306327
CB3BE7CB1ECF58E9008D884F /* JSONFeedTests.swift in Sources */,
328+
CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */,
307329
CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */,
308330
CB3BE7E81ECFAA53008D884F /* URLExtensionsTests.swift in Sources */,
309331
CB3BE7F01ECFC124008D884F /* ItemTests.swift in Sources */,
332+
CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */,
310333
CB3BE7F31ED0ADB5008D884F /* TimetableFeedTests.swift in Sources */,
311334
CB3BE7E31ECFA409008D884F /* HubTests.swift in Sources */,
312335
CB3BE7E51ECFA4C8008D884F /* AuthorTests.swift in Sources */,
336+
CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */,
313337
CB3BE7EE1ECFBF9E008D884F /* AttachmentTests.swift in Sources */,
314338
);
315339
runOnlyForDeploymentPostprocessing = 0;

JSONFeed/JSONFeedReader.swift

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
//
2+
// Created by Wes Billman on 5/20/17.
3+
// Copyright © 2017 wesbillman. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
public enum JSONFeedReaderError: Error {
9+
case invalidRequestString
10+
case emptyResponseData
11+
}
12+
13+
public class JSONFeedReader {
14+
private let session: URLSession
15+
16+
init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default)) {
17+
self.session = session
18+
}
19+
20+
public func read(url: URL, complete: @escaping (JSONFeed?, Error?) -> Void) {
21+
let task = session.dataTask(with: URLRequest(url: url)) { (data, _, error) in
22+
if let error = error {
23+
complete(nil, error)
24+
return
25+
}
26+
guard let data = data else {
27+
complete(nil, JSONFeedReaderError.emptyResponseData)
28+
return
29+
}
30+
do {
31+
let feed = try JSONFeed(data: data)
32+
complete(feed, nil)
33+
} catch {
34+
complete(nil, error)
35+
}
36+
}
37+
task.resume()
38+
}
39+
40+
public func read(string: String, complete: @escaping (JSONFeed?, Error?) -> Void) {
41+
guard let url = URL(string: string) else {
42+
complete(nil, JSONFeedReaderError.invalidRequestString)
43+
return
44+
}
45+
read(url: url, complete: complete)
46+
}
47+
}
+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//
2+
// Created by Wes Billman on 5/20/17.
3+
// Copyright © 2017 wesbillman. All rights reserved.
4+
//
5+
6+
import XCTest
7+
@testable import JSONFeed
8+
9+
class JSONFeedReaderTests: XCTestCase {
10+
enum TestError: Error {
11+
case sessionError
12+
}
13+
14+
private let url = "https://jsonfeed.org/version/1"
15+
private let text = "Some Text"
16+
17+
private var session: MockURLSession!
18+
private var subject: JSONFeedReader!
19+
20+
var validData: Data {
21+
let json = ["version": url, "title": text]
22+
guard let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) else {
23+
return Data()
24+
}
25+
return data
26+
}
27+
28+
override func setUp() {
29+
super.setUp()
30+
session = MockURLSession()
31+
subject = JSONFeedReader(session: session)
32+
}
33+
34+
func testInvalidURLString() {
35+
var resultFeed: JSONFeed? = nil
36+
var resultError: Error? = nil
37+
let nilString: String? = nil
38+
39+
subject.read(string: "bogus\\\(String(describing: nilString))") { (feed, error) in
40+
resultFeed = feed
41+
resultError = error
42+
}
43+
XCTAssert(!session.task.resumed)
44+
XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.invalidRequestString)
45+
XCTAssertNil(resultFeed)
46+
}
47+
48+
func testSessionError() {
49+
session.error = TestError.sessionError
50+
var resultFeed: JSONFeed? = nil
51+
var resultError: Error? = nil
52+
53+
subject.read(string: "example.com") { (feed, error) in
54+
resultFeed = feed
55+
resultError = error
56+
}
57+
XCTAssert(session.task.resumed)
58+
XCTAssertEqual(resultError as? TestError, TestError.sessionError)
59+
XCTAssertNil(resultFeed)
60+
}
61+
62+
func testNilData() {
63+
session.data = nil
64+
var resultFeed: JSONFeed? = nil
65+
var resultError: Error? = nil
66+
67+
subject.read(string: "example.com") { (feed, error) in
68+
resultFeed = feed
69+
resultError = error
70+
}
71+
XCTAssert(session.task.resumed)
72+
XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.emptyResponseData)
73+
XCTAssertNil(resultFeed)
74+
}
75+
76+
func testInvalidData() {
77+
guard let data = "bogus".data(using: .utf8) else {
78+
XCTFail()
79+
return
80+
}
81+
session.data = data
82+
var resultFeed: JSONFeed? = nil
83+
var resultError: Error? = nil
84+
85+
subject.read(string: "example.com") { (feed, error) in
86+
resultFeed = feed
87+
resultError = error
88+
}
89+
XCTAssert(session.task.resumed)
90+
XCTAssertEqual(resultError as? JSONFeedError, JSONFeedError.invalidData)
91+
XCTAssertNil(resultFeed)
92+
}
93+
94+
func testReadingFeedURL() {
95+
session.data = validData
96+
var resultFeed: JSONFeed? = nil
97+
var resultError: Error? = nil
98+
99+
guard let feedUrl = URL(string: "example.com") else {
100+
XCTFail("unable to create url")
101+
return
102+
}
103+
104+
subject.read(url: feedUrl) { (feed, error) in
105+
resultFeed = feed
106+
resultError = error
107+
}
108+
XCTAssert(session.task.resumed)
109+
XCTAssertEqual(resultFeed?.version.absoluteString, url)
110+
XCTAssertEqual(resultFeed?.title, text)
111+
XCTAssertNil(resultError)
112+
}
113+
114+
func testReadingFeedURLString() {
115+
session.data = validData
116+
var resultFeed: JSONFeed? = nil
117+
var resultError: Error? = nil
118+
119+
subject.read(string: "example.com") { (feed, error) in
120+
resultFeed = feed
121+
resultError = error
122+
}
123+
XCTAssert(session.task.resumed)
124+
XCTAssertEqual(resultFeed?.version.absoluteString, url)
125+
XCTAssertEqual(resultFeed?.title, text)
126+
XCTAssertNil(resultError)
127+
}
128+
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// Created by Wes Billman on 5/20/17.
3+
// Copyright © 2017 wesbillman. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
class MockURLSession: URLSession {
9+
let task = MockURLSessionDataTask()
10+
var data: Data?
11+
var response: URLResponse?
12+
var error: Error?
13+
14+
override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
15+
completionHandler(data, response, error)
16+
return task
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// Created by Wes Billman on 5/20/17.
3+
// Copyright © 2017 wesbillman. All rights reserved.
4+
//
5+
6+
import Foundation
7+
8+
class MockURLSessionDataTask: URLSessionDataTask {
9+
private(set) var resumed = false
10+
override func resume() {
11+
resumed = true
12+
}
13+
}

0 commit comments

Comments
 (0)