Skip to content

Commit

Permalink
Bug 1378997 - Backend API fetching for Pocket Trending stories. (#2984)…
Browse files Browse the repository at this point in the history
… r=garvan

* Bug 1378997 - Backend API fetching for Pocket Trending stories.

* Rename a few things and clean up the API usage.
  • Loading branch information
farhanpatel committed Aug 2, 2017
1 parent b7d2697 commit 24e215f
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 0 deletions.
12 changes: 12 additions & 0 deletions Client.xcodeproj/project.pbxproj
Expand Up @@ -265,6 +265,9 @@
3B4988CE1E42B01800A12FDA /* SwiftyJSON.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B4988CD1E42B01800A12FDA /* SwiftyJSON.framework */; };
3B4AA24B1D8B8C4C00A2E008 /* ArrayExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B4AA24A1D8B8C4C00A2E008 /* ArrayExtensionTests.swift */; };
3B546EC01D95ECAE00BDBE36 /* ActivityStreamTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B546EBF1D95ECAE00BDBE36 /* ActivityStreamTest.swift */; };
3B61CD491F2A74EF00D38DE1 /* PocketFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */; };
3B61CD591F2A750800D38DE1 /* PocketFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */; };
3B61CD631F2A769D00D38DE1 /* pocketglobalfeed.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */; };
3B6889C51D66950E002AC85E /* UIImageColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6889C41D66950E002AC85E /* UIImageColors.swift */; };
3B6F40181DC7849C00656CC6 /* ActivityStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B6F40171DC7849C00656CC6 /* ActivityStreamTests.swift */; };
3BA9A0231D2C208C00BD418C /* Fuzi.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3BA9A0221D2C208C00BD418C /* Fuzi.framework */; };
Expand Down Expand Up @@ -1529,6 +1532,9 @@
3B4988CD1E42B01800A12FDA /* SwiftyJSON.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftyJSON.framework; path = Carthage/Build/iOS/SwiftyJSON.framework; sourceTree = "<group>"; };
3B4AA24A1D8B8C4C00A2E008 /* ArrayExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayExtensionTests.swift; sourceTree = "<group>"; };
3B546EBF1D95ECAE00BDBE36 /* ActivityStreamTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTest.swift; sourceTree = "<group>"; };
3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketFeed.swift; sourceTree = "<group>"; };
3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PocketFeedTests.swift; sourceTree = "<group>"; };
3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = pocketglobalfeed.json; sourceTree = "<group>"; };
3B6889C41D66950E002AC85E /* UIImageColors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = UIImageColors.swift; path = ThirdParty/UIImageColors.swift; sourceTree = "<group>"; };
3B6F40171DC7849C00656CC6 /* ActivityStreamTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActivityStreamTests.swift; sourceTree = "<group>"; };
3BA9A0221D2C208C00BD418C /* Fuzi.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Fuzi.framework; path = Carthage/Build/iOS/Fuzi.framework; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2957,6 +2963,7 @@
isa = PBXGroup;
children = (
E68F36971EA694000048CF44 /* PanelDataObservers.swift */,
3B61CD481F2A74EF00D38DE1 /* PocketFeed.swift */,
E60D03171D511398002FE3F6 /* SyncStatusResolver.swift */,
D34DC84D1A16C40C00D49B7B /* Profile.swift */,
0BD19A661A25309B0084FBA7 /* NSUserDefaultsPrefs.swift */,
Expand Down Expand Up @@ -3582,6 +3589,7 @@
E683F0A51E92E0820035D990 /* MockableHistory.swift */,
E61D11671EAF8F43008A305B /* PanelDataObserversTests.swift */,
E6C70E811E28314700F8DB57 /* PingCentreTests.swift */,
3B61CD581F2A750800D38DE1 /* PocketFeedTests.swift */,
2FDB10921A9FBEC5006CF312 /* PrefsTests.swift */,
0BA896491A250E6500C1010C /* ProfileTest.swift */,
03CCC9171AF05E7300DBF30D /* RelativeDatesTests.swift */,
Expand Down Expand Up @@ -3611,6 +3619,7 @@
A83E5B181C1DA8BF0026D912 /* image.gif */,
F84B21D81A090F8100AAB793 /* Info.plist */,
A83E5B191C1DA8BF0026D912 /* image.png */,
3B61CD621F2A769D00D38DE1 /* pocketglobalfeed.json */,
);
name = "Supporting Files";
sourceTree = "<group>";
Expand Down Expand Up @@ -4911,6 +4920,7 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
3B61CD631F2A769D00D38DE1 /* pocketglobalfeed.json in Resources */,
D38A1BF11A9FA2CA00F6A386 /* SiteTableViewControllerHeader.xib in Resources */,
A83E5B1A1C1DA8BF0026D912 /* image.gif in Resources */,
A83E5B1B1C1DA8BF0026D912 /* image.png in Resources */,
Expand Down Expand Up @@ -5472,6 +5482,7 @@
7B3631EA1C244FEE00D12AF9 /* Theme.swift in Sources */,
7BC68D321CC153920043562A /* AppMenuItem.swift in Sources */,
7BC68CCD1CC152B70043562A /* MenuView.swift in Sources */,
3B61CD491F2A74EF00D38DE1 /* PocketFeed.swift in Sources */,
396E38F11EE0C8EC00CC180F /* FxAPushMessageHandler.swift in Sources */,
E66C5B481BDA81050051AA93 /* UIImage+ImageEffects.m in Sources */,
E4CD9F6D1A77DD2800318571 /* ReaderModeStyleViewController.swift in Sources */,
Expand Down Expand Up @@ -5691,6 +5702,7 @@
A83E5B1D1C1DA8D80026D912 /* UIPasteboardExtensionsTests.swift in Sources */,
E61D11681EAF8F43008A305B /* PanelDataObserversTests.swift in Sources */,
28D52E2F1BCDF53900187A1D /* ResetTests.swift in Sources */,
3B61CD591F2A750800D38DE1 /* PocketFeedTests.swift in Sources */,
E696FE511C47F86E00EC007C /* AuthenticatorTests.swift in Sources */,
3B6F40181DC7849C00656CC6 /* ActivityStreamTests.swift in Sources */,
);
Expand Down
67 changes: 67 additions & 0 deletions ClientTests/PocketFeedTests.swift
@@ -0,0 +1,67 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import UIKit
import GCDWebServers
import XCTest

