diff --git a/ConfCore/Adapter.swift b/ConfCore/Adapter.swift index 8739d07..a711bdb 100644 --- a/ConfCore/Adapter.swift +++ b/ConfCore/Adapter.swift @@ -11,12 +11,15 @@ import SwiftyJSON enum AdapterError: Error { case invalidData + case unsupported case missingKey(JSONSubscriptType) var localizedDescription: String { switch self { case .invalidData: return "Invalid input data" + case .unsupported: + return "This type of entity is not supported" case .missingKey(let key): return "Input is missing a required key: \"\(key)\"" } diff --git a/ConfCore/NewsItem.swift b/ConfCore/NewsItem.swift index b57dad6..91f3eb3 100644 --- a/ConfCore/NewsItem.swift +++ b/ConfCore/NewsItem.swift @@ -9,6 +9,13 @@ import Cocoa import RealmSwift +enum NewsType: Int { + case news + case unsupportedUnknown + case gallery + case unsupportedPassbook +} + /// NewsItem can be a simple news text or a photo gallery class NewsItem: Object { @@ -18,9 +25,6 @@ class NewsItem: Object { /// The type of news (0 = regular news, 2 = photo gallery) dynamic var newsType = 0 - /// The location of the news (seems to always be zero) - dynamic var location = 0 - /// The condition that must be true so the user can see this item (used to limit some items to only attendees) dynamic var visibility = "" diff --git a/ConfCore/NewsItemsJSONAdapter.swift b/ConfCore/NewsItemsJSONAdapter.swift new file mode 100644 index 0000000..cb493d3 --- /dev/null +++ b/ConfCore/NewsItemsJSONAdapter.swift @@ -0,0 +1,63 @@ +// +// NewsItemsJSONAdapter.swift +// WWDC +// +// Created by Guilherme Rambo on 16/02/17. +// Copyright © 2017 Guilherme Rambo. All rights reserved. +// + +import Foundation +import SwiftyJSON + +private enum NewsItemKeys: String, JSONSubscriptType { + case id, title, body, timestamp, visibility, photos, type + + var jsonKey: JSONKey { + return JSONKey.key(rawValue) + } +} + +final class NewsItemsJSONAdapter: Adapter { + + typealias InputType = JSON + typealias OutputType = NewsItem + + func adapt(_ input: JSON) -> Result { + if let type = input[NewsItemKeys.type].string { + guard type != "pass" else { return .error(.unsupported) } + } + + guard let id = input[NewsItemKeys.id].string else { + return .error(.missingKey(NewsItemKeys.id)) + } + + guard let title = input[NewsItemKeys.title].string else { + return .error(.missingKey(NewsItemKeys.title)) + } + + guard let timestamp = input[NewsItemKeys.timestamp].double else { + return .error(.missingKey(NewsItemKeys.timestamp)) + } + + let visibility = input[NewsItemKeys.visibility].stringValue + + let item = NewsItem() + + if let photosJson = input[NewsItemKeys.photos].array { + if case .success(let photos) = PhotosJSONAdapter().adapt(photosJson) { + item.photos.append(objectsIn: photos) + } + } + + item.identifier = id + item.title = title + item.body = input[NewsItemKeys.body].stringValue + item.visibility = visibility + item.date = Date(timeIntervalSince1970: timestamp) + item.newsType = item.photos.count > 0 ? NewsType.gallery.rawValue : NewsType.news.rawValue + + return .success(item) + } + +} + diff --git a/ConfCore/PhotoRepresentation.swift b/ConfCore/PhotoRepresentation.swift index f71a493..d438b51 100644 --- a/ConfCore/PhotoRepresentation.swift +++ b/ConfCore/PhotoRepresentation.swift @@ -9,11 +9,25 @@ import Cocoa import RealmSwift +enum PhotoRepresentationSize: Int { + case mini = 256 + case small = 512 + case medium = 1024 + case large = 2048 + + static let all: [PhotoRepresentationSize] = [ + .mini, + .small, + .medium, + .large + ] +} + /// Photo representations are assets for photos specifying widths and URLs for different photo sizes class PhotoRepresentation: Object { - /// The URL for the photo - dynamic var remoteURL = "" + /// The path for the photo + dynamic var remotePath = "" /// The width of the photo dynamic var width = 0 @@ -22,7 +36,7 @@ class PhotoRepresentation: Object { let photo = LinkingObjects(fromType: Photo.self, property: "representations") override class func primaryKey() -> String? { - return "remoteURL" + return "remotePath" } } diff --git a/ConfCore/PhotosJSONAdapter.swift b/ConfCore/PhotosJSONAdapter.swift new file mode 100644 index 0000000..a93fdbb --- /dev/null +++ b/ConfCore/PhotosJSONAdapter.swift @@ -0,0 +1,52 @@ +// +// PhotosJSONAdapter.swift +// WWDC +// +// Created by Guilherme Rambo on 16/02/17. +// Copyright © 2017 Guilherme Rambo. All rights reserved. +// + +import Foundation +import SwiftyJSON + +private enum PhotoKeys: String, JSONSubscriptType { + case id, ratio + + var jsonKey: JSONKey { + return JSONKey.key(rawValue) + } +} + +final class PhotosJSONAdapter: Adapter { + + typealias InputType = JSON + typealias OutputType = Photo + + func adapt(_ input: JSON) -> Result { + guard let id = input[PhotoKeys.id].string else { + return .error(.missingKey(PhotoKeys.id)) + } + + guard let ratio = input[PhotoKeys.ratio].double else { + return .error(.missingKey(PhotoKeys.ratio)) + } + + let representations = PhotoRepresentationSize.all.map { size -> PhotoRepresentation in + let rep = PhotoRepresentation() + + rep.remotePath = "\(id)/\(size.rawValue).jpeg" + rep.width = size.rawValue + + return rep + } + + let photo = Photo() + + photo.identifier = id + photo.aspectRatio = ratio + photo.representations.append(objectsIn: representations) + + return .success(photo) + } + +} diff --git a/ConfCoreTests/AdapterTests.swift b/ConfCoreTests/AdapterTests.swift index c235158..1d64d09 100644 --- a/ConfCoreTests/AdapterTests.swift +++ b/ConfCoreTests/AdapterTests.swift @@ -319,4 +319,37 @@ class AdapterTests: XCTestCase { } } + func testNewsAndPhotoAdapters() { + let json = getJson(from: "news") + + guard let newsArray = json["items"].array else { + XCTFail("Couldn't find an array of items in news.json") + fatalError() + } + + let result = NewsItemsJSONAdapter().adapt(newsArray) + + switch result { + case .error(let error): + XCTFail(error.localizedDescription) + case .success(let items): + XCTAssertEqual(items.count, 16) + + // Regular news + XCTAssertEqual(items[0].identifier, "991DF480-4435-4930-B0BC-8F75EFB85002") + XCTAssertEqual(items[0].title, "Welcome") + XCTAssertEqual(items[0].body, "We’re looking forward to an exciting week at the Apple Worldwide Developers Conference. Use this app to stream session videos, browse the conference schedule, mark schedule items as favorites, keep up with the latest conference news, and view maps of Moscone West.") + XCTAssertEqual(items[0].date, Date(timeIntervalSince1970: 1464973210)) + + // Photo gallery + XCTAssertEqual(items[5].identifier, "047E4F0B-2B8C-499A-BB23-F2CFDB3EB730") + XCTAssertEqual(items[5].title, "Scholarship Winners Get the Excitement Started") + XCTAssertEqual(items[5].body, "") + XCTAssertEqual(items[5].date, Date(timeIntervalSince1970: 1465833604)) + XCTAssertEqual(items[5].photos[0].identifier, "6F3D98B4-71A9-4321-9D4E-974346E784FD") + XCTAssertEqual(items[5].photos[0].representations[0].width, 256) + XCTAssertEqual(items[5].photos[0].representations[0].remotePath, "6F3D98B4-71A9-4321-9D4E-974346E784FD/256.jpeg") + } + } + } diff --git a/ConfCoreTests/DatabaseTests.swift b/ConfCoreTests/DatabaseTests.swift index d383da1..b41113c 100644 --- a/ConfCoreTests/DatabaseTests.swift +++ b/ConfCoreTests/DatabaseTests.swift @@ -79,11 +79,11 @@ class DatabaseTests: XCTestCase { asset2.remoteURL = "http://devstreaming.apple.com/videos/wwdc/2014/208xx42tf0hw3vv/208/ref.mov" let photoRep = PhotoRepresentation() - photoRep.remoteURL = "https://test.com/test.jpg" + photoRep.remotePath = "4FF1EAAF-7D24-4F20-A182-0AA1FBB4D8DE/512.jpeg" photoRep.width = 512 let photoRep2 = PhotoRepresentation() - photoRep2.remoteURL = "https://test.com/test1024.jpg" + photoRep2.remotePath = "CC9D2377-90B0-4750-8C97-549DEC08C028/1024.jpeg" photoRep2.width = 1024 let instance = SessionInstance() diff --git a/WWDC.xcodeproj/project.pbxproj b/WWDC.xcodeproj/project.pbxproj index 570fdc5..961cce4 100644 --- a/WWDC.xcodeproj/project.pbxproj +++ b/WWDC.xcodeproj/project.pbxproj @@ -21,6 +21,8 @@ DD36A4DE1E478D7E00B2EA88 /* ConfCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DD36A4C81E478D7E00B2EA88 /* ConfCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DD3C4D251E561E4D0093BBD0 /* RxRealm.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD5F51821E47DE7E0017F9EC /* RxRealm.framework */; }; DD3C4D261E561E4E0093BBD0 /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DD5F51831E47DE7E0017F9EC /* RxSwift.framework */; }; + DD3C4D291E5622950093BBD0 /* PhotosJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3C4D281E5622950093BBD0 /* PhotosJSONAdapter.swift */; }; + DD3C4D2B1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD3C4D2A1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift */; }; DD54CD101E4AB8ED000BF5F2 /* KeywordsJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD54CD0F1E4AB8ED000BF5F2 /* KeywordsJSONAdapter.swift */; }; DD54CD121E4AB95F000BF5F2 /* FocusesJSONAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD54CD111E4AB95F000BF5F2 /* FocusesJSONAdapter.swift */; }; DD54CD141E4AC21F000BF5F2 /* SessionAssetsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD54CD131E4AC21F000BF5F2 /* SessionAssetsAdapter.swift */; }; @@ -138,6 +140,8 @@ DD36A4D01E478D7E00B2EA88 /* ConfCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ConfCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DD36A4D71E478D7E00B2EA88 /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = ""; }; DD36A4D91E478D7E00B2EA88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DD3C4D281E5622950093BBD0 /* PhotosJSONAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotosJSONAdapter.swift; sourceTree = ""; }; + DD3C4D2A1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsItemsJSONAdapter.swift; sourceTree = ""; }; DD54CD0F1E4AB8ED000BF5F2 /* KeywordsJSONAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeywordsJSONAdapter.swift; sourceTree = ""; }; DD54CD111E4AB95F000BF5F2 /* FocusesJSONAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FocusesJSONAdapter.swift; sourceTree = ""; }; DD54CD131E4AC21F000BF5F2 /* SessionAssetsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SessionAssetsAdapter.swift; sourceTree = ""; }; @@ -328,6 +332,8 @@ DD54CD111E4AB95F000BF5F2 /* FocusesJSONAdapter.swift */, DD6477321E5535DE00386D93 /* SessionsJSONAdapter.swift */, DD0353481E5545B300D5E343 /* SessionInstancesJSONAdapter.swift */, + DD3C4D281E5622950093BBD0 /* PhotosJSONAdapter.swift */, + DD3C4D2A1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift */, ); name = Adapters; sourceTree = ""; @@ -631,6 +637,7 @@ DD5DEC1E1E4AA6B800426FA6 /* Result.swift in Sources */, DD65FD5B1E48A1080054DD35 /* Focus.swift in Sources */, DD54CD141E4AC21F000BF5F2 /* SessionAssetsAdapter.swift in Sources */, + DD3C4D291E5622950093BBD0 /* PhotosJSONAdapter.swift in Sources */, DD65FD671E48BAAD0054DD35 /* Transcript.swift in Sources */, DD0353491E5545B300D5E343 /* SessionInstancesJSONAdapter.swift in Sources */, DD65FD631E48ACA90054DD35 /* Keyword.swift in Sources */, @@ -638,6 +645,7 @@ DD65FD551E48A0A70054DD35 /* Room.swift in Sources */, DD65FD691E48BBEF0054DD35 /* TranscriptAnnotation.swift in Sources */, DD65FD4C1E489FF50054DD35 /* SessionAsset.swift in Sources */, + DD3C4D2B1E564AE80093BBD0 /* NewsItemsJSONAdapter.swift in Sources */, DD65FD5F1E48A3900054DD35 /* SessionInstance.swift in Sources */, DD65FD571E48A0BD0054DD35 /* Event.swift in Sources */, DD6477311E55231A00386D93 /* LiveVideosAdapter.swift in Sources */,