Added API client, test server and related tests

This commit is contained in:
Guilherme Rambo
2017-02-21 14:33:06 -03:00
parent c89be368b2
commit 003561e289
11 changed files with 571 additions and 14 deletions

View File

@@ -0,0 +1,169 @@
//
// AppleAPIClient.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
import Siesta
import SwiftyJSON
// MARK: - Initialization and configuration
final class AppleAPIClient {
fileprivate let environment: Environment
fileprivate let service: Service
init(environment: Environment) {
self.environment = environment
self.service = Service(baseURL: environment.baseURL)
configureService()
}
private let jsonParser = ResponseContentTransformer { JSON($0.content as AnyObject) }
private func configureService() {
service.configure("**") { config in
config.pipeline[.parsing].add(self.jsonParser, contentTypes: ["*/json"])
}
service.configureTransformer(environment.newsPath) { [weak self] (entity: Entity<JSON>) throws -> [NewsItem]? in
let json = entity.content as JSON
guard let newsItemsJson = json["items"].array else {
throw APIError.adapter
}
return try self?.failableAdaptCollection(newsItemsJson, using: NewsItemsJSONAdapter())
}
service.configureTransformer(environment.sessionsPath) { [weak self] (entity: Entity<JSON>) throws -> ScheduleResponse? in
let json = entity.content as JSON
return try self?.failableAdapt(json, using: ScheduleResponseAdapter())
}
service.configureTransformer(environment.videosPath) { [weak self] (entity: Entity<JSON>) throws -> SessionsResponse? in
let json = entity.content as JSON
return try self?.failableAdapt(json, using: SessionsResponseAdapter())
}
service.configureTransformer(environment.liveVideosPath) { [weak self] (entity: Entity<JSON>) throws -> [SessionAsset]? in
let json = entity.content as JSON
guard let sessionsDict = json["live_sessions"].dictionary else {
throw APIError.adapter
}
let sessionsArray = sessionsDict.flatMap { key, value -> JSON? in
guard let id = JSON.init(rawValue: key) else { return nil }
var v = value
v["sessionId"] = id
return v
}
return try self?.failableAdaptCollection(sessionsArray, using: LiveVideosAdapter())
}
}
// MARK: - Resources
fileprivate lazy var liveVideoAssets: Resource = {
return self.service.resource(self.environment.liveVideosPath)
}()
fileprivate lazy var sessions: Resource = {
return self.service.resource(self.environment.videosPath)
}()
fileprivate lazy var schedule: Resource = {
return self.service.resource(self.environment.sessionsPath)
}()
fileprivate lazy var news: Resource = {
return self.service.resource(self.environment.newsPath)
}()
}
// MARK: - Standard API requests
extension AppleAPIClient {
func fetchLiveVideoAssets(completion: @escaping (Result<[SessionAsset], APIError>) -> Void) {
liveVideoAssets.addObserver(owner: self) { [weak self] resource, event in
self?.process(resource, event: event, with: completion)
}.loadIfNeeded()
}
func fetchSchedule(completion: @escaping (Result<ScheduleResponse, APIError>) -> Void) {
schedule.addObserver(owner: self) { [weak self] resource, event in
self?.process(resource, event: event, with: completion)
}.loadIfNeeded()
}
func fetchSessions(completion: @escaping (Result<SessionsResponse, APIError>) -> Void) {
sessions.addObserver(owner: self) { [weak self] resource, event in
self?.process(resource, event: event, with: completion)
}.loadIfNeeded()
}
func fetchNewsItems(completion: @escaping (Result<[NewsItem], APIError>) -> Void) {
news.addObserver(owner: self) { [weak self] resource, event in
self?.process(resource, event: event, with: completion)
}.loadIfNeeded()
}
}
// MARK: - API results processing
extension AppleAPIClient {
/// Convenience method to use a model adapter as a method that returns the model(s) or throws an error
fileprivate func failableAdapt<A: Adapter, T>(_ input: JSON, using adapter: A) throws -> T where A.InputType == JSON, A.OutputType == T {
let result = adapter.adapt(input)
switch result {
case .error(let error):
throw error
case .success(let output):
return output
}
}
/// Convenience method to use a model adapter as a method that returns the model(s) or throws an error
fileprivate func failableAdaptCollection<A: Adapter, T>(_ input: [JSON], using adapter: A) throws -> [T] where A.InputType == JSON, A.OutputType == T {
let result = adapter.adapt(input)
switch result {
case .error(let error):
throw error
case .success(let output):
return output
}
}
fileprivate func process<M>(_ resource: Resource, event: ResourceEvent, with completion: @escaping (Result<M, APIError>) -> ()) {
switch event {
case .error:
completion(.error(resource.error))
case .newData(_), .notModified:
if let results: M = resource.typedContent() {
completion(.success(results))
} else {
completion(.error(.adapter))
}
default: break
}
}
}