@testable import Client

class PocketStoriesTests: XCTestCase {

var pocketAPI: String!
let webServer: GCDWebServer = GCDWebServer()

/// Setup a basic web server that binds to a random port and that has one default handler on /hello
fileprivate func setupWebServer() {
let path = Bundle(for: type(of: self)).path(forResource: "pocketglobalfeed", ofType: "json")
let data = try! Data(contentsOf: URL(fileURLWithPath: path!))

webServer.addHandler(forMethod: "GET", path: "/pocketglobalfeed", request: GCDWebServerRequest.self) { (request) -> GCDWebServerResponse! in
return GCDWebServerDataResponse(data: data, contentType: "application/json")
}

if webServer.start(withPort: 0, bonjourName: nil) == false {
XCTFail("Can't start the GCDWebServer")
}
pocketAPI = "http://localhost:\(webServer.port)/pocketglobalfeed"
}

override func setUp() {
super.setUp()
setupWebServer()
}

override func tearDown() {
super.tearDown()
}

func testPocketStoriesCaching() {
let expect = expectation(description: "Pocket")
let PocketFeed = Pocket(endPoint: pocketAPI)

PocketFeed.globalFeed(items: 4).upon { result in
let items = result
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
self.webServer.stop() // Stop the webserver so we can check caching

// Try again now that the webserver is down
PocketFeed.globalFeed(items: 4).upon { result in
let items = result
XCTAssertEqual(items.count, 2, "We are fetching a static feed. There are only 2 items in it")
let item = items.first
//These are all not optional so they should never be nil.
//But lets check in case someone decides to change something
XCTAssertNotNil(item?.domain, "Why")
XCTAssertNotNil(item?.imageURL, "You")
XCTAssertNotNil(item?.storyDescription, "Do")
XCTAssertNotNil(item?.title, "This")
XCTAssertNotNil(item?.url, "?")
expect.fulfill()
}
}
waitForExpectations(timeout: 10, handler: nil)
}

}
24 changes: 24 additions & 0 deletions ClientTests/pocketglobalfeed.json
@@ -0,0 +1,24 @@
{
"status": 1,
"list": [{
"id": 2092,
"url": "https:\/\/pocket.co\/xMnD5u",
"dedupe_url": "https:\/\/www.wired.com\/story\/turn-off-your-push-notifications\/",
"title": "Turn Off Your Push Notifications. All of Them",
"excerpt": "Push notifications are ruining my life. Yours too, I bet. Download more than a few apps and the notifications become a non-stop, cacophonous waterfall of nonsense. Here's just part of an afternoon on my phone:",
"domain": "wired.com",
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=https%3A%2F%2Fmedia.wired.com%2Fphotos%2F597267fd023c38366e1ae497%2Fmaster%2Fw_1200%2Cc_limit%2Fno_notifications-TA.gif&resize=w450",
"published_timestamp": "1332306000",
"sort_id": 0
}, {
"id": 2091,
"url": "https:\/\/pocket.co\/sMnD5m",
"dedupe_url": "http:\/\/www.latimes.com\/business\/la-fi-agenda-best-buy-20170717-htmlstory.html",
"title": "Why the grim reaper of retail hasn't come to claim Best Buy",
"excerpt": "Five years ago Best Buy Co. looked like a retail dinosaur, another victim of e-commerce juggernaut Amazon.com and other online sellers.",
"domain": "latimes.com",
"image_src": "https:\/\/d33ypg4xwx0n86.cloudfront.net\/direct?url=http%3A%2F%2Fwww.trbimg.com%2Fimg-547cc080%2Fturbine%2Fla-fi-mh-radio-shack-20141201-004%2F&resize=w450",
"published_timestamp": "1500267600",
"sort_id": 1
}]
}
134 changes: 134 additions & 0 deletions Providers/PocketFeed.swift
@@ -0,0 +1,134 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import Foundation
import Alamofire
import Shared
import Deferred
import Storage