View File

@@ -0,0 +1,29 @@
//
// Environment.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
struct Environment {
let baseURL: String
let videosPath: String
let sessionsPath: String
let newsPath: String
let liveVideosPath: String
}
extension Environment {
static let test = Environment(baseURL: "http://localhost:9042",
videosPath: "/videos.json",
sessionsPath: "/sessions.json",
newsPath: "/news.json",
liveVideosPath: "/videos_live.json")
}

View File

@@ -0,0 +1,39 @@
//
// Resource+Error.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
import Siesta
enum APIError: Error {
case http(Error)
case adapter
case unknown
var localizedDescription: String {
switch self {
case .http(let error):
return error.localizedDescription
case .adapter:
return "Unable to process the data returned by the server"
case .unknown:
return "An unknown networking error occurred"
}
}
}
extension Resource {
var error: APIError {
if let underlyingError = self.latestError {
return .http(underlyingError)
} else {
return .unknown
}
}
}

View File

@@ -0,0 +1,17 @@
//
// ScheduleResponse.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
struct ScheduleResponse {
let rooms: [Room]
let tracks: [Track]
let instances: [SessionInstance]
}

View File

@@ -0,0 +1,57 @@
//
// ScheduleResponseAdapter.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
import SwiftyJSON
private enum ScheduleKeys: String, JSONSubscriptType {
case response, rooms, tracks, sessions
var jsonKey: JSONKey {
return JSONKey.key(rawValue)
}
}
final class ScheduleResponseAdapter: Adapter {
typealias InputType = JSON
typealias OutputType = ScheduleResponse
func adapt(_ input: JSON) -> Result<ScheduleResponse, AdapterError> {
guard let roomsJson = input[ScheduleKeys.response][ScheduleKeys.rooms].array else {
return .error(.missingKey(ScheduleKeys.rooms))
}
guard let tracksJson = input[ScheduleKeys.response][ScheduleKeys.tracks].array else {
return .error(.missingKey(ScheduleKeys.rooms))
}
guard let instancesJson = input[ScheduleKeys.response][ScheduleKeys.sessions].array else {
return .error(.missingKey(ScheduleKeys.rooms))
}
guard case .success(let rooms) = RoomsJSONAdapter().adapt(roomsJson) else {
return .error(.invalidData)
}
guard case .success(let tracks) = TracksJSONAdapter().adapt(tracksJson) else {
return .error(.invalidData)
}
guard case .success(let instances) = SessionInstancesJSONAdapter().adapt(instancesJson) else {
return .error(.invalidData)
}
let response = ScheduleResponse(rooms: rooms,
tracks: tracks,
instances: instances)
return .success(response)
}
}

View File

@@ -0,0 +1,17 @@
//
// SessionsResponse.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
struct SessionsResponse {
let events: [Event]
let sessions: [Session]
let assets: [SessionAsset]
}

View File