private let PocketEnvAPIKey = "PocketEnvironmentAPIKey"
private let PocketGlobalFeed = "https://getpocket.com/v3/firefox/global-recs"
private let MaxCacheAge: Timestamp = OneMinuteInMilliseconds * 60 // 1 hour in milliseconds

/*s
The Pocket class is used to fetch stories from the Pocked API.
Right now this only supports the global feed
For a sample feed item check ClientTests/pocketglobalfeed.json
*/
struct PocketStory {
let url: URL
let title: String
let storyDescription: String
let imageURL: URL
let domain: String

static func parseJSON(list: Array<[String: Any]>) -> [PocketStory] {
return list.flatMap({ (storyDict) -> PocketStory? in
guard let urlS = storyDict["url"] as? String, let domain = storyDict["domain"] as? String,
let imageURLS = storyDict["image_src"] as? String,
let title = storyDict["title"] as? String,
let description = storyDict["excerpt"] as? String else {
return nil
}
guard let url = URL(string: urlS), let imageURL = URL(string: imageURLS) else {
return nil
}
return PocketStory(url: url, title: title, storyDescription: description, imageURL: imageURL, domain: domain)
})
}
}

private class PocketError: MaybeErrorType {
var description = "Failed to load from API"
}

class Pocket {

private let pocketGlobalFeed: String
// Allow endPoint to be overriden for testing
init(endPoint: String? = nil) {
pocketGlobalFeed = endPoint ?? PocketGlobalFeed
}

lazy fileprivate var alamofire: SessionManager = {
let ua = UserAgent.fxaUserAgent //TODO: use a different UA
let configuration = URLSessionConfiguration.default
return SessionManager.managerWithUserAgent(ua, configuration: configuration)
}()

private func findCachedResponse(for request: URLRequest) -> [String: Any]? {
let cachedResponse = URLCache.shared.cachedResponse(for: request)
guard let cachedAtTime = cachedResponse?.userInfo?["cache-time"] as? Timestamp, (Date.now() - cachedAtTime) < MaxCacheAge else {
return nil
}

guard let data = cachedResponse?.data, let json = try? JSONSerialization.jsonObject(with: data, options: []) else {
return nil
}

return json as? [String: Any]
}

private func cache(response: HTTPURLResponse?, for request: URLRequest, with data: Data?) {
guard let resp = response, let data = data else {
return
}
let metadata = ["cache-time": Date.now()]
let cachedResp = CachedURLResponse(response: resp, data: data, userInfo: metadata, storagePolicy: .allowed)
URLCache.shared.removeCachedResponse(for: request)
URLCache.shared.storeCachedResponse(cachedResp, for: request)
}

// Fetch items from the global pocket feed
func globalFeed(items: Int = 2) -> Deferred<Array<PocketStory>> {
let deferred = Deferred<Array<PocketStory>>()

guard let request = createGlobalFeedRequest(items: items) else {
deferred.fill([])
return deferred
}

if let cachedResponse = findCachedResponse(for: request), let items = cachedResponse["list"] as? Array<[String: Any]> {
deferred.fill(PocketStory.parseJSON(list: items))
return deferred
}

alamofire.request(request).validate(contentType: ["application/json"]).responseJSON { response in
guard response.error == nil, let result = response.result.value as? [String: Any] else {
return deferred.fill([])
}
self.cache(response: response.response, for: request, with: response.data)
guard let items = result["list"] as? Array<[String: Any]> else {
return deferred.fill([])
}
return deferred.fill(PocketStory.parseJSON(list: items))
}

return deferred
}

// Create the URL request to query the Pocket API. The max items that the query can return is 20
private func createGlobalFeedRequest(items: Int = 2) -> URLRequest? {
guard items > 0 && items <= 20 else {
return nil
}

guard let feedURL = URL(string: pocketGlobalFeed)?.withQueryParam("count", value: "\(items)") else {
return nil
}
let apiURL = addAPIKey(url: feedURL)
return URLRequest(url: apiURL, cachePolicy: URLRequest.CachePolicy.reloadIgnoringCacheData, timeoutInterval: 5)
}

private func addAPIKey(url: URL) -> URL {
let bundle = Bundle.main
guard let api_key = bundle.object(forInfoDictionaryKey: PocketEnvAPIKey) as? String else {
return url
}
return url.withQueryParam("consumer_key", value: api_key)
}

}

0 comments on commit 24e215f

Please sign in to comment.