@@ -0,0 +1,51 @@
//
// SessionsResponseAdapter.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import Foundation
import SwiftyJSON
private enum SessionResponseKeys: String, JSONSubscriptType {
case events, sessions
var jsonKey: JSONKey {
return JSONKey.key(rawValue)
}
}
final class SessionsResponseAdapter: Adapter {
typealias InputType = JSON
typealias OutputType = SessionsResponse
func adapt(_ input: JSON) -> Result<SessionsResponse, AdapterError> {
guard let eventsJson = input[SessionResponseKeys.events].array else {
return .error(.missingKey(SessionResponseKeys.events))
}
guard let sessionsJson = input[SessionResponseKeys.sessions].array else {
return .error(.missingKey(SessionResponseKeys.sessions))
}
guard case .success(let events) = EventsJSONAdapter().adapt(eventsJson) else {
return .error(.invalidData)
}
guard case .success(let sessions) = SessionsJSONAdapter().adapt(sessionsJson) else {
return .error(.invalidData)
}
guard case .success(let assets) = SessionAssetsJSONAdapter().adapt(sessionsJson) else {
return .error(.invalidData)
}
let response = SessionsResponse(events: events, sessions: sessions, assets: assets.flatMap({$0}))
return .success(response)
}
}

View File

@@ -0,0 +1,88 @@
//
// NetworkingTests.swift
// WWDC
//
// Created by Guilherme Rambo on 21/02/17.
// Copyright © 2017 Guilherme Rambo. All rights reserved.
//
import XCTest
@testable import ConfCore
class NetworkingTests: XCTestCase {
let client = AppleAPIClient(environment: .test)
func testNewsItemEndpoint() {
let exp = expectation(description: "News items response")
client.fetchNewsItems { result in
switch result {
case .error(let error):
XCTFail(error.localizedDescription)
case .success(let items):
XCTAssertEqual(items.count, 16)
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
func testScheduleEndpoint() {
let exp = expectation(description: "Schedule response")
client.fetchSchedule { result in
switch result {
case .error(let error):
XCTFail(error.localizedDescription)
case .success(let response):
XCTAssertEqual(response.tracks.count, 8)
XCTAssertEqual(response.rooms.count, 29)
XCTAssertEqual(response.instances.count, 316)
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
func testSessionsAndAssetsEndpoint() {
let exp = expectation(description: "Sessions and assets response")
client.fetchSessions { result in
switch result {
case .error(let error):
XCTFail(error.localizedDescription)
case .success(let response):
XCTAssertEqual(response.events.count, 5)
XCTAssertEqual(response.sessions.count, 550)
XCTAssertEqual(response.assets.count, 2947)
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
func testLiveVideosEndpoint() {
let exp = expectation(description: "Live videos response")
client.fetchLiveVideoAssets { result in
switch result {
case .error(let error):
XCTFail(error.localizedDescription)
case .success(let assets):
XCTAssertEqual(assets.count, 111)
}
exp.fulfill()
}
waitForExpectations(timeout: 10)
}
}

13
Fixtures/testserver.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
if ! type "npm" > /dev/null; then
echo "NPM is not installed, installing...\n"
curl -L https://www.npmjs.com/install.sh | sh
fi
if ! type "http-server" > /dev/null; then
echo "http-server module is not installed, installing...\n"
npm install http-server -g
fi
http-server -p 9042 ./json

View File

@@ -69,6 +69,14 @@
DDAE7C4D1E5BA4D100CEA205 /* TranscriptsJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C4C1E5BA4D100CEA205 /* TranscriptsJSONAdapter.swift */; };
DDAE7C4F1E5BA4E300CEA205 /* transcript.json in Resources */ = {isa = PBXBuildFile; fileRef = DDAE7C4E1E5BA4E300CEA205 /* transcript.json */; };
DDAE7C551E5BC6CD00CEA205 /* Siesta.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DDAE7C541E5BC6CD00CEA205 /* Siesta.framework */; };
DDAE7C581E5C629800CEA205 /* AppleAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C571E5C629800CEA205 /* AppleAPIClient.swift */; };
DDAE7C5A1E5C652600CEA205 /* Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C591E5C652600CEA205 /* Environment.swift */; };
DDAE7C5C1E5C707A00CEA205 /* NetworkingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C5B1E5C707A00CEA205 /* NetworkingTests.swift */; };
DDAE7C5E1E5C723A00CEA205 /* Resource+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C5D1E5C723A00CEA205 /* Resource+Error.swift */; };
DDAE7C601E5C7E3400CEA205 /* ScheduleResponseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C5F1E5C7E3400CEA205 /* ScheduleResponseAdapter.swift */; };
DDAE7C631E5C7E7800CEA205 /* ScheduleResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C621E5C7E7800CEA205 /* ScheduleResponse.swift */; };
DDAE7C651E5CA81C00CEA205 /* SessionsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C641E5CA81C00CEA205 /* SessionsResponse.swift */; };
DDAE7C6B1E5CA97A00CEA205 /* SessionsResponseAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDAE7C6A1E5CA97A00CEA205 /* SessionsResponseAdapter.swift */; };
DDD3797D1E4AB655005FE876 /* TracksJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD3797C1E4AB655005FE876 /* TracksJSONAdapter.swift */; };
/* End PBXBuildFile section */
@@ -191,6 +199,14 @@
DDAE7C4E1E5BA4E300CEA205 /* transcript.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = transcript.json; sourceTree = "<group>"; };
DDAE7C541E5BC6CD00CEA205 /* Siesta.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Siesta.framework; path = Carthage/Build/Mac/Siesta.framework; sourceTree = "<group>"; };
DDAE7C561E5BC6E300CEA205 /* Siesta.framework.dSYM */ = {isa = PBXFileReference; lastKnownFileType = wrapper.dsym; name = Siesta.framework.dSYM; path = Carthage/Build/Mac/Siesta.framework.dSYM; sourceTree = "<group>"; };
DDAE7C571E5C629800CEA205 /* AppleAPIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppleAPIClient.swift; sourceTree = "<group>"; };
DDAE7C591E5C652600CEA205 /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = "<group>"; };
DDAE7C5B1E5C707A00CEA205 /* NetworkingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingTests.swift; sourceTree = "<group>"; };
DDAE7C5D1E5C723A00CEA205 /* Resource+Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Resource+Error.swift"; sourceTree = "<group>"; };
DDAE7C5F1E5C7E3400CEA205 /* ScheduleResponseAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleResponseAdapter.swift; sourceTree = "<group>"; };
DDAE7C621E5C7E7800CEA205 /* ScheduleResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScheduleResponse.swift; sourceTree = "<group>"; };
DDAE7C641E5CA81C00CEA205 /* SessionsResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionsResponse.swift; sourceTree = "<group>"; };
DDAE7C6A1E5CA97A00CEA205 /* SessionsResponseAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionsResponseAdapter.swift; sourceTree = "<group>"; };
DDD3797C1E4AB655005FE876 /* TracksJSONAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TracksJSONAdapter.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -323,6 +339,7 @@
DD5DEC231E4AA8C800426FA6 /* Fixtures */,
DD36A4D71E478D7E00B2EA88 /* DatabaseTests.swift */,
DD34DA061E4AB0E400F25C45 /* AdapterTests.swift */,
DDAE7C5B1E5C707A00CEA205 /* NetworkingTests.swift */,
DD36A4D91E478D7E00B2EA88 /* Info.plist */,
);
path = ConfCoreTests;
@@ -331,20 +348,10 @@
DD5DEC1C1E4AA68F00426FA6 /* Adapters */ = {
isa = PBXGroup;
children = (
DD5DEC1F1E4AA6F800426FA6 /* Adapter.swift */,
DD5DEC2E1E4AAC9300426FA6 /* DateAdapter.swift */,
DD5DEC211E4AA7F100426FA6 /* EventsJSONAdapter.swift */,
DD54CD131E4AC21F000BF5F2 /* SessionAssetsAdapter.swift */,
DD6477301E55231A00386D93 /* LiveVideosAdapter.swift */,
DD34DA0A1E4AB3E400F25C45 /* RoomsJSONAdapter.swift */,
DDD3797C1E4AB655005FE876 /* TracksJSONAdapter.swift */,
DD54CD0F1E4AB8ED000BF5F2 /* KeywordsJSONAdapter.swift */,
DD54CD111E4AB95F000BF5F2 /* FocusesJSONAdapter.swift */,
DD6477321E5535DE00386D93 /* SessionsJSONAdapter.swift */,
DD0353481E5545B300D5E343 /* SessionInstancesJSONAdapter.swift */,
DD3C4D281E5622950093BBD0 /* PhotosJSONAdapter.swift */,
DD3C4D2A1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift */,
DDAE7C4C1E5BA4D100CEA205 /* TranscriptsJSONAdapter.swift */,
DDAE7C681E5CA84B00CEA205 /* Apple */,
DDAE7C671E5CA84200CEA205 /* Base */,
DDAE7C691E5CA85900CEA205 /* ASCIIWWDC */,
DDAE7C661E5CA83500CEA205 /* Responses */,
);
name = Adapters;
sourceTree = "<group>";
@@ -405,6 +412,7 @@
isa = PBXGroup;
children = (
DD5DEC1D1E4AA6B800426FA6 /* Result.swift */,
DDAE7C611E5C7E6A00CEA205 /* Responses */,
DD65FD6E1E48BE2F0054DD35 /* Apple */,
DD65FD6F1E48BE400054DD35 /* ASCIIWWDC */,
DD65FD701E48BE480054DD35 /* UserDefined */,
@@ -451,6 +459,9 @@
DDAE7C521E5BC6B000CEA205 /* Networking */ = {
isa = PBXGroup;
children = (
DDAE7C5D1E5C723A00CEA205 /* Resource+Error.swift */,
DDAE7C591E5C652600CEA205 /* Environment.swift */,
DDAE7C571E5C629800CEA205 /* AppleAPIClient.swift */,
);
name = Networking;
sourceTree = "<group>";
@@ -462,6 +473,59 @@
name = Frameworks;
sourceTree = "<group>";
};
DDAE7C611E5C7E6A00CEA205 /* Responses */ = {
isa = PBXGroup;
children = (
DDAE7C621E5C7E7800CEA205 /* ScheduleResponse.swift */,
DDAE7C641E5CA81C00CEA205 /* SessionsResponse.swift */,
);
name = Responses;
sourceTree = "<group>";
};
DDAE7C661E5CA83500CEA205 /* Responses */ = {
isa = PBXGroup;
children = (
DDAE7C5F1E5C7E3400CEA205 /* ScheduleResponseAdapter.swift */,
DDAE7C6A1E5CA97A00CEA205 /* SessionsResponseAdapter.swift */,
);
name = Responses;
sourceTree = "<group>";
};
DDAE7C671E5CA84200CEA205 /* Base */ = {
isa = PBXGroup;
children = (
DD5DEC1F1E4AA6F800426FA6 /* Adapter.swift */,
DD5DEC2E1E4AAC9300426FA6 /* DateAdapter.swift */,
);
name = Base;
sourceTree = "<group>";
};
DDAE7C681E5CA84B00CEA205 /* Apple */ = {
isa = PBXGroup;
children = (
DD5DEC211E4AA7F100426FA6 /* EventsJSONAdapter.swift */,
DD54CD131E4AC21F000BF5F2 /* SessionAssetsAdapter.swift */,
DD6477301E55231A00386D93 /* LiveVideosAdapter.swift */,
DD34DA0A1E4AB3E400F25C45 /* RoomsJSONAdapter.swift */,
DDD3797C1E4AB655005FE876 /* TracksJSONAdapter.swift */,
DD54CD0F1E4AB8ED000BF5F2 /* KeywordsJSONAdapter.swift */,
DD54CD111E4AB95F000BF5F2 /* FocusesJSONAdapter.swift */,
DD6477321E5535DE00386D93 /* SessionsJSONAdapter.swift */,
DD0353481E5545B300D5E343 /* SessionInstancesJSONAdapter.swift */,
DD3C4D281E5622950093BBD0 /* PhotosJSONAdapter.swift */,
DD3C4D2A1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift */,
);
name = Apple;
sourceTree = "<group>";
};
DDAE7C691E5CA85900CEA205 /* ASCIIWWDC */ = {
isa = PBXGroup;
children = (
DDAE7C4C1E5BA4D100CEA205 /* TranscriptsJSONAdapter.swift */,
);
name = ASCIIWWDC;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -654,6 +718,9 @@
buildActionMask = 2147483647;
files = (
DD5DEC2F1E4AAC9300426FA6 /* DateAdapter.swift in Sources */,
DDAE7C5E1E5C723A00CEA205 /* Resource+Error.swift in Sources */,
DDAE7C5A1E5C652600CEA205 /* Environment.swift in Sources */,
DDAE7C651E5CA81C00CEA205 /* SessionsResponse.swift in Sources */,
DD65FD591E48A0C90054DD35 /* Photo.swift in Sources */,
DD65FD6B1E48BD9F0054DD35 /* Favorite.swift in Sources */,
DD65FD511E48A05C0054DD35 /* Session.swift in Sources */,
@@ -661,12 +728,15 @@
DD65FD531E48A06A0054DD35 /* NewsItem.swift in Sources */,
DDD3797D1E4AB655005FE876 /* TracksJSONAdapter.swift in Sources */,
DD65FD5D1E48A11F0054DD35 /* Track.swift in Sources */,
DDAE7C631E5C7E7800CEA205 /* ScheduleResponse.swift in Sources */,
DD5DEC201E4AA6F800426FA6 /* Adapter.swift in Sources */,
DD5DEC221E4AA7F100426FA6 /* EventsJSONAdapter.swift in Sources */,
DDAE7C601E5C7E3400CEA205 /* ScheduleResponseAdapter.swift in Sources */,
DD65FD6D1E48BE270054DD35 /* Bookmark.swift in Sources */,
DD5DEC1E1E4AA6B800426FA6 /* Result.swift in Sources */,
DD65FD5B1E48A1080054DD35 /* Focus.swift in Sources */,
DD54CD141E4AC21F000BF5F2 /* SessionAssetsAdapter.swift in Sources */,
DDAE7C581E5C629800CEA205 /* AppleAPIClient.swift in Sources */,
DD3C4D291E5622950093BBD0 /* PhotosJSONAdapter.swift in Sources */,
DD65FD671E48BAAD0054DD35 /* Transcript.swift in Sources */,
DD0353491E5545B300D5E343 /* SessionInstancesJSONAdapter.swift in Sources */,
@@ -680,6 +750,7 @@
DD65FD5F1E48A3900054DD35 /* SessionInstance.swift in Sources */,
DD65FD571E48A0BD0054DD35 /* Event.swift in Sources */,
DD6477311E55231A00386D93 /* LiveVideosAdapter.swift in Sources */,
DDAE7C6B1E5CA97A00CEA205 /* SessionsResponseAdapter.swift in Sources */,
DD65FD611E48A86A0054DD35 /* PhotoRepresentation.swift in Sources */,
DD6477331E5535DE00386D93 /* SessionsJSONAdapter.swift in Sources */,
DD54CD121E4AB95F000BF5F2 /* FocusesJSONAdapter.swift in Sources */,
@@ -691,6 +762,7 @@
buildActionMask = 2147483647;
files = (
DD36A4D81E478D7E00B2EA88 /* DatabaseTests.swift in Sources */,
DDAE7C5C1E5C707A00CEA205 /* NetworkingTests.swift in Sources */,
DD34DA071E4AB0E400F25C45 /* AdapterTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;

View File

@@ -28,5 +28,10 @@
<string>Main</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>