From 21dd5f9c8d40122d6a77fb9a3f5c4f92ff218540 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Fri, 8 Sep 2017 14:55:42 +0200 Subject: [PATCH 01/10] Remove on CoreLocation to avoid dynamic linking issues --- .../Contents.swift | 7 +- Sources/Contentful/Query.swift | 22 +- Sources/Contentful/Resource.swift | 5 +- Tests/ContentfulTests/QueryTests.swift | 233 +++++++++++------- 4 files changed, 172 insertions(+), 95 deletions(-) diff --git a/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift index 10b4a86c..ba01dcfc 100644 --- a/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift @@ -77,16 +77,15 @@ client.fetchEntries(with: query).next { } //: If you have location-enabled content, you can use it for searching as well. Sort results by distance: -import CoreLocation -query = Query(onContentTypeFor: "1t9IbcfdCk6m04uISSsaIK").where("fields.center", .isNear(CLLocationCoordinate2D(latitude: 38, longitude: -122))) +query = Query(onContentTypeFor: "1t9IbcfdCk6m04uISSsaIK").where("fields.center", .isNear(Location(latitude: 38, longitude: -122))) client.fetchEntries(with: query).next { let names = $0.items.flatMap { $0.fields.string(at: "name") } print(names) } //: Or retrieve all resources in a bounding rectangle: -let bottomLeft = CLLocationCoordinate2D(latitude: 40, longitude: -124) -let topRight = CLLocationCoordinate2D(latitude: 36, longitude: -121) +let bottomLeft = Location(latitude: 40, longitude: -124) +let topRight = Location(latitude: 36, longitude: -121) let boundingBox = Bounds.box(bottomLeft: bottomLeft, topRight: topRight) query = Query(onContentTypeFor: "1t9IbcfdCk6m04uISSsaIK").where("fields.center", .isWithin(boundingBox)) client.fetchEntries(with: query).next { diff --git a/Sources/Contentful/Query.swift b/Sources/Contentful/Query.swift index 35471f72..1cbf10f5 100644 --- a/Sources/Contentful/Query.swift +++ b/Sources/Contentful/Query.swift @@ -7,8 +7,6 @@ // import Foundation -import Interstellar -import CoreLocation /// The available URL parameter names for queries; used internally by the various `Contentful.Query` types. /// Use these static variables to avoid making typos when constructing queries. It is recommended to take @@ -74,11 +72,25 @@ extension Date: QueryableRange { } } +/** + Small struct to store location coordinates. This is used in preferences over CoreLocation types to avoid + extra linking requirements for the SDK. + */ +public struct Location { + let latitude: Double + let longitude: Double + + public init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } +} + /// Use bounding boxes or bounding circles to perform queries on location-enabled content. /// See: public enum Bounds { - case box(bottomLeft: CLLocationCoordinate2D, topRight: CLLocationCoordinate2D) - case circle(center: CLLocationCoordinate2D, radius: Double) + case box(bottomLeft: Location, topRight: Location) + case circle(center: Location, radius: Double) } /// All the possible MIME types that are supported by Contentful. \ @@ -135,7 +147,7 @@ public enum QueryOperation { case isGreaterThanOrEqualTo(QueryableRange) /// Proximity searches. - case isNear(CLLocationCoordinate2D) + case isNear(Location) case isWithin(Bounds) fileprivate var string: String { diff --git a/Sources/Contentful/Resource.swift b/Sources/Contentful/Resource.swift index a40e1307..81d2bf55 100644 --- a/Sources/Contentful/Resource.swift +++ b/Sources/Contentful/Resource.swift @@ -8,7 +8,6 @@ import Foundation import ObjectMapper -import CoreLocation /// Protocol for resources inside Contentful public class Resource: ImmutableMappable { @@ -236,11 +235,11 @@ public extension Dictionary where Key: ExpressibleByStringLiteral { - Parameter key: The name of the field to extract the `Bool` value from. - Returns: The `Bool` value, or `nil` if data contained is not convertible to a `Bool`. */ - public func location(at key: Key) -> CLLocationCoordinate2D? { + public func location(at key: Key) -> Location? { let coordinateJSON = self[key] as? [String: Any] guard let longitude = coordinateJSON?["lon"] as? Double else { return nil } guard let latitude = coordinateJSON?["lat"] as? Double else { return nil } - let location = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + let location = Location(latitude: latitude, longitude: longitude) return location } diff --git a/Tests/ContentfulTests/QueryTests.swift b/Tests/ContentfulTests/QueryTests.swift index 319a27d4..fb14eb99 100644 --- a/Tests/ContentfulTests/QueryTests.swift +++ b/Tests/ContentfulTests/QueryTests.swift @@ -10,9 +10,7 @@ import XCTest import Nimble import DVR -import Interstellar import CoreData -import CoreLocation final class Cat: EntryModellable { @@ -48,7 +46,7 @@ final class City: EntryModellable { init(entry: Entry) { self.id = entry.id self.localeCode = entry.localeCode - self.location = CLLocationCoordinate2D(latitude: 1, longitude: 1) + self.location = Location(latitude: 1, longitude: 1) } func populateLinks(from cache: [FieldName : Any]) {} @@ -57,7 +55,7 @@ final class City: EntryModellable { var id: String var localeCode: String - var location: CLLocationCoordinate2D? + var location: Location? } final class Dog: EntryModellable { @@ -405,11 +403,17 @@ class QueryTests: XCTestCase { let query = Query(where: "sys.updatedAt", .isLessThanOrEqualTo(date)) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - expect(entries.count).to(equal(10)) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + expect(entries.count).to(equal(10)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -422,12 +426,16 @@ class QueryTests: XCTestCase { let query = QueryOn(where: "sys.updatedAt", .isLessThanOrEqualTo("2015-01-01T00:00:00Z")) - QueryTests.client.fetchMappedEntries(with: query).then { catsResponse in - let cats = catsResponse.items - expect(cats.count).to(equal(3)) + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let catsResponse): + let cats = catsResponse.items + expect(cats.count).to(equal(3)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } - + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -438,12 +446,17 @@ class QueryTests: XCTestCase { let query = try! Query(orderedUsing: OrderParameter("sys.createdAt")) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - let ids = entries.map { $0.sys.id } - expect(ids).to(equal(EntryTests.orderedEntries)) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + let ids = entries.map { $0.sys.id } + expect(ids).to(equal(EntryTests.orderedEntries)) + case .error(let error): + fail("\(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -452,12 +465,17 @@ class QueryTests: XCTestCase { let query = try! Query(orderedUsing: OrderParameter("sys.createdAt", inReverse: true)) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - let ids = entries.map { $0.sys.id } - expect(ids).to(equal(EntryTests.orderedEntries.reversed())) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + let ids = entries.map { $0.sys.id } + expect(ids).to(equal(EntryTests.orderedEntries.reversed())) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -468,13 +486,18 @@ class QueryTests: XCTestCase { let query = try! QueryOn(orderedUsing: OrderParameter("sys.createdAt")) - QueryTests.client.fetchMappedEntries(with: query).then { catsResponse in - let cats = catsResponse.items - let ids = cats.map { $0.id } - expect(cats.count).to(equal(3)) - expect(ids).to(equal(QueryTests.orderedCatNames)) + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let catsResponse): + let cats = catsResponse.items + let ids = cats.map { $0.id } + expect(cats.count).to(equal(3)) + expect(ids).to(equal(QueryTests.orderedCatNames)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -483,12 +506,17 @@ class QueryTests: XCTestCase { let query = try! Query(orderedUsing: OrderParameter("sys.revision"), OrderParameter("sys.id")) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - let ids = entries.map { $0.sys.id } - expect(ids).to(equal(EntryTests.orderedEntriesByMultiple)) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + let ids = entries.map { $0.sys.id } + expect(ids).to(equal(EntryTests.orderedEntriesByMultiple)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -499,11 +527,16 @@ class QueryTests: XCTestCase { let query = try! QueryOn(searchingFor: "bacon") - QueryTests.client.fetchMappedEntries(with: query).then { dogsResponse in - let dogs = dogsResponse.items - expect(dogs.count).to(equal(1)) + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let dogsResponse): + let dogs = dogsResponse.items + expect(dogs.count).to(equal(1)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -512,12 +545,17 @@ class QueryTests: XCTestCase { let query = QueryOn(where: "fields.description", .matches("bacon pancakes")) - QueryTests.client.fetchMappedEntries(with: query).then { dogsResponse in - let dogs = dogsResponse.items - expect(dogs.count).to(equal(1)) - expect(dogs.first?.name).to(equal("Jake")) + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let dogsResponse): + let dogs = dogsResponse.items + expect(dogs.count).to(equal(1)) + expect(dogs.first?.name).to(equal("Jake")) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -528,29 +566,38 @@ class QueryTests: XCTestCase { func testFetchEntriesWithLocationProximitySearch() { let expectation = self.expectation(description: "Location proximity search") - let query = QueryOn(where: "fields.center", .isNear(CLLocationCoordinate2D(latitude: 38, longitude: -122))) + let query = QueryOn(where: "fields.center", .isNear(Location(latitude: 38, longitude: -122))) - QueryTests.client.fetchMappedEntries(with: query).then { citiesResponse in + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let citiesResponse): let cities = citiesResponse.items expect(cities.count).to(equal(4)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } - + } waitForExpectations(timeout: 10.0, handler: nil) } func testFetchEntriesWithBoundingBoxLocationsSearch() { let expectation = self.expectation(description: "Location bounding box") - let bounds = Bounds.box(bottomLeft: CLLocationCoordinate2D(latitude: 36, longitude: -124), topRight: CLLocationCoordinate2D(latitude: 40, longitude: -120)) + let bounds = Bounds.box(bottomLeft: Location(latitude: 36, longitude: -124), topRight: Location(latitude: 40, longitude: -120)) let query = QueryOn(where: "fields.center", .isWithin(bounds)) - QueryTests.client.fetchMappedEntries(with: query).then { citiesResponse in - let cities = citiesResponse.items - expect(cities.count).to(equal(1)) + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let citiesResponse): + let cities = citiesResponse.items + expect(cities.count).to(equal(1)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -561,11 +608,16 @@ class QueryTests: XCTestCase { let query = try! Query(limitingResultsTo: 5) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - expect(entries.count).to(equal(5)) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + expect(entries.count).to(equal(5)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -575,12 +627,17 @@ class QueryTests: XCTestCase { let query = Query(skippingTheFirst: 9) try! query.order(using: OrderParameter("sys.createdAt")) - QueryTests.client.fetchEntries(with: query).then { entriesResponse in - let entries = entriesResponse.items - expect(entries.count).to(equal(1)) - expect(entries.first?.sys.id).to(equal("7qVBlCjpWE86Oseo40gAEY")) + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let entriesResponse): + let entries = entriesResponse.items + expect(entries.count).to(equal(1)) + expect(entries.first?.sys.id).to(equal("7qVBlCjpWE86Oseo40gAEY")) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } @@ -592,16 +649,18 @@ class QueryTests: XCTestCase { let filterQuery = FilterQuery(where: "fields.name", .matches("Happy Cat")) let query = QueryOn(whereLinkAt: "bestFriend", matches: filterQuery) - QueryTests.client.fetchMappedEntries(with: query).then { catsWithHappyCatAsBestFriendResponse in - let catsWithHappyCatAsBestFriend = catsWithHappyCatAsBestFriendResponse.items - expect(catsWithHappyCatAsBestFriend.count).to(equal(1)) - expect(catsWithHappyCatAsBestFriend.first?.name).to(equal("Nyan Cat")) - expect(catsWithHappyCatAsBestFriend.first?.bestFriend?.name).to(equal("Happy Cat")) - expectation.fulfill() - }.error { error in + QueryTests.client.fetchMappedEntries(with: query) { result in + switch result { + case .success(let catsWithHappyCatAsBestFriendResponse): + let catsWithHappyCatAsBestFriend = catsWithHappyCatAsBestFriendResponse.items + expect(catsWithHappyCatAsBestFriend.count).to(equal(1)) + expect(catsWithHappyCatAsBestFriend.first?.name).to(equal("Nyan Cat")) + expect(catsWithHappyCatAsBestFriend.first?.bestFriend?.name).to(equal("Happy Cat")) + case .error(let error): fail("Should not throw an error \(error)") + } + expectation.fulfill() } - waitForExpectations(timeout: 10.0, handler: nil) } @@ -612,18 +671,21 @@ class QueryTests: XCTestCase { hasValueAt: "fields.name", ofType: "cat", that: .matches("Happy Cat")) - QueryTests.client.fetchEntries(with: query).then { catsWithHappyCatAsBestFriendResponse in - let catsWithHappyCatAsBestFriend = catsWithHappyCatAsBestFriendResponse.items - expect(catsWithHappyCatAsBestFriend.count).to(equal(1)) - expect(catsWithHappyCatAsBestFriend.first?.fields["name"] as? String).to(equal("Nyan Cat")) - if let happyCatsBestFriend = catsWithHappyCatAsBestFriend.first?.fields.linkedEntry(at: "bestFriend") { - expect(happyCatsBestFriend.fields.string(at: "name")).to(equal("Happy Cat")) - } else { - fail("Should be able to get linked entry.") + QueryTests.client.fetchEntries(with: query) { result in + switch result { + case .success(let catsWithHappyCatAsBestFriendResponse): + let catsWithHappyCatAsBestFriend = catsWithHappyCatAsBestFriendResponse.items + expect(catsWithHappyCatAsBestFriend.count).to(equal(1)) + expect(catsWithHappyCatAsBestFriend.first?.fields["name"] as? String).to(equal("Nyan Cat")) + if let happyCatsBestFriend = catsWithHappyCatAsBestFriend.first?.fields.linkedEntry(at: "bestFriend") { + expect(happyCatsBestFriend.fields.string(at: "name")).to(equal("Happy Cat")) + } else { + fail("Should be able to get linked entry.") + } + case .error(let error): + fail("Should not throw an error \(error)") } expectation.fulfill() - }.error { error in - fail("Should not throw an error \(error)") } waitForExpectations(timeout: 10.0, handler: nil) @@ -636,11 +698,16 @@ class QueryTests: XCTestCase { let query = AssetQuery(whereMimetypeGroupIs: .image) - QueryTests.client.fetchAssets(with: query).then { assetsResponse in - let assets = assetsResponse.items - expect(assets.count).to(equal(4)) + QueryTests.client.fetchAssets(with: query) { result in + switch result { + case .success(let assetsResponse): + let assets = assetsResponse.items + expect(assets.count).to(equal(4)) + case .error(let error): + fail("Should not throw an error \(error)") + } expectation.fulfill() - }.error { fail("\($0)") } + } waitForExpectations(timeout: 10.0, handler: nil) } From 2b5b9ea2b13c8fd9b207d6c7907c85c383854485 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Fri, 1 Sep 2017 13:55:37 +0200 Subject: [PATCH 02/10] Update Changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f7d9cf1..feb6a44f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,8 @@ This project adheres to [Semantic Versioning](http://semver.org/) starting from ### Merged, but not yet released > ~~All recent changes are published~~ -> Fixed -> - Ensured all functions and instance members had an explicit protection level set. +> #### Changed +> - **BREAKING:** [Interstellar](https://github.com/JensRavens/Interstellar) has been pruned, and therefore all method that previously returned an `Observable` are no longer part of the SDK. --- ## Table of contents From 82da891370767b920953aea7bb33e40f65ff46b2 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Fri, 1 Sep 2017 17:20:04 +0200 Subject: [PATCH 03/10] Migrate to Swift 4 --- Carthage/Checkouts/DVR | 2 +- Carthage/Checkouts/Nimble | 2 +- Contentful.xcodeproj/project.pbxproj | 72 ++++--- .../xcschemes/ContentfulTests.xcscheme | 4 +- .../xcschemes/Contentful_iOS.xcscheme | 4 +- .../xcschemes/Contentful_macOS.xcscheme | 4 +- .../xcschemes/Contentful_tvOS.xcscheme | 4 +- .../xcschemes/Contentful_watchOS.xcscheme | 4 +- Sources/Contentful/Client+Modellable.swift | 2 +- Sources/Contentful/ContentModellable.swift | 2 +- Sources/Contentful/Query.swift | 7 +- Sources/Contentful/Resource.swift | 4 +- Sources/Contentful/Result.swift | 201 ++++++++++++++++++ 13 files changed, 268 insertions(+), 44 deletions(-) create mode 100644 Sources/Contentful/Result.swift diff --git a/Carthage/Checkouts/DVR b/Carthage/Checkouts/DVR index e430723a..890067e0 160000 --- a/Carthage/Checkouts/DVR +++ b/Carthage/Checkouts/DVR @@ -1 +1 @@ -Subproject commit e430723af54c2b5b95b54d4a552dc3aa19778194 +Subproject commit 890067e0abb361e80514806f2232c3de4592bf28 diff --git a/Carthage/Checkouts/Nimble b/Carthage/Checkouts/Nimble index 39b67002..38c9ab08 160000 --- a/Carthage/Checkouts/Nimble +++ b/Carthage/Checkouts/Nimble @@ -1 +1 @@ -Subproject commit 39b67002306fda9de4c9fd1290a6295f97edd09e +Subproject commit 38c9ab0846a3fbec308eb2aa9ef68b10a7434eb4 diff --git a/Contentful.xcodeproj/project.pbxproj b/Contentful.xcodeproj/project.pbxproj index ef133a8b..12ea4db5 100644 --- a/Contentful.xcodeproj/project.pbxproj +++ b/Contentful.xcodeproj/project.pbxproj @@ -248,7 +248,6 @@ ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; ED0876D91E1D07B3008E1A06 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; ED1638C41ED5CA6D009BAA9F /* ImageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageTests.swift; sourceTree = ""; }; - ED1B1E611EB370F900347DD7 /* Interstellar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Interstellar.framework; path = Carthage/Build/iOS/Interstellar.framework; sourceTree = ""; }; ED1D78991F2F96260048E169 /* DataCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataCache.swift; sourceTree = ""; }; ED1D789E1F3069FF0048E169 /* Client+Modellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Client+Modellable.swift"; sourceTree = ""; }; ED2AD58B1F3B08BC00C101E6 /* Client+Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Client+Query.swift"; sourceTree = ""; }; @@ -264,9 +263,6 @@ ED4937DC1ECB8A5B008E6860 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; ED52DB3F1EE94C8E00225140 /* RateLimitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimitTests.swift; sourceTree = ""; }; ED65CF951E3F6BF8000EBC62 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; - ED65CF9D1E3F6C2A000EBC62 /* Interstellar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Interstellar.framework; path = Carthage/Build/Mac/Interstellar.framework; sourceTree = ""; }; - ED65CFA11E3F6C3B000EBC62 /* Interstellar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Interstellar.framework; path = Carthage/Build/tvOS/Interstellar.framework; sourceTree = ""; }; - ED65CFA51E3F6C55000EBC62 /* Interstellar.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Interstellar.framework; path = Carthage/Build/watchOS/Interstellar.framework; sourceTree = ""; }; ED68BD711E6DC47A00939F6D /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Query.swift; sourceTree = ""; }; ED6EA9081F44675C00FCA0F2 /* QueryTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = QueryTests.json; sourceTree = ""; }; ED737C691EF0095200ECB3F1 /* AssetTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = AssetTests.json; sourceTree = ""; }; @@ -499,7 +495,6 @@ ED9102651E65AADA0076FDBD /* DVR.framework */, ED65CF951E3F6BF8000EBC62 /* Nimble.framework */, ED3CBF101E7A98DD00E5B02E /* ObjectMapper.framework */, - ED1B1E611EB370F900347DD7 /* Interstellar.framework */, ); name = iOS; sourceTree = ""; @@ -510,7 +505,6 @@ EDA690CA1F4319AC00581FC6 /* DVR.framework */, EDA690C81F4319A000581FC6 /* Nimble.framework */, ED3CBF121E7A98EB00E5B02E /* ObjectMapper.framework */, - ED65CF9D1E3F6C2A000EBC62 /* Interstellar.framework */, ); name = macOS; sourceTree = ""; @@ -521,7 +515,6 @@ EDA690CC1F431A0C00581FC6 /* DVR.framework */, EDA690CD1F431A0C00581FC6 /* Nimble.framework */, ED3CBF0E1E7A98CF00E5B02E /* ObjectMapper.framework */, - ED65CFA11E3F6C3B000EBC62 /* Interstellar.framework */, ); name = tvOS; sourceTree = ""; @@ -530,7 +523,6 @@ isa = PBXGroup; children = ( ED3CBF161E7A990700E5B02E /* ObjectMapper.framework */, - ED65CFA51E3F6C55000EBC62 /* Interstellar.framework */, ); name = watchOS; sourceTree = ""; @@ -736,7 +728,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0830; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 0900; ORGANIZATIONNAME = "Contentful GmbH"; TargetAttributes = { A19CA3C31B836EDD00A0EFCD = { @@ -750,6 +742,7 @@ EDA690731F43185200581FC6 = { CreatedOnToolsVersion = 8.3.3; DevelopmentTeam = RWJ5E97L7R; + LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; EDA690831F43186600581FC6 = { @@ -767,6 +760,7 @@ }; EDDC07281E3BCEB10022F2F9 = { CreatedOnToolsVersion = 8.2.1; + LastSwiftMigration = 0900; ProvisioningStyle = Automatic; }; }; @@ -1155,14 +1149,20 @@ CLANG_ENABLE_CODE_COVERAGE = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -1210,14 +1210,20 @@ CLANG_ENABLE_CODE_COVERAGE = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -1270,7 +1276,7 @@ PRODUCT_NAME = Contentful; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1296,7 +1302,7 @@ PRODUCT_NAME = Contentful; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1314,7 +1320,7 @@ OTHER_SWIFT_FLAGS = "-D DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.contentful.ContentfulTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1332,7 +1338,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.contentful.ContentfulTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1354,7 +1360,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1379,7 +1385,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = API_Coverage; }; @@ -1401,7 +1407,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = macosx; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1420,7 +1426,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Debug; @@ -1443,7 +1449,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = API_Coverage; @@ -1463,7 +1469,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TVOS_DEPLOYMENT_TARGET = 10.2; }; name = Release; @@ -1490,7 +1496,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 3.1; }; @@ -1518,7 +1524,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 3.1; }; @@ -1546,7 +1552,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 10.1; }; @@ -1574,7 +1580,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 10.1; }; @@ -1605,7 +1611,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -1634,7 +1640,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = Release; }; @@ -1649,14 +1655,20 @@ CLANG_ENABLE_CODE_COVERAGE = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -1718,7 +1730,7 @@ PRODUCT_NAME = Contentful; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = API_Coverage; }; @@ -1739,7 +1751,7 @@ OTHER_SWIFT_FLAGS = "-D API_COVERAGE"; PRODUCT_BUNDLE_IDENTIFIER = com.contentful.ContentfulTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = API_Coverage; }; @@ -1768,7 +1780,7 @@ SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 4; WATCHOS_DEPLOYMENT_TARGET = 3.1; }; @@ -1799,7 +1811,7 @@ SDKROOT = appletvos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 10.1; }; @@ -1833,7 +1845,7 @@ SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 4.0; }; name = API_Coverage; }; diff --git a/Contentful.xcodeproj/xcshareddata/xcschemes/ContentfulTests.xcscheme b/Contentful.xcodeproj/xcshareddata/xcschemes/ContentfulTests.xcscheme index fc7b4e07..df39e0ca 100644 --- a/Contentful.xcodeproj/xcshareddata/xcschemes/ContentfulTests.xcscheme +++ b/Contentful.xcodeproj/xcshareddata/xcschemes/ContentfulTests.xcscheme @@ -1,6 +1,6 @@ @@ -31,6 +32,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_iOS.xcscheme b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_iOS.xcscheme index 947c5f7a..40a174ce 100644 --- a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_iOS.xcscheme +++ b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_iOS.xcscheme @@ -1,6 +1,6 @@ @@ -56,6 +57,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_macOS.xcscheme b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_macOS.xcscheme index 986a2112..dfe4eb4c 100644 --- a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_macOS.xcscheme +++ b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_macOS.xcscheme @@ -1,6 +1,6 @@ @@ -56,6 +57,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_tvOS.xcscheme b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_tvOS.xcscheme index 67e66a37..18fe8ad4 100644 --- a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_tvOS.xcscheme +++ b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_tvOS.xcscheme @@ -1,6 +1,6 @@ @@ -56,6 +57,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_watchOS.xcscheme b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_watchOS.xcscheme index bcc24701..8a3a880a 100644 --- a/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_watchOS.xcscheme +++ b/Contentful.xcodeproj/xcshareddata/xcschemes/Contentful_watchOS.xcscheme @@ -1,6 +1,6 @@ @@ -37,6 +38,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/Sources/Contentful/Client+Modellable.swift b/Sources/Contentful/Client+Modellable.swift index 4eaa93ac..9c6392e4 100644 --- a/Sources/Contentful/Client+Modellable.swift +++ b/Sources/Contentful/Client+Modellable.swift @@ -76,7 +76,7 @@ extension Client { */ @discardableResult public func fetchMappedEntries(with query: QueryOn, - then completion: @escaping ResultsHandler>) -> URLSessionDataTask? where EntryType: EntryModellable { + then completion: @escaping ResultsHandler>) -> URLSessionDataTask? { guard let contentModel = self.contentModel else { return nil } diff --git a/Sources/Contentful/ContentModellable.swift b/Sources/Contentful/ContentModellable.swift index 4258eb21..5d793c49 100644 --- a/Sources/Contentful/ContentModellable.swift +++ b/Sources/Contentful/ContentModellable.swift @@ -84,7 +84,7 @@ public class ContentModel { let mirror = Mirror(reflecting: emptyInstance) let relationshipNames: [String] = mirror.children.flatMap { propertyName, value in - let type = type(of: value) + let type = Swift.type(of: value) // Filter out relationship names. if type is EntryModellable.Type || type is Asset.Type { diff --git a/Sources/Contentful/Query.swift b/Sources/Contentful/Query.swift index 1cbf10f5..1ea14f08 100644 --- a/Sources/Contentful/Query.swift +++ b/Sources/Contentful/Query.swift @@ -77,8 +77,9 @@ extension Date: QueryableRange { extra linking requirements for the SDK. */ public struct Location { - let latitude: Double - let longitude: Double + + public let latitude: Double + public let longitude: Double public init(latitude: Double, longitude: Double) { self.latitude = latitude @@ -837,7 +838,7 @@ public final class QueryOn: ChainableQuery where EntryType: EntryMode set on the `Client` instance is used. */ public convenience init(whereLinkAt fieldNameForLink: String, matches filterQuery: FilterQuery? = nil, - for locale: String? = nil) where LinkType: EntryModellable { + for locale: String? = nil) { self.init() self.parameters["fields.\(fieldNameForLink).sys.contentType.sys.id"] = LinkType.contentTypeId diff --git a/Sources/Contentful/Resource.swift b/Sources/Contentful/Resource.swift index 81d2bf55..44552f48 100644 --- a/Sources/Contentful/Resource.swift +++ b/Sources/Contentful/Resource.swift @@ -259,12 +259,12 @@ public func == (lhs: Resource, rhs: Resource) -> Bool { } -internal func += (left: [K: V], right: [K: V]) -> [K: V] { +internal func += (left: [K: V], right: [K: V]) -> [K: V] { var result = left right.forEach { (key, value) in result[key] = value } return result } -internal func + (left: [K: V], right: [K: V]) -> [K: V] { +internal func + (left: [K: V], right: [K: V]) -> [K: V] { return left += right } diff --git a/Sources/Contentful/Result.swift b/Sources/Contentful/Result.swift new file mode 100644 index 00000000..b1dc6a54 --- /dev/null +++ b/Sources/Contentful/Result.swift @@ -0,0 +1,201 @@ +// Result.swift +// +// Copyright (c) 2015 Jens Ravens (http://jensravens.de) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + + +/// Conform to ResultType to use your own result type, e.g. from other libraries with Interstellar. +public protocol ResultType { + /// Describes the contained successful type of this result. + associatedtype Value + + /// Return an error if the result is unsuccessful, otherwise nil. + var error: Error? { get } + + /// Return the value if the result is successful, otherwise nil. + var value: Value? { get } + + /// Convert this result into an `Interstellar.Result`. This implementation is optional. + var result: Result { get } +} + +extension ResultType { + public var result: Result { + return Result(value: value, error: error) + } +} + +extension Result { + public init(value: T?, error: Error?) { + if let error = error { + self = .error(error) + } else { + self = .success(value!) + } + } + + public init(block: () throws -> T) { + do { + self = try .success(block()) + } catch let e { + self = .error(e) + } + } + + public var result: Result { + return self + } +} + + +/** + A result contains the result of a computation or task. It might be either successfull + with an attached value or a failure with an attached error (which conforms to Swift 2's + ErrorType). You can read more about the implementation in + [this blog post](http://jensravens.de/a-swifter-way-of-handling-errors/). + */ +public enum Result: ResultType { + case success(T) + case error(Error) + + /** + Initialize a result containing a successful value. + */ + public init(success value: T) { + self = Result.success(value) + } + + /** + Initialize a result containing an error + */ + public init(error: Error) { + self = .error(error) + } + + /** + Transform a result into another result using a function. If the result was an error, + the function will not be executed and the error returned instead. + */ + public func map(_ f: @escaping (T) -> U) -> Result { + switch self { + case let .success(v): return .success(f(v)) + case let .error(error): return .error(error) + } + } + + /** + Transform a result into another result using a function. If the result was an error, + the function will not be executed and the error returned instead. + */ + public func flatMap(_ f: (T) -> Result) -> Result { + switch self { + case let .success(v): return f(v) + case let .error(error): return .error(error) + } + } + + /** + Transform a result into another result using a function. If the result was an error, + the function will not be executed and the error returned instead. + */ + public func flatMap(_ f: (T) throws -> U) -> Result { + return flatMap { t in + do { + return .success(try f(t)) + } catch let error { + return .error(error) + } + } + } + /** + Transform a result into another result using a function. If the result was an error, + the function will not be executed and the error returned instead. + */ + public func flatMap(_ f:@escaping (T, (@escaping(Result)->Void))->Void) -> (@escaping(Result)->Void)->Void { + return { g in + switch self { + case let .success(v): f(v, g) + case let .error(error): g(.error(error)) + } + } + } + + /** + Call a function with the result as an argument. Use this if the function should be + executed no matter if the result was a success or not. + */ + public func ensure(_ f: (Result) -> Result) -> Result { + return f(self) + } + + /** + Call a function with the result as an argument. Use this if the function should be + executed no matter if the result was a success or not. + */ + public func ensure(_ f:@escaping (Result, ((Result)->Void))->Void) -> ((Result)->Void)->Void { + return { g in + f(self, g) + } + } + + /** + Direct access to the content of the result as an optional. If the result was a success, + the optional will contain the value of the result. + */ + public var value: T? { + switch self { + case let .success(v): return v + case .error(_): return nil + } + } + + /** + Direct access to the error of the result as an optional. If the result was an error, + the optional will contain the error of the result. + */ + public var error: Error? { + switch self { + case .success: return nil + case .error(let x): return x + } + } + + /** + Access the value of this result. If the result contains an error, that error is thrown. + */ + public func get() throws -> T { + switch self { + case let .success(value): return value + case .error(let error): throw error + } + } +} + + +/** + Provide a default value for failed results. + */ +public func ?? (result: Result, defaultValue: @autoclosure () -> T) -> T { + switch result { + case .success(let x): return x + case .error: return defaultValue() + } +} From ceab2de7614d5711abc92cd43740254702452db6 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Mon, 4 Sep 2017 18:19:09 +0200 Subject: [PATCH 04/10] Prune ObjectMapper; use Swift 4 Decodable protocols instead Write extensions to decode arrays and dictionarys Reinstate self made hex string transform --- .gitmodules | 3 - .swiftlint.yml | 3 +- Cartfile | 1 - Cartfile.resolved | 1 - Carthage/Checkouts/ObjectMapper | 1 - Contentful.xcodeproj/project.pbxproj | 74 +++---- .../contents.xcworkspacedata | 3 - Sources/Contentful/ArrayResponse.swift | 50 +++-- Sources/Contentful/Asset.swift | 86 ++++---- Sources/Contentful/Client+Modellable.swift | 2 +- Sources/Contentful/Client.swift | 50 ++--- Sources/Contentful/ContentType.swift | 21 +- Sources/Contentful/Date.swift | 15 -- Sources/Contentful/Decodable.swift | 105 +++++++++ Sources/Contentful/Entry.swift | 2 +- Sources/Contentful/Error.swift | 51 +++-- Sources/Contentful/Field.swift | 46 ++-- Sources/Contentful/ImageOptions.swift | 42 +++- Sources/Contentful/Link.swift | 38 ++-- Sources/Contentful/Locale.swift | 35 ++- Sources/Contentful/Resource.swift | 70 +++--- Sources/Contentful/Result.swift | 201 ------------------ Sources/Contentful/Space.swift | 20 +- Sources/Contentful/SyncSpace.swift | 63 +++--- Sources/Contentful/Sys.swift | 45 ++-- Tests/ContentfulTests/ImageTests.swift | 15 +- ...ingTests.swift => JSONDecodingTests.swift} | 84 ++++---- Tests/ContentfulTests/LocalizationTests.swift | 23 +- 28 files changed, 570 insertions(+), 580 deletions(-) delete mode 160000 Carthage/Checkouts/ObjectMapper create mode 100644 Sources/Contentful/Decodable.swift delete mode 100644 Sources/Contentful/Result.swift rename Tests/ContentfulTests/{ObjectMappingTests.swift => JSONDecodingTests.swift} (53%) diff --git a/.gitmodules b/.gitmodules index e609276c..27a96f99 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "Carthage/Checkouts/ObjectMapper"] - path = Carthage/Checkouts/ObjectMapper - url = https://github.com/Hearst-DD/ObjectMapper.git [submodule "Carthage/Checkouts/Interstellar"] path = Carthage/Checkouts/Interstellar url = https://github.com/JensRavens/Interstellar.git diff --git a/.swiftlint.yml b/.swiftlint.yml index 811ece89..040aa4bf 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -21,7 +21,8 @@ disabled_rules: - vertical_parameter_alignment # ignoring since some file that exists on travis, but not locally is throwing this as a compile error # Also, swiftlint says it will be removed in future versions -- identifier_name +- identifier_name +- nesting # Disable warnings about structures nested more than 1 level deep. # Parameterized line_length: diff --git a/Cartfile b/Cartfile index 84f2b39f..8a05acd5 100644 --- a/Cartfile +++ b/Cartfile @@ -1,3 +1,2 @@ github "JensRavens/Interstellar" ~> 2.1.0 -github "Hearst-DD/ObjectMapper" ~> 2.2 diff --git a/Cartfile.resolved b/Cartfile.resolved index 799d185a..bebcc79b 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,3 @@ -github "Hearst-DD/ObjectMapper" "2.2.8" github "JensRavens/Interstellar" "2.1.0" github "Quick/Nimble" "v7.0.1" github "venmo/DVR" "v1.0.1" diff --git a/Carthage/Checkouts/ObjectMapper b/Carthage/Checkouts/ObjectMapper deleted file mode 160000 index 40916302..00000000 --- a/Carthage/Checkouts/ObjectMapper +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 40916302f077985bbb017b7ed6ddff7256c67479 diff --git a/Contentful.xcodeproj/project.pbxproj b/Contentful.xcodeproj/project.pbxproj index 12ea4db5..7e9546b4 100644 --- a/Contentful.xcodeproj/project.pbxproj +++ b/Contentful.xcodeproj/project.pbxproj @@ -7,7 +7,7 @@ objects = { /* Begin PBXBuildFile section */ - A10FF9D21BBB2E5F001AA4E9 /* ObjectMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* ObjectMappingTests.swift */; }; + A10FF9D21BBB2E5F001AA4E9 /* JSONDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* JSONDecodingTests.swift */; }; A10FF9D31BBB32FD001AA4E9 /* Asset.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15193AD1BB9236300FB83CD /* Asset.swift */; }; A10FF9D51BBB3676001AA4E9 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = A10FF9D41BBB3676001AA4E9 /* Error.swift */; }; A10FF9D81BBB3860001AA4E9 /* Data in Resources */ = {isa = PBXBuildFile; fileRef = A10FF9D61BBB3753001AA4E9 /* Data */; }; @@ -31,7 +31,6 @@ ED02BC611E7047B900BAB2CA /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */; }; ED0876DA1E1D07B3008E1A06 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0876D91E1D07B3008E1A06 /* Date.swift */; }; ED1638C51ED5CA6D009BAA9F /* ImageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1638C41ED5CA6D009BAA9F /* ImageTests.swift */; }; - ED1B1E621EB370F900347DD7 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED1B1E611EB370F900347DD7 /* Interstellar.framework */; }; ED1D789A1F2F96260048E169 /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D78991F2F96260048E169 /* DataCache.swift */; }; ED1D789B1F2F96260048E169 /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D78991F2F96260048E169 /* DataCache.swift */; }; ED1D789C1F2F96260048E169 /* DataCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED1D78991F2F96260048E169 /* DataCache.swift */; }; @@ -92,10 +91,6 @@ ED3CBF0B1E793F8200E5B02E /* ContentModellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */; }; ED3CBF0C1E793F8200E5B02E /* ContentModellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */; }; ED3CBF0D1E793F8200E5B02E /* ContentModellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */; }; - ED3CBF0F1E7A98CF00E5B02E /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3CBF0E1E7A98CF00E5B02E /* ObjectMapper.framework */; }; - ED3CBF111E7A98DD00E5B02E /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3CBF101E7A98DD00E5B02E /* ObjectMapper.framework */; }; - ED3CBF131E7A98EB00E5B02E /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3CBF121E7A98EB00E5B02E /* ObjectMapper.framework */; }; - ED3CBF171E7A990700E5B02E /* ObjectMapper.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3CBF161E7A990700E5B02E /* ObjectMapper.framework */; }; ED3CBF181E7A991500E5B02E /* ContentModellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */; }; ED3CBF1A1E7AD57100E5B02E /* Sys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF191E7AD57100E5B02E /* Sys.swift */; }; ED3CBF1B1E7AD57100E5B02E /* Sys.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF191E7AD57100E5B02E /* Sys.swift */; }; @@ -106,10 +101,11 @@ ED3CBF211E7AD58100E5B02E /* Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF1E1E7AD58100E5B02E /* Link.swift */; }; ED3CBF221E7AD58100E5B02E /* Link.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED3CBF1E1E7AD58100E5B02E /* Link.swift */; }; ED52DB401EE94C8E00225140 /* RateLimitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED52DB3F1EE94C8E00225140 /* RateLimitTests.swift */; }; + ED535E641F5E8E1300886D93 /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED535E631F5E8E1300886D93 /* Decodable.swift */; }; + ED535E651F5E8E1300886D93 /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED535E631F5E8E1300886D93 /* Decodable.swift */; }; + ED535E661F5E8E1300886D93 /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED535E631F5E8E1300886D93 /* Decodable.swift */; }; + ED535E671F5E8E1300886D93 /* Decodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED535E631F5E8E1300886D93 /* Decodable.swift */; }; ED65CF981E3F6BF8000EBC62 /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED65CF951E3F6BF8000EBC62 /* Nimble.framework */; }; - ED65CF9F1E3F6C2A000EBC62 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED65CF9D1E3F6C2A000EBC62 /* Interstellar.framework */; }; - ED65CFA31E3F6C3B000EBC62 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED65CFA11E3F6C3B000EBC62 /* Interstellar.framework */; }; - ED65CFA71E3F6C55000EBC62 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED65CFA51E3F6C55000EBC62 /* Interstellar.framework */; }; ED68BD721E6DC47A00939F6D /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED68BD711E6DC47A00939F6D /* Query.swift */; }; ED68BD731E6DC47F00939F6D /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED68BD711E6DC47A00939F6D /* Query.swift */; }; ED68BD741E6DC48000939F6D /* Query.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED68BD711E6DC47A00939F6D /* Query.swift */; }; @@ -131,6 +127,10 @@ ED737C951EF1785A00ECB3F1 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED737C931EF1785A00ECB3F1 /* Persistence.swift */; }; ED737C961EF1785A00ECB3F1 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED737C931EF1785A00ECB3F1 /* Persistence.swift */; }; ED737C971EF1785A00ECB3F1 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED737C931EF1785A00ECB3F1 /* Persistence.swift */; }; + ED7862721F7D2F9400CB2625 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED7862731F7D2F9400CB2625 /* Interstellar.framework */; }; + ED7862741F7D2FAC00CB2625 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED7862751F7D2FAC00CB2625 /* Interstellar.framework */; }; + ED7862761F7D2FB300CB2625 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED7862771F7D2FB300CB2625 /* Interstellar.framework */; }; + ED7862781F7D2FBD00CB2625 /* Interstellar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED7862791F7D2FBD00CB2625 /* Interstellar.framework */; }; ED88B7DA1EF7CD3800538D1F /* ClientConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED88B7D91EF7CD3800538D1F /* ClientConfigurationTests.swift */; }; ED9102661E65AADA0076FDBD /* DVR.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED9102651E65AADA0076FDBD /* DVR.framework */; }; ED91C1401EDCB03600F0FC0A /* ImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED91C13F1EDCB03600F0FC0A /* ImageOptions.swift */; }; @@ -145,7 +145,7 @@ EDA690941F43187B00581FC6 /* ClientConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED88B7D91EF7CD3800538D1F /* ClientConfigurationTests.swift */; }; EDA690951F43187B00581FC6 /* ContentTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19C743E1C47A7F5005334AD /* ContentTypeTests.swift */; }; EDA690961F43187B00581FC6 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED307F211EE6E5D700633390 /* LocalizationTests.swift */; }; - EDA690971F43187B00581FC6 /* ObjectMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* ObjectMappingTests.swift */; }; + EDA690971F43187B00581FC6 /* JSONDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* JSONDecodingTests.swift */; }; EDA690981F43187B00581FC6 /* EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19C74401C47A804005334AD /* EntryTests.swift */; }; EDA690991F43187B00581FC6 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */; }; EDA6909A1F43187B00581FC6 /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A114BBC11C50E9E200DE6679 /* SyncTests.swift */; }; @@ -157,7 +157,7 @@ EDA690A01F43187C00581FC6 /* ClientConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED88B7D91EF7CD3800538D1F /* ClientConfigurationTests.swift */; }; EDA690A11F43187C00581FC6 /* ContentTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19C743E1C47A7F5005334AD /* ContentTypeTests.swift */; }; EDA690A21F43187C00581FC6 /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED307F211EE6E5D700633390 /* LocalizationTests.swift */; }; - EDA690A31F43187C00581FC6 /* ObjectMappingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* ObjectMappingTests.swift */; }; + EDA690A31F43187C00581FC6 /* JSONDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2E3661BBAD06000814D63 /* JSONDecodingTests.swift */; }; EDA690A41F43187C00581FC6 /* EntryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A19C74401C47A804005334AD /* EntryTests.swift */; }; EDA690A51F43187C00581FC6 /* QueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */; }; EDA690A61F43187C00581FC6 /* SyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A114BBC11C50E9E200DE6679 /* SyncTests.swift */; }; @@ -243,7 +243,7 @@ A19CA3E51B8388ED00A0EFCD /* Entry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Entry.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A19CA3E71B83890D00A0EFCD /* Resource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Resource.swift; sourceTree = ""; }; A1B2E3641BBACF2C00814D63 /* Locale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Locale.swift; sourceTree = ""; }; - A1B2E3661BBAD06000814D63 /* ObjectMappingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = ObjectMappingTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + A1B2E3661BBAD06000814D63 /* JSONDecodingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = JSONDecodingTests.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; A1BF6BBB1C0D0FBD00049712 /* SignalUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SignalUtils.swift; sourceTree = ""; }; ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryTests.swift; sourceTree = ""; }; ED0876D91E1D07B3008E1A06 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; @@ -254,14 +254,11 @@ ED2C99791ECDD0AF0039056E /* Client+UIKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Client+UIKit.swift"; sourceTree = ""; }; ED307F211EE6E5D700633390 /* LocalizationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; }; ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentModellable.swift; sourceTree = ""; }; - ED3CBF0E1E7A98CF00E5B02E /* ObjectMapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjectMapper.framework; path = Carthage/Build/tvOS/ObjectMapper.framework; sourceTree = ""; }; - ED3CBF101E7A98DD00E5B02E /* ObjectMapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjectMapper.framework; path = Carthage/Build/iOS/ObjectMapper.framework; sourceTree = ""; }; - ED3CBF121E7A98EB00E5B02E /* ObjectMapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjectMapper.framework; path = Carthage/Build/Mac/ObjectMapper.framework; sourceTree = ""; }; - ED3CBF161E7A990700E5B02E /* ObjectMapper.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ObjectMapper.framework; path = Carthage/Build/watchOS/ObjectMapper.framework; sourceTree = ""; }; ED3CBF191E7AD57100E5B02E /* Sys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Sys.swift; sourceTree = ""; }; ED3CBF1E1E7AD58100E5B02E /* Link.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Link.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; ED4937DC1ECB8A5B008E6860 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; ED52DB3F1EE94C8E00225140 /* RateLimitTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RateLimitTests.swift; sourceTree = ""; }; + ED535E631F5E8E1300886D93 /* Decodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Decodable.swift; sourceTree = ""; }; ED65CF951E3F6BF8000EBC62 /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; ED68BD711E6DC47A00939F6D /* Query.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = Query.swift; sourceTree = ""; }; ED6EA9081F44675C00FCA0F2 /* QueryTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = QueryTests.json; sourceTree = ""; }; @@ -276,6 +273,10 @@ ED737C871EF15CC600ECB3F1 /* PreviewSyncTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = PreviewSyncTests.json; sourceTree = ""; }; ED737C901EF1748700ECB3F1 /* SyncTests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SyncTests.json; sourceTree = ""; }; ED737C931EF1785A00ECB3F1 /* Persistence.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; + ED7862731F7D2F9400CB2625 /* Interstellar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Interstellar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ED7862751F7D2FAC00CB2625 /* Interstellar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Interstellar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ED7862771F7D2FB300CB2625 /* Interstellar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Interstellar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + ED7862791F7D2FBD00CB2625 /* Interstellar.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Interstellar.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED88B7D91EF7CD3800538D1F /* ClientConfigurationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClientConfigurationTests.swift; sourceTree = ""; }; ED9102651E65AADA0076FDBD /* DVR.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = DVR.framework; path = Carthage/Build/iOS/DVR.framework; sourceTree = ""; }; ED91C13F1EDCB03600F0FC0A /* ImageOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageOptions.swift; sourceTree = ""; }; @@ -300,8 +301,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED3CBF111E7A98DD00E5B02E /* ObjectMapper.framework in Frameworks */, - ED1B1E621EB370F900347DD7 /* Interstellar.framework in Frameworks */, + ED7862721F7D2F9400CB2625 /* Interstellar.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -336,8 +336,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED3CBF171E7A990700E5B02E /* ObjectMapper.framework in Frameworks */, - ED65CFA71E3F6C55000EBC62 /* Interstellar.framework in Frameworks */, + ED7862741F7D2FAC00CB2625 /* Interstellar.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -345,8 +344,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED3CBF0F1E7A98CF00E5B02E /* ObjectMapper.framework in Frameworks */, - ED65CFA31E3F6C3B000EBC62 /* Interstellar.framework in Frameworks */, + ED7862761F7D2FB300CB2625 /* Interstellar.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -354,8 +352,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - ED3CBF131E7A98EB00E5B02E /* ObjectMapper.framework in Frameworks */, - ED65CF9F1E3F6C2A000EBC62 /* Interstellar.framework in Frameworks */, + ED7862781F7D2FBD00CB2625 /* Interstellar.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -429,6 +426,7 @@ A15193AD1BB9236300FB83CD /* Asset.swift */, A19CA3E31B83888200A0EFCD /* ArrayResponse.swift */, ED3CBF1E1E7AD58100E5B02E /* Link.swift */, + ED535E631F5E8E1300886D93 /* Decodable.swift */, ED91C13F1EDCB03600F0FC0A /* ImageOptions.swift */, ED1D789E1F3069FF0048E169 /* Client+Modellable.swift */, ED3CBF0A1E793F8200E5B02E /* ContentModellable.swift */, @@ -460,7 +458,7 @@ ED88B7D91EF7CD3800538D1F /* ClientConfigurationTests.swift */, A19C743E1C47A7F5005334AD /* ContentTypeTests.swift */, ED307F211EE6E5D700633390 /* LocalizationTests.swift */, - A1B2E3661BBAD06000814D63 /* ObjectMappingTests.swift */, + A1B2E3661BBAD06000814D63 /* JSONDecodingTests.swift */, A19C74401C47A804005334AD /* EntryTests.swift */, ED02BC5F1E7047AE00BAB2CA /* QueryTests.swift */, A114BBC11C50E9E200DE6679 /* SyncTests.swift */, @@ -481,10 +479,13 @@ ED65CF8A1E3F6B87000EBC62 /* Frameworks */ = { isa = PBXGroup; children = ( + ED7862791F7D2FBD00CB2625 /* Interstellar.framework */, + ED7862771F7D2FB300CB2625 /* Interstellar.framework */, + ED7862751F7D2FAC00CB2625 /* Interstellar.framework */, + ED7862731F7D2F9400CB2625 /* Interstellar.framework */, ED65CF8B1E3F6B9E000EBC62 /* iOS */, ED65CF8C1E3F6BA2000EBC62 /* macOS */, ED65CF8D1E3F6BA8000EBC62 /* tvOS */, - ED65CF8E1E3F6BAD000EBC62 /* watchOS */, ); name = Frameworks; sourceTree = ""; @@ -494,7 +495,6 @@ children = ( ED9102651E65AADA0076FDBD /* DVR.framework */, ED65CF951E3F6BF8000EBC62 /* Nimble.framework */, - ED3CBF101E7A98DD00E5B02E /* ObjectMapper.framework */, ); name = iOS; sourceTree = ""; @@ -504,7 +504,6 @@ children = ( EDA690CA1F4319AC00581FC6 /* DVR.framework */, EDA690C81F4319A000581FC6 /* Nimble.framework */, - ED3CBF121E7A98EB00E5B02E /* ObjectMapper.framework */, ); name = macOS; sourceTree = ""; @@ -514,19 +513,10 @@ children = ( EDA690CC1F431A0C00581FC6 /* DVR.framework */, EDA690CD1F431A0C00581FC6 /* Nimble.framework */, - ED3CBF0E1E7A98CF00E5B02E /* ObjectMapper.framework */, ); name = tvOS; sourceTree = ""; }; - ED65CF8E1E3F6BAD000EBC62 /* watchOS */ = { - isa = PBXGroup; - children = ( - ED3CBF161E7A990700E5B02E /* ObjectMapper.framework */, - ); - name = watchOS; - sourceTree = ""; - }; ED9102671E65AE880076FDBD /* DVRRecordings */ = { isa = PBXGroup; children = ( @@ -947,6 +937,7 @@ A19CA3E21B8386CC00A0EFCD /* ClientConfiguration.swift in Sources */, ED2AD58C1F3B08BC00C101E6 /* Client+Query.swift in Sources */, A10FF9DA1BBB4680001AA4E9 /* ContentType.swift in Sources */, + ED535E641F5E8E1300886D93 /* Decodable.swift in Sources */, ED1D789A1F2F96260048E169 /* DataCache.swift in Sources */, ED91C1401EDCB03600F0FC0A /* ImageOptions.swift in Sources */, ED1D789F1F3069FF0048E169 /* Client+Modellable.swift in Sources */, @@ -980,7 +971,7 @@ ED02BC611E7047B900BAB2CA /* QueryTests.swift in Sources */, ED88B7DA1EF7CD3800538D1F /* ClientConfigurationTests.swift in Sources */, ED307F231EE6E5DF00633390 /* LocalizationTests.swift in Sources */, - A10FF9D21BBB2E5F001AA4E9 /* ObjectMappingTests.swift in Sources */, + A10FF9D21BBB2E5F001AA4E9 /* JSONDecodingTests.swift in Sources */, A19C74431C47A812005334AD /* AssetTests.swift in Sources */, A19CA3D41B836EDD00A0EFCD /* ContentfulTests.swift in Sources */, ); @@ -996,7 +987,7 @@ EDA6909A1F43187B00581FC6 /* SyncTests.swift in Sources */, EDA690981F43187B00581FC6 /* EntryTests.swift in Sources */, EDA690921F43187B00581FC6 /* ImageTests.swift in Sources */, - EDA690971F43187B00581FC6 /* ObjectMappingTests.swift in Sources */, + EDA690971F43187B00581FC6 /* JSONDecodingTests.swift in Sources */, EDA690951F43187B00581FC6 /* ContentTypeTests.swift in Sources */, EDA690911F43187B00581FC6 /* RateLimitTests.swift in Sources */, EDA690961F43187B00581FC6 /* LocalizationTests.swift in Sources */, @@ -1015,7 +1006,7 @@ EDA690A61F43187C00581FC6 /* SyncTests.swift in Sources */, EDA690A41F43187C00581FC6 /* EntryTests.swift in Sources */, EDA6909E1F43187C00581FC6 /* ImageTests.swift in Sources */, - EDA690A31F43187C00581FC6 /* ObjectMappingTests.swift in Sources */, + EDA690A31F43187C00581FC6 /* JSONDecodingTests.swift in Sources */, EDA690A11F43187C00581FC6 /* ContentTypeTests.swift in Sources */, EDA6909D1F43187C00581FC6 /* RateLimitTests.swift in Sources */, EDA690A21F43187C00581FC6 /* LocalizationTests.swift in Sources */, @@ -1036,6 +1027,7 @@ ED2AD58D1F3B08BC00C101E6 /* Client+Query.swift in Sources */, ED36B7001E3BD052005581FD /* Field.swift in Sources */, ED1D789B1F2F96260048E169 /* DataCache.swift in Sources */, + ED535E651F5E8E1300886D93 /* Decodable.swift in Sources */, ED36B7031E3BD052005581FD /* Space.swift in Sources */, ED1D78A01F3069FF0048E169 /* Client+Modellable.swift in Sources */, ED36B6F71E3BD052005581FD /* Asset.swift in Sources */, @@ -1067,6 +1059,7 @@ ED2AD58E1F3B08BC00C101E6 /* Client+Query.swift in Sources */, ED36B7101E3BD052005581FD /* Field.swift in Sources */, ED1D789C1F2F96260048E169 /* DataCache.swift in Sources */, + ED535E661F5E8E1300886D93 /* Decodable.swift in Sources */, ED36B7131E3BD052005581FD /* Space.swift in Sources */, ED1D78A11F3069FF0048E169 /* Client+Modellable.swift in Sources */, ED36B7071E3BD052005581FD /* Asset.swift in Sources */, @@ -1098,6 +1091,7 @@ ED36B7221E3BD053005581FD /* SignalUtils.swift in Sources */, ED2AD58F1F3B08BC00C101E6 /* Client+Query.swift in Sources */, ED1D789D1F2F96260048E169 /* DataCache.swift in Sources */, + ED535E671F5E8E1300886D93 /* Decodable.swift in Sources */, ED36B7201E3BD053005581FD /* Field.swift in Sources */, ED1D78A21F3069FF0048E169 /* Client+Modellable.swift in Sources */, ED36B7231E3BD053005581FD /* Space.swift in Sources */, diff --git a/Contentful.xcworkspace/contents.xcworkspacedata b/Contentful.xcworkspace/contents.xcworkspacedata index f3c46f9a..02b51d48 100644 --- a/Contentful.xcworkspace/contents.xcworkspacedata +++ b/Contentful.xcworkspace/contents.xcworkspacedata @@ -10,9 +10,6 @@ - - diff --git a/Sources/Contentful/ArrayResponse.swift b/Sources/Contentful/ArrayResponse.swift index 4c1ea9c0..39cb0fd6 100644 --- a/Sources/Contentful/ArrayResponse.swift +++ b/Sources/Contentful/ArrayResponse.swift @@ -6,9 +6,6 @@ // Copyright © 2015 Contentful GmbH. All rights reserved. // -import ObjectMapper - - private protocol Array { associatedtype ItemType @@ -28,7 +25,7 @@ private protocol Array { This is the result type for any request of a collection of resources. See: **/ -public struct ArrayResponse: Array, ImmutableMappable where ItemType: Resource { +public struct ArrayResponse: Array where ItemType: Resource, ItemType: Decodable { /// The resources which are part of the given array public let items: [ItemType] @@ -42,20 +39,41 @@ public struct ArrayResponse: Array, ImmutableMappable where ItemType: /// The total number of resources which matched the original request public let total: UInt - internal let includedAssets: [Asset]? - internal let includedEntries: [Entry]? + internal let includes: Includes? + + internal var includedAssets: [Asset]? { + return includes?.assets + } + internal var includedEntries: [Entry]? { + return includes?.entries + } + + internal struct Includes: Decodable { + let assets: [Asset]? + let entries: [Entry]? - // MARK: + private enum CodingKeys: String, CodingKey { + case assets = "Asset" + case entries = "Entry" + } - public init(map: Map) throws { + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + assets = try values.decodeIfPresent([Asset].self, forKey: CodingKeys.assets) + entries = try values.decodeIfPresent([Entry].self, forKey: CodingKeys.entries) + } + } +} - items = try map.value("items") - limit = try map.value("limit") - skip = try map.value("skip") - total = try map.value("total") - includedAssets = try? map.value("includes.Asset") - includedEntries = try? map.value("includes.Entry") +extension ArrayResponse: Decodable { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + items = try container.decode([ItemType].self, forKey: .items) + includes = try container.decodeIfPresent(ArrayResponse.Includes.self, forKey: .includes) + skip = try container.decode(UInt.self, forKey: .skip) + total = try container.decode(UInt.self, forKey: .total) + limit = try container.decode(UInt.self, forKey: .limit) // Annoying workaround for type system not allowing cast of items to [Entry] let entries: [Entry] = items.flatMap { $0 as? Entry } @@ -67,8 +85,10 @@ public struct ArrayResponse: Array, ImmutableMappable where ItemType: entry.resolveLinks(against: allIncludedEntries, and: (includedAssets ?? [])) } } + private enum CodingKeys: String, CodingKey { + case items, includes, skip, limit, total + } } - /** A list of Contentful entries that have been mapped to types conforming to `EntryModellable` diff --git a/Sources/Contentful/Asset.swift b/Sources/Contentful/Asset.swift index 225f6266..bb41483a 100644 --- a/Sources/Contentful/Asset.swift +++ b/Sources/Contentful/Asset.swift @@ -7,8 +7,6 @@ // import Foundation -import ObjectMapper - public extension String { @@ -17,20 +15,25 @@ public extension String { */ public func url() throws -> URL { guard let url = URL(string: self) else { throw SDKError.invalidURL(string: self) } - return url } } -/// An asset represents a media file in Contentful +/// An asset represents a media file in Contentful. public class Asset: LocalizableResource { - /// URL of the media file associated with this asset. Optional for compatibility with `select` operator queries. + /// The URL for the underlying media file. Returns nil if the url was omitted from the response (i.e. `select` operation in query) + /// or if the underlying media file is still processing with Contentful. + public var url: URL? { + guard let url = file?.url else { return nil } + return url + } + + /// String representation for the URL of the media file associated with this asset. Optional for compatibility with `select` operator queries. /// Also, If the media file is still being processed, as the final stage of uploading to your space, this property will be nil. public var urlString: String? { - guard let urlString = localizedString(path: "file.url") else { return nil } - let urlStringWithScheme = "https:" + urlString - return urlStringWithScheme + guard let urlString = url?.absoluteString else { return nil } + return urlString } /// The title of the asset. Optional for compatibility with `select` operator queries. @@ -45,10 +48,12 @@ public class Asset: LocalizableResource { /// Metadata describing the file associated with the asset. Optional for compatibility with `select` operator queries. public var file: FileMetadata? { - return localizedBaseMappable(path: "file") + let localizableValue = localizableFields["file"] + let value = localizableValue?[currentlySelectedLocale.code] as? FileMetadata + return value } - public struct FileMetadata: ImmutableMappable { + public struct FileMetadata: Decodable { /// Original filename of the file. public let fileName: String @@ -57,45 +62,48 @@ public class Asset: LocalizableResource { public let contentType: String /// Details of the file, depending on it's MIME type. - public let details: [String: Any] + public let details: Details? + + /// The remote URL for the binary data for this Asset. + /// If the media file is still being processed, as the final stage of uploading to your space, this property will be nil. + public let url: URL? - /// The size of the file in bytes. - public let size: Int + public struct Details: Decodable { + /// The size of the file in bytes. + public let size: Int - public init(map: Map) throws { - self.fileName = try map.value("fileName") - self.contentType = try map.value("contentType") - self.details = try map.value("details") - self.size = try map.value("details.size") + /// Additional information describing the image the asset references. + public let imageInfo: ImageInfo? + + public struct ImageInfo: Decodable { + let width: Double + let height: Double + } } - } - /// The URL for the underlying media file - public func url() throws -> URL { - guard let url = try urlString?.url() else { - throw SDKError.invalidURL(string: urlString ?? "No url string is stored for Asset: \(sys.id)") + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + fileName = try container.decode(String.self, forKey: .fileName) + contentType = try container.decode(String.self, forKey: .contentType) + details = try container.decode(Details.self, forKey: .details) + // Decodable handles URL's automatically but we need to prepend the https protocol. + let urlString = try container.decode(String.self, forKey: .url) + guard let url = URL(string: "https:" + urlString) else { + throw SDKError.invalidURL(string: "Asset had urlString incapable of being made into a Foundation.URL object \(urlString)") + } + self.url = url + } + + private enum CodingKeys: String, CodingKey { + case fileName, contentType, url, details } - return url } // MARK: Private - // Helper methods to enable retreiving localized values for fields which are static `Asset`. - // i.e. all `Asset` instances have fields named "description", "title" etc. - private var map: Map { - let map = Map(mappingType: .fromJSON, JSON: fields) - return map - } - private func localizedString(path: String) -> String? { - var value: String? - value <- map[path] - return value - } - - private func localizedBaseMappable(path: String) -> MappableType? { - var value: MappableType? - value <- map[path] + let localizableValue = localizableFields[path] + let value = localizableValue?[currentlySelectedLocale.code] as? String return value } } diff --git a/Sources/Contentful/Client+Modellable.swift b/Sources/Contentful/Client+Modellable.swift index 9c6392e4..4f79173a 100644 --- a/Sources/Contentful/Client+Modellable.swift +++ b/Sources/Contentful/Client+Modellable.swift @@ -100,7 +100,7 @@ extension Client { - Returns: A tuple of data task and an observable for the resulting array of EntryModellable types. */ @discardableResult public func fetchMappedEntries(with query: QueryOn) - -> Observable>> where EntryType: EntryModellable { + -> Observable>> { let asyncDataTask: AsyncDataTask, MappedArrayResponse> = fetchMappedEntries(with:then:) return toObservable(parameter: query, asyncDataTask: asyncDataTask).observable diff --git a/Sources/Contentful/Client.swift b/Sources/Contentful/Client.swift index 4a1f4ddb..6e1741e0 100644 --- a/Sources/Contentful/Client.swift +++ b/Sources/Contentful/Client.swift @@ -6,7 +6,6 @@ // Copyright © 2015 Contentful GmbH. All rights reserved. // -import ObjectMapper import Foundation import Interstellar @@ -128,8 +127,8 @@ open class Client { return nil } - internal func fetch(url: URL?, - then completion: @escaping ResultsHandler) -> URLSessionDataTask? { + internal func fetch(url: URL?, + then completion: @escaping ResultsHandler) -> URLSessionDataTask? { guard let url = url else { completion(Result.error(SDKError.invalidURL(string: ""))) @@ -227,15 +226,10 @@ open class Client { fileprivate func handleRateLimitJSON(_ data: Data, timeUntilLimitReset: Int, _ completion: ResultsHandler) { - do { - guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - let error = SDKError.unparseableJSON(data: data, errorMessage: "SDK unable to parse RateLimitError payload") - completion(Result.error(error)) - return - } - let map = Map(mappingType: .fromJSON, JSON: json) - guard let rateLimitError = RateLimitError(map: map) else { + let jsonDecoder = JSONDecoder() + + guard let rateLimitError = try? jsonDecoder.decode(RateLimitError.self, from: data) else { completion(.error(SDKError.unparseableJSON(data: data, errorMessage: "SDK unable to parse RateLimitError payload"))) return } @@ -243,37 +237,37 @@ open class Client { // In this case, .success means that a RateLimitError was successfully initialized. completion(Result.success(rateLimitError)) - } catch _ { - completion(.error(SDKError.unparseableJSON(data: data, errorMessage: "SDK unable to parse RateLimitError payload"))) - } } - fileprivate func handleJSON(_ data: Data, _ completion: ResultsHandler) { - do { - guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { - let error = SDKError.unparseableJSON(data: data, errorMessage: "Foundation.JSONSerialization failed") - completion(Result.error(error)) - return - } + internal static var jsonDecoderWithoutContext: JSONDecoder = { + let jsonDecoder = JSONDecoder() + jsonDecoder.dateDecodingStrategy = .formatted(Date.Formatter.iso8601) + return jsonDecoder + }() + fileprivate func handleJSON(_ data: Data, _ completion: ResultsHandler) { + do { let localizationContext = space?.localizationContext - let map = Map(mappingType: .fromJSON, JSON: json, context: localizationContext) + + let jsonDecoder = Client.jsonDecoderWithoutContext // JSONDecoder() + jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = localizationContext // Use `Mappable` failable initialzer to optional rather throwing `ImmutableMappable` initializer // because failure to find an error in the JSON should error should not throw an error that JSON is not parseable. - if let apiError = ContentfulError(map: map) { + + if let apiError = ContentfulError.error(with: jsonDecoder, and: data) { completion(Result.error(apiError)) return } - // Locales will be injected via the map.property option. - let decodedObject = try MappableType(map: map) + // Locales will be injected via the JSONDecoder's userInfo property. + let decodedObject = try jsonDecoder.decode(DecodableType.self, from: data) completion(Result.success(decodedObject)) - } catch let error as MapError { - completion(.error(SDKError.unparseableJSON(data: data, errorMessage: "\(error)"))) + } catch let error as DecodingError { + completion(Result.error(SDKError.unparseableJSON(data: data, errorMessage: "The SDK was unable to parse the JSON: \(error)"))) } catch _ { - completion(.error(SDKError.unparseableJSON(data: data, errorMessage: ""))) + completion(Result.error(SDKError.unparseableJSON(data: data, errorMessage: ""))) } } } diff --git a/Sources/Contentful/ContentType.swift b/Sources/Contentful/ContentType.swift index 3cf60910..795a7200 100644 --- a/Sources/Contentful/ContentType.swift +++ b/Sources/Contentful/ContentType.swift @@ -7,10 +7,12 @@ // import Foundation -import ObjectMapper /// A Content Type represents your data model for Entries in a Contentful Space -public class ContentType: Resource { +public class ContentType: Resource, Decodable { + + /// System fields. + public let sys: Sys /// The fields which are part of this Content Type public let fields: [Field] @@ -23,13 +25,14 @@ public class ContentType: Resource { return sys.type } + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + sys = try container.decode(Sys.self, forKey: .sys) + fields = try container.decode([Field].self, forKey: .fields) + name = try container.decode(String.self, forKey: .name) + } - // MARK: - - public required init(map: Map) throws { - fields = try map.value("fields") - name = try map.value("name") - - try super.init(map: map) + enum CodingKeys: String, CodingKey { + case sys, fields, name } } diff --git a/Sources/Contentful/Date.swift b/Sources/Contentful/Date.swift index 669430f8..621cf8f4 100644 --- a/Sources/Contentful/Date.swift +++ b/Sources/Contentful/Date.swift @@ -7,7 +7,6 @@ // import Foundation -import ObjectMapper // Formatter and extensions pulled from: https://stackoverflow.com/a/28016692/4068264 public extension Date { @@ -37,17 +36,3 @@ public extension String { return Date.Formatter.iso8601.date(from: self) } } - -public final class SysISO8601DateTransform: DateFormatterTransform { - - public init() { - - let formatter = Date.Formatter.iso8601 - formatter.calendar = Calendar(identifier: .iso8601) - formatter.locale = Foundation.Locale(identifier: "en_US_POSIX") - formatter.timeZone = TimeZone(secondsFromGMT: 0) - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX" - - super.init(dateFormatter: formatter) - } -} diff --git a/Sources/Contentful/Decodable.swift b/Sources/Contentful/Decodable.swift new file mode 100644 index 00000000..df820730 --- /dev/null +++ b/Sources/Contentful/Decodable.swift @@ -0,0 +1,105 @@ +// +// Decodable.swift +// Contentful +// +// Created by JP Wright on 05.09.17. +// Copyright © 2017 Contentful GmbH. All rights reserved. +// + +import Foundation + +// Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a + +internal struct JSONCodingKeys: CodingKey { + internal var stringValue: String + + internal init?(stringValue: String) { + self.stringValue = stringValue + } + + internal var intValue: Int? + + internal init?(intValue: Int) { + self.init(stringValue: "\(intValue)") + self.intValue = intValue + } +} + +internal extension KeyedDecodingContainer { + + internal func decode(_ type: Dictionary.Type, forKey key: K) throws -> Dictionary { + let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) + return try container.decode(type) + } + + internal func decodeIfPresent(_ type: Dictionary.Type, forKey key: K) throws -> Dictionary? { + guard contains(key) else { + return nil + } + return try decode(type, forKey: key) + } + + internal func decode(_ type: Array.Type, forKey key: K) throws -> Array { + var container = try self.nestedUnkeyedContainer(forKey: key) + return try container.decode(type) + } + + internal func decodeIfPresent(_ type: Array.Type, forKey key: K) throws -> Array? { + guard contains(key) else { + return nil + } + return try decode(type, forKey: key) + } + + internal func decode(_ type: Dictionary.Type) throws -> Dictionary { + var dictionary = Dictionary() + + for key in allKeys { + if let intValue = try? decode(Int.self, forKey: key) { + dictionary[key.stringValue] = intValue + } else if let stringValue = try? decode(String.self, forKey: key) { + dictionary[key.stringValue] = stringValue + } else if let boolValue = try? decode(Bool.self, forKey: key) { + dictionary[key.stringValue] = boolValue + } else if let doubleValue = try? decode(Double.self, forKey: key) { + dictionary[key.stringValue] = doubleValue + } else if let fileMetaData = try? decode(Asset.FileMetadata.self, forKey: key) { + dictionary[key.stringValue] = fileMetaData // Custom contentful type. + } else if let nestedDictionary = try? decode(Dictionary.self, forKey: key) { + dictionary[key.stringValue] = nestedDictionary + } else if let nestedArray = try? decode(Array.self, forKey: key) { + dictionary[key.stringValue] = nestedArray + } else if try decodeNil(forKey: key) { + dictionary[key.stringValue] = true + } + } + return dictionary + } +} + +internal extension UnkeyedDecodingContainer { + + internal mutating func decode(_ type: Array.Type) throws -> Array { + var array: [Any] = [] + while isAtEnd == false { + if let value = try? decode(Bool.self) { + array.append(value) + } else if let value = try? decode(Double.self) { + array.append(value) + } else if let value = try? decode(String.self) { + array.append(value) + } else if let nestedDictionary = try? decode(Dictionary.self) { + array.append(nestedDictionary) + } else if let nestedArray = try? decode(Array.self) { + array.append(nestedArray) + } + } + return array + } + + internal mutating func decode(_ type: Dictionary.Type) throws -> Dictionary { + + let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self) + return try nestedContainer.decode(type) + } +} diff --git a/Sources/Contentful/Entry.swift b/Sources/Contentful/Entry.swift index e3adbeca..ea55cbd6 100644 --- a/Sources/Contentful/Entry.swift +++ b/Sources/Contentful/Entry.swift @@ -47,7 +47,7 @@ public class Entry: LocalizableResource { if let dictionaryRepresentationArray = fieldValueForLocaleCode as? [[String: Any]] { - let mixedLinks = dictionaryRepresentationArray.flatMap({ Link.link(from: $0) }) + let mixedLinks = dictionaryRepresentationArray.flatMap { Link.link(from: $0) } // The conversion from dictionary representation should only ever happen once let alreadyResolvedLinks = mixedLinks.filter { $0.isResolved == true } diff --git a/Sources/Contentful/Error.swift b/Sources/Contentful/Error.swift index cc41ccef..9484c92e 100644 --- a/Sources/Contentful/Error.swift +++ b/Sources/Contentful/Error.swift @@ -7,7 +7,6 @@ // import Foundation -import ObjectMapper /// Possible errors being thrown by the SDK public enum SDKError: Error { @@ -37,7 +36,7 @@ public enum SDKError: Error { * @param Data The data being parsed * @param String The error which occured during parsing */ - case unparseableJSON(data: Data, errorMessage: String) + case unparseableJSON(data: Data?, errorMessage: String) /// Thrown when no entry is found matching a specific Entry id case noEntryFoundFor(id: String) @@ -100,7 +99,17 @@ public enum QueryError: Error { /// Information regarding an error received from Contentful -public class ContentfulError: Mappable, Error { +public class ContentfulError: Decodable, Error { + + /// System fields for the error. + public struct Sys: Decodable { + /// The identifier for fo rth eerror. + let id: String? + /// The type of the error. + let type: String? + } + + public let sys: Sys /// Human readable error message. public private(set) var message: String? @@ -112,27 +121,35 @@ public class ContentfulError: Mappable, Error { // Rather than throw an error which will trigger the Swift error breakpoint in Xcode, // we want to use failable ObjectMapper initializers. - public private(set) var id: String? + public var id: String? { + return sys.id + } - public private(set) var type: String? + public var type: String? { + return sys.type + } - // MARK: + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + sys = try container.decode(Sys.self, forKey: .sys) + message = try container.decodeIfPresent(String.self, forKey: .message) + requestId = try container.decodeIfPresent(String.self, forKey: .requestId) - public required init?(map: Map) { - mapping(map: map) + } - // An error must have these things. - guard message != nil && requestId != nil else { - return nil + static func error(with decoder: JSONDecoder, and data: Data) -> ContentfulError? { + if let error = try? decoder.decode(ContentfulError.self, from: data) { + // An error must have these things. + guard error.message != nil && error.requestId != nil else { + return nil + } + return error } + return nil } - // Required by ObjectMapper.BaseMappable - public func mapping(map: Map) { - message <- map["message"] - requestId <- map["requestId"] - id <- map["sys.id"] - type <- map["sys.type"] + private enum CodingKeys: String, CodingKey { + case sys, message, requestId } } diff --git a/Sources/Contentful/Field.swift b/Sources/Contentful/Field.swift index d6defe48..341d3aac 100644 --- a/Sources/Contentful/Field.swift +++ b/Sources/Contentful/Field.swift @@ -6,12 +6,10 @@ // Copyright © 2015 Contentful GmbH. All rights reserved. // -import ObjectMapper - public typealias FieldName = String /// The possible Field types in Contentful -public enum FieldType: String { +public enum FieldType: String, Decodable { /// An array of links or symbols case array = "Array" /// A link to an Asset @@ -43,7 +41,7 @@ public enum FieldType: String { /// A Field describes a single value inside an Entry // Hitting the /content_types endpoint will return a JSON field "fields" that // maps to an array where each element has the following structure. -public struct Field: ImmutableMappable { +public struct Field: Decodable { /// The unique identifier of this Field public let id: String /// The name of this Field @@ -64,25 +62,37 @@ public struct Field: ImmutableMappable { // For `Link`s, itemType is inferred via "linkType" public let itemType: FieldType? - // MARK: - - public init(map: Map) throws { - id = try map.value("id") - name = try map.value("name") - disabled = try map.value("disabled") - localized = try map.value("localized") - required = try map.value("required") + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + id = try container.decode(String.self, forKey: .id) + name = try container.decode(String.self, forKey: .name) + disabled = try container.decode(Bool.self, forKey: .disabled) + localized = try container.decode(Bool.self, forKey: .localized) + required = try container.decode(Bool.self, forKey: .required) - var typeString: String! - typeString <- map["type"] - type = FieldType(rawValue: typeString) ?? .none + type = try container.decode(FieldType.self, forKey: .type) var itemTypeString: String? - itemTypeString <- map["items.type"] - itemTypeString <- map["items.linkType"] - itemTypeString <- map["linkType"] + if type == FieldType.array { + if let items = try container.decodeIfPresent([String: Any].self, forKey: .items) { + itemTypeString = items["type"] as? String + if itemTypeString == FieldType.link.rawValue { + itemTypeString = items["linkType"] as? String + } + } + } else if type == FieldType.link { + itemTypeString = try container.decode(String.self, forKey: .linkType) + } self.itemType = FieldType(rawValue: itemTypeString ?? FieldType.none.rawValue) ?? .none } + + + private enum CodingKeys: String, CodingKey { + case id, name, disabled, localized, required + case type + case items + case linkType + } } diff --git a/Sources/Contentful/ImageOptions.swift b/Sources/Contentful/ImageOptions.swift index b1a6c6eb..b48599b4 100644 --- a/Sources/Contentful/ImageOptions.swift +++ b/Sources/Contentful/ImageOptions.swift @@ -8,7 +8,7 @@ import Foundation import CoreGraphics -import ObjectMapper + #if os(iOS) || os(tvOS) || os(watchOS) import UIKit #elseif os(macOS) @@ -286,10 +286,8 @@ public enum Fit: URLImageQueryExtendable { fileprivate func additionalQueryItem() throws -> URLQueryItem? { switch self { case .pad(let .some(color)): - let hexTransform = ObjectMapper.HexColorTransform() - guard let hexRepresentation = hexTransform.transformToJSON(color) else { - throw SDKError.invalidImageParameters("Unable to generate Hex representation for color: \(color)") - } + let cgColor = color.cgColor + let hexRepresentation = cgColor.hexRepresentation() return URLQueryItem(name: ImageParameters.backgroundColor, value: "rgb:" + hexRepresentation) case .thumb(let .some(focus)): @@ -347,3 +345,37 @@ private struct ImageParameters { static let quality = "q" static let progressiveJPG = "fl" } + + +// Use CGColor instead of UIColor to enable cross-platform compatibility: macOS, iOS, tvOS, watchOS. +internal extension CGColor { + + // If for some reason the following code fails to create a hex string, the color black will be + // returned. + internal func hexRepresentation() -> String { + let hexForBlack = "000000" + guard let colorComponents = components else { return hexForBlack } + guard let colorSpace = colorSpace else { return hexForBlack } + + let r, g, b: Float + + switch colorSpace.model { + case .monochrome: + r = Float(colorComponents[0]) + g = Float(colorComponents[0]) + b = Float(colorComponents[0]) + + case .rgb: + r = Float(colorComponents[0]) + g = Float(colorComponents[1]) + b = Float(colorComponents[2]) + default: + return hexForBlack + } + + // Search the web for Swift UIColor to hex. + // This answer helped: https://stackoverflow.com/a/30967091/4068264 + let hexString = String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) + return hexString + } +} diff --git a/Sources/Contentful/Link.swift b/Sources/Contentful/Link.swift index 5cfdd6e1..1e4f0558 100644 --- a/Sources/Contentful/Link.swift +++ b/Sources/Contentful/Link.swift @@ -7,17 +7,6 @@ // import Foundation -import ObjectMapper - -public struct LinkSys { - - /// The identifier of the linked resource - public let id: String - - /// The type of the linked resource: either "Entry" or "Asset". - public let linkType: String -} - /** A representation of Linked Resources that a field may point to in your content model. @@ -27,6 +16,19 @@ public struct LinkSys { */ public enum Link { + public struct Sys: Decodable { + + /// The identifier of the linked resource + public let id: String + + /// The type of the linked resource: either "Entry" or "Asset". + public let linkType: String + + /// The content type identifier for the linked resource. + public let type: String + } + + /// The Link points to an `Asset` case asset(Asset) @@ -34,7 +36,7 @@ public enum Link { case entry(Entry) /// The Link is unresolved, and therefore contains a dictionary of metadata describing the linked resource. - case unresolved(LinkSys) + case unresolved(Link.Sys) /// The unique identifier of the linked asset or entry public var id: String { @@ -68,16 +70,18 @@ public enum Link { // Linked objects are stored as a dictionary with "type": "Link", // value for "linkType" can be "Asset", "Entry", "Space", "ContentType". - if let linkJSON = fieldValue as? [String: AnyObject], - let sys = linkJSON["sys"] as? [String: AnyObject], + if let linkJSON = fieldValue as? [String: Any], + let sys = linkJSON["sys"] as? [String: Any], let id = sys["id"] as? String, - let linkType = sys["linkType"] as? String { - return Link.unresolved(LinkSys(id: id, linkType: linkType)) + let linkType = sys["linkType"] as? String, + let type = sys["type"] as? String { + return Link.unresolved(Link.Sys(id: id, linkType: linkType, type: type)) } + return nil } - private var sys: LinkSys { + private var sys: Link.Sys { switch self { case .unresolved(let sys): return sys diff --git a/Sources/Contentful/Locale.swift b/Sources/Contentful/Locale.swift index fa8ee263..b55a340b 100644 --- a/Sources/Contentful/Locale.swift +++ b/Sources/Contentful/Locale.swift @@ -7,12 +7,11 @@ // import Foundation -import ObjectMapper public typealias LocaleCode = String /// A Locale represents possible translations for Entry Fields -public class Locale: ImmutableMappable { +public class Locale: Decodable { /// Linked list accessor for going to the next fallback locale public let fallbackLocaleCode: LocaleCode? @@ -27,17 +26,11 @@ public class Locale: ImmutableMappable { /// The name of this Locale public let name: String - // MARK: - - public required init(map: Map) throws { - code = try map.value("code") - isDefault = try map.value("default") - name = try map.value("name") - - // Fallback locale code isn't always present. - var fallbackLocaleCode: LocaleCode? - fallbackLocaleCode <- map["fallbackCode"] - self.fallbackLocaleCode = fallbackLocaleCode + private enum CodingKeys: String, CodingKey { + case code + case isDefault = "default" + case name + case fallbackLocaleCode = "fallbackCode" } } @@ -50,10 +43,10 @@ public class Locale: ImmutableMappable { for an `Entry` does not have data for the currently selected locale, the SDK will walk the fallback chain for this field until a non-null value is found, or full chain has been walked. */ -public class LocalizationContext: MapContext { +public class LocalizationContext { /// An ordered collection of locales representing the fallback chain. - public let locales: [LocaleCode: Locale] + public let locales: [LocaleCode: Locale] /// The default locale of the space. public let `default`: Locale @@ -103,15 +96,11 @@ internal struct Localization { } // Normalizes fields to have a value for every locale in the space. - internal static func fieldsInMultiLocaleFormat(from map: Map, - selectedLocale: Locale) throws -> [FieldName: [LocaleCode: Any]] { - let fields: [FieldName: Any] = try map.value("fields") - - var firstLocaleForThisResource: LocaleCode? - // For locale=* and /sync, this property will not be present. - firstLocaleForThisResource <- map["sys.locale"] - if firstLocaleForThisResource == nil { // sanitize. + internal static func fieldsInMultiLocaleFormat(from fields: [FieldName: Any], + selectedLocale: Locale, + wasSelectedOnAPILevel: Bool) throws -> [FieldName: [LocaleCode: Any]] { + if wasSelectedOnAPILevel == false { // sanitize. // If there was no locale it the response, then we have the format with all locales present and we can simply map from localecode to locale and exit guard let fields = fields as? [FieldName: [LocaleCode: Any]] else { throw SDKError.localeHandlingError(message: "Unexpected response format: 'sys.locale' not present, and" diff --git a/Sources/Contentful/Resource.swift b/Sources/Contentful/Resource.swift index 44552f48..19a88c02 100644 --- a/Sources/Contentful/Resource.swift +++ b/Sources/Contentful/Resource.swift @@ -7,32 +7,30 @@ // import Foundation -import ObjectMapper /// Protocol for resources inside Contentful -public class Resource: ImmutableMappable { +public protocol Resource { /// System fields - public let sys: Sys + var sys: Sys { get } +} +extension Resource { /// The unique identifier of this Resource public var id: String { return sys.id } +} - internal init(sys: Sys) { - self.sys = sys - } +class DeletedResource: Resource, Decodable { - // MARK: - + let sys: Sys - public required init(map: Map) throws { - sys = try map.value("sys") + init(sys: Sys) { + self.sys = sys } } -class DeletedResource: Resource {} - /** LocalizableResource @@ -42,7 +40,10 @@ class DeletedResource: Resource {} all locales. This class gives an interface to specify which locale should be used when fetching data from `Resource` instances that are in memory. */ -public class LocalizableResource: Resource { +public class LocalizableResource: Resource, Decodable { + + /// System fields + public let sys: Sys /// Currently selected locale to use when reading data from the `fields` dictionary. public var currentlySelectedLocale: Locale @@ -74,36 +75,43 @@ public class LocalizableResource: Resource { // Context used for handling locales during decoding of `Asset` and `Entry` instances. internal let localizationContext: LocalizationContext + static let localizationContextKey = CodingUserInfoKey(rawValue: "localizationContext")! - // MARK: + public required init(from decoder: Decoder) throws { - public required init(map: Map) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let sys = try container.decode(Sys.self, forKey: .sys) - // Optional propery, not returned when hitting `/sync`. - var localeCodeSelectedAtAPILevel: LocaleCode? - localeCodeSelectedAtAPILevel <- map["sys.locale"] - - guard let localizationContext = map.context as? LocalizationContext else { - // Should never get here; but just in case, let's inform the user what the deal is. - throw SDKError.localeHandlingError(message: "SDK failed to find the necessary LocalizationContext" - + "necessary to properly map API responses to internal format.") + guard let localizationContext = decoder.userInfo[LocalizableResource.localizationContextKey] as? LocalizationContext else { + throw SDKError.localeHandlingError(message: """ + SDK failed to find the necessary LocalizationContext + necessary to properly map API responses to internal format. + """ + ) } self.localizationContext = localizationContext - // Get currently selected locale. - if let localeCode = localeCodeSelectedAtAPILevel, let locale = localizationContext.locales[localeCode] { - self.currentlySelectedLocale = locale + if let localeCode = sys.locale, let locale = localizationContext.locales[localeCode] { + currentlySelectedLocale = locale } else { - self.currentlySelectedLocale = localizationContext.default + currentlySelectedLocale = localizationContext.default } + self.sys = sys - self.localizableFields = try Localization.fieldsInMultiLocaleFormat(from: map, selectedLocale: currentlySelectedLocale) + let fieldsDictionary = try container.decode(Dictionary.self, forKey: .fields) + localizableFields = try Localization.fieldsInMultiLocaleFormat(from: fieldsDictionary, + selectedLocale: currentlySelectedLocale, + wasSelectedOnAPILevel: sys.locale != nil) + } - try super.init(map: map) + private enum CodingKeys: String, CodingKey { + case sys + case fields } } + /// Convenience methods for reading from dictionaries without conditional casts. public extension Dictionary where Key: ExpressibleByStringLiteral { @@ -247,14 +255,14 @@ public extension Dictionary where Key: ExpressibleByStringLiteral { // MARK: Internal -extension Resource: Hashable { +extension LocalizableResource: Hashable { public var hashValue: Int { return id.hashValue } } -extension Resource: Equatable {} -public func == (lhs: Resource, rhs: Resource) -> Bool { +extension LocalizableResource: Equatable {} +public func == (lhs: LocalizableResource, rhs: LocalizableResource) -> Bool { return lhs.id == rhs.id && lhs.sys.updatedAt == rhs.sys.updatedAt } diff --git a/Sources/Contentful/Result.swift b/Sources/Contentful/Result.swift deleted file mode 100644 index b1dc6a54..00000000 --- a/Sources/Contentful/Result.swift +++ /dev/null @@ -1,201 +0,0 @@ -// Result.swift -// -// Copyright (c) 2015 Jens Ravens (http://jensravens.de) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - - - -/// Conform to ResultType to use your own result type, e.g. from other libraries with Interstellar. -public protocol ResultType { - /// Describes the contained successful type of this result. - associatedtype Value - - /// Return an error if the result is unsuccessful, otherwise nil. - var error: Error? { get } - - /// Return the value if the result is successful, otherwise nil. - var value: Value? { get } - - /// Convert this result into an `Interstellar.Result`. This implementation is optional. - var result: Result { get } -} - -extension ResultType { - public var result: Result { - return Result(value: value, error: error) - } -} - -extension Result { - public init(value: T?, error: Error?) { - if let error = error { - self = .error(error) - } else { - self = .success(value!) - } - } - - public init(block: () throws -> T) { - do { - self = try .success(block()) - } catch let e { - self = .error(e) - } - } - - public var result: Result { - return self - } -} - - -/** - A result contains the result of a computation or task. It might be either successfull - with an attached value or a failure with an attached error (which conforms to Swift 2's - ErrorType). You can read more about the implementation in - [this blog post](http://jensravens.de/a-swifter-way-of-handling-errors/). - */ -public enum Result: ResultType { - case success(T) - case error(Error) - - /** - Initialize a result containing a successful value. - */ - public init(success value: T) { - self = Result.success(value) - } - - /** - Initialize a result containing an error - */ - public init(error: Error) { - self = .error(error) - } - - /** - Transform a result into another result using a function. If the result was an error, - the function will not be executed and the error returned instead. - */ - public func map(_ f: @escaping (T) -> U) -> Result { - switch self { - case let .success(v): return .success(f(v)) - case let .error(error): return .error(error) - } - } - - /** - Transform a result into another result using a function. If the result was an error, - the function will not be executed and the error returned instead. - */ - public func flatMap(_ f: (T) -> Result) -> Result { - switch self { - case let .success(v): return f(v) - case let .error(error): return .error(error) - } - } - - /** - Transform a result into another result using a function. If the result was an error, - the function will not be executed and the error returned instead. - */ - public func flatMap(_ f: (T) throws -> U) -> Result { - return flatMap { t in - do { - return .success(try f(t)) - } catch let error { - return .error(error) - } - } - } - /** - Transform a result into another result using a function. If the result was an error, - the function will not be executed and the error returned instead. - */ - public func flatMap(_ f:@escaping (T, (@escaping(Result)->Void))->Void) -> (@escaping(Result)->Void)->Void { - return { g in - switch self { - case let .success(v): f(v, g) - case let .error(error): g(.error(error)) - } - } - } - - /** - Call a function with the result as an argument. Use this if the function should be - executed no matter if the result was a success or not. - */ - public func ensure(_ f: (Result) -> Result) -> Result { - return f(self) - } - - /** - Call a function with the result as an argument. Use this if the function should be - executed no matter if the result was a success or not. - */ - public func ensure(_ f:@escaping (Result, ((Result)->Void))->Void) -> ((Result)->Void)->Void { - return { g in - f(self, g) - } - } - - /** - Direct access to the content of the result as an optional. If the result was a success, - the optional will contain the value of the result. - */ - public var value: T? { - switch self { - case let .success(v): return v - case .error(_): return nil - } - } - - /** - Direct access to the error of the result as an optional. If the result was an error, - the optional will contain the error of the result. - */ - public var error: Error? { - switch self { - case .success: return nil - case .error(let x): return x - } - } - - /** - Access the value of this result. If the result contains an error, that error is thrown. - */ - public func get() throws -> T { - switch self { - case let .success(value): return value - case .error(let error): throw error - } - } -} - - -/** - Provide a default value for failed results. - */ -public func ?? (result: Result, defaultValue: @autoclosure () -> T) -> T { - switch result { - case .success(let x): return x - case .error: return defaultValue() - } -} diff --git a/Sources/Contentful/Space.swift b/Sources/Contentful/Space.swift index 6f18fd47..5d79257c 100644 --- a/Sources/Contentful/Space.swift +++ b/Sources/Contentful/Space.swift @@ -7,11 +7,12 @@ // import Foundation -import ObjectMapper /// A Space represents a collection of Content Types, Assets and Entries in Contentful -public class Space: Resource { +public class Space: Resource, Decodable { + + public let sys: Sys /// Available Locales for this Space public let locales: [Locale] @@ -29,15 +30,22 @@ public class Space: Resource { // MARK: - public required init(map: Map) throws { - name = try map.value("name") - locales = try map.value("locales") + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + sys = try container.decode(Sys.self, forKey: .sys) + name = try container.decode(String.self, forKey: .name) + locales = try container.decode([Locale].self, forKey: .locales) guard let defaultLocale = locales.filter({ $0.isDefault }).first else { throw SDKError.localeHandlingError(message: "Locale with default == true not found in Space!") } localizationContext = LocalizationContext(default: defaultLocale, locales: locales) - try super.init(map: map) + } + + private enum CodingKeys: String, CodingKey { + case sys + case name + case locales } } diff --git a/Sources/Contentful/SyncSpace.swift b/Sources/Contentful/SyncSpace.swift index 6b9d97a8..3ca5f370 100644 --- a/Sources/Contentful/SyncSpace.swift +++ b/Sources/Contentful/SyncSpace.swift @@ -8,10 +8,9 @@ import Foundation import Interstellar -import ObjectMapper /// A container for the synchronized state of a Space -public final class SyncSpace: ImmutableMappable { +public final class SyncSpace: Decodable { internal var assetsMap = [String: Asset]() internal var entriesMap = [String: Entry]() @@ -45,7 +44,7 @@ public final class SyncSpace: ImmutableMappable { self.syncToken = syncToken } - internal func syncToken(from urlString: String) -> String { + internal static func syncToken(from urlString: String) -> String { guard let components = URLComponents(string: urlString)?.queryItems else { return "" } for component in components { if let value = component.value, component.name == "sync_token" { @@ -55,50 +54,54 @@ public final class SyncSpace: ImmutableMappable { return "" } - // MARK: + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var syncUrl = try container.decodeIfPresent(String.self, forKey: .nextPageUrl) - public required init(map: Map) throws { var hasMorePages = true - var syncUrl: String? - syncUrl <- map["nextPageUrl"] - if syncUrl == nil { hasMorePages = false - syncUrl <- map["nextSyncUrl"] + syncUrl = try container.decodeIfPresent(String.self, forKey: .nextSyncUrl) } - self.hasMorePages = hasMorePages - self.syncToken = self.syncToken(from: syncUrl!) + guard let nextSyncUrl = syncUrl else { + throw SDKError.unparseableJSON(data: nil, errorMessage: "No sync url for future sync operations was serialized from the response.") + } - var items: [[String: Any]]! - items <- map["items"] + self.syncToken = SyncSpace.syncToken(from: nextSyncUrl) + self.hasMorePages = hasMorePages - let resources: [Resource] = try items.flatMap { itemJSON in - let map = Map(mappingType: .fromJSON, JSON: itemJSON, context: map.context) + // A copy as an array of dictionaries just to extract "sys.type" field. + guard let items = try container.decode(Array.self, forKey: .items) as? [[String: Any]] else { + throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources") + } + var itemsArrayContainer = try container.nestedUnkeyedContainer(forKey: .items) - let type: String = try map.value("sys.type") + var resources = [Resource]() + while itemsArrayContainer.isAtEnd == false { + guard let sys = items[itemsArrayContainer.currentIndex]["sys"] as? [String: Any], let type = sys["type"] as? String else { + let errorMessage = "SDK was unable to parse sys.type property necessary to finish resource serialization." + throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) + } + let item: Resource switch type { - case "Asset": return try? Asset(map: map) - case "Entry": return try? Entry(map: map) - case "ContentType": return try? ContentType(map: map) - case "DeletedAsset": return try? DeletedResource(map: map) - case "DeletedEntry": return try? DeletedResource(map: map) + case "Asset": item = try itemsArrayContainer.decode(Asset.self) + case "Entry": item = try itemsArrayContainer.decode(Entry.self) + case "DeletedAsset": item = try itemsArrayContainer.decode(DeletedResource.self) + case "DeletedEntry": item = try itemsArrayContainer.decode(DeletedResource.self) default: fatalError("Unsupported resource type '\(type)'") } - - return nil + resources.append(item) } cache(resources: resources) + } - // If it's a one page sync, resolve links. - // Otherwise, we will wait until all pages have come in to resolve them. - if hasMorePages == false { - for entry in entries { - entry.resolveLinks(against: entries, and: assets) - } - } + private enum CodingKeys: String, CodingKey { + case nextSyncUrl + case nextPageUrl + case items } internal func updateWithDiffs(from syncSpace: SyncSpace) { diff --git a/Sources/Contentful/Sys.swift b/Sources/Contentful/Sys.swift index e9ddb4b6..6ae226da 100644 --- a/Sources/Contentful/Sys.swift +++ b/Sources/Contentful/Sys.swift @@ -7,9 +7,8 @@ // import Foundation -import ObjectMapper -public struct Sys: ImmutableMappable { +public struct Sys { /// The unique id. public let id: String @@ -24,28 +23,38 @@ public struct Sys: ImmutableMappable { public let updatedAt: Date? /// Currently selected locale - public var locale: String? + public var locale: LocaleCode? // Not present when hitting /sync or using "*" wildcard locale in request. - /// The identifier for the ContentType. - public let contentTypeId: String? + /// The identifier for the ContentType, if the Resource is an `Entry`. + public var contentTypeId: String? { + return contentTypeInfo?["sys"]?.id + } + /// The number denoting what published version of the resource is. public let revision: Int? - // MARK: + // Because we have a root key of "sys" we will use a dictionary. + private let contentTypeInfo: [String: Link.Sys]? // Not present on `Asset` or `ContentType` + + +} + +extension Sys: Decodable { - public init(map: Map) throws { - // Properties present in all sys structures. - id = try map.value("id") - type = try map.value("type") + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) - // Optional properties. - locale = try? map.value("locale") - contentTypeId = try? map.value("contentType.sys.id") - revision = try? map.value("revision") + id = try container.decode(String.self, forKey: .id) + type = try container.decode(String.self, forKey: .type) + createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) + updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) + locale = try container.decodeIfPresent(String.self, forKey: .locale) + revision = try container.decodeIfPresent(Int.self, forKey: .revision) + contentTypeInfo = try container.decodeIfPresent([String: Link.Sys].self, forKey: .contentTypeId) + } - // Dates - let iso8601DateTransform = SysISO8601DateTransform() - createdAt = try? map.value("createdAt", using: iso8601DateTransform) - updatedAt = try? map.value("updatedAt", using: iso8601DateTransform) + private enum CodingKeys: String, CodingKey { + case id, type, createdAt, updatedAt, locale, revision + case contentTypeId = "contentType" } } diff --git a/Tests/ContentfulTests/ImageTests.swift b/Tests/ContentfulTests/ImageTests.swift index 7bcfdd9c..1002e808 100644 --- a/Tests/ContentfulTests/ImageTests.swift +++ b/Tests/ContentfulTests/ImageTests.swift @@ -12,18 +12,18 @@ import Foundation import XCTest import DVR import Nimble -import ObjectMapper class ImageTests: XCTestCase { let nyanCatAsset: Asset = { - // Load nyan cat from "asset.json" file. - let spaceMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("space")) - let space = try! Space(map: spaceMap) + let jsonDecoder = Client.jsonDecoderWithoutContext + let spaceJSONData = JSONDecodingTests.jsonData("space") + let space = try! jsonDecoder.decode(Space.self, from: spaceJSONData) + jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = space.localizationContext - let assetMappingContext = space.localizationContext - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("asset"), context: assetMappingContext) - let asset = try! Asset(map: map) + // Load nyan cat from "asset.json" file. + let nyanCatJSONData = JSONDecodingTests.jsonData("asset") + let asset = try! jsonDecoder.decode(Asset.self, from: nyanCatJSONData) return asset }() @@ -267,3 +267,4 @@ class ImageTests: XCTestCase { waitForExpectations(timeout: 10.0, handler: nil) } } + diff --git a/Tests/ContentfulTests/ObjectMappingTests.swift b/Tests/ContentfulTests/JSONDecodingTests.swift similarity index 53% rename from Tests/ContentfulTests/ObjectMappingTests.swift rename to Tests/ContentfulTests/JSONDecodingTests.swift index 4b3f1f9e..1f77e128 100644 --- a/Tests/ContentfulTests/ObjectMappingTests.swift +++ b/Tests/ContentfulTests/JSONDecodingTests.swift @@ -10,38 +10,46 @@ import Foundation import XCTest import Nimble -import ObjectMapper -class ObjectMappingTests: XCTestCase { +class JSONDecodingTests: XCTestCase { - static func jsonData(_ fileName: String) -> [String: Any] { + static func jsonData(_ fileName: String) -> Data { let path = NSString(string: "Data").appendingPathComponent(fileName) - let bundle = Bundle(for: ObjectMappingTests.self) + let bundle = Bundle(for: JSONDecodingTests.self) let urlPath = bundle.path(forResource: path, ofType: "json")! let data = try! Data(contentsOf: URL(fileURLWithPath: urlPath)) - return try! JSONSerialization.jsonObject(with: data, options: []) as! [String : Any] + return data } func testDecodingWithoutLocalizationContextThrows() { do { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("asset")) - let _ = try Asset(map: map) + let assetData = JSONDecodingTests.jsonData("asset") + let jsonDecoder = Client.jsonDecoderWithoutContext + // Reset userInfo state since it's a static var that exists through the test cycle. + jsonDecoder.userInfo = [CodingUserInfoKey: Any]() + let _ = try jsonDecoder.decode(Asset.self, from: assetData) fail("Mapping without a localizatoin context should throw an error") + } catch let error as SDKError { + switch error { + case .localeHandlingError: + XCTAssert(true) + default: fail("Wrong error thrown") + } } catch _ { - XCTAssert(true) + fail("Wrong error thrown") } } func testDecodeAsset() { do { - // We must have a space first to pass in locale information. - let spaceMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("space")) - let space = try Space(map: spaceMap) + let jsonDecoder = Client.jsonDecoderWithoutContext + let spaceJSONData = JSONDecodingTests.jsonData("space") + let space = try! jsonDecoder.decode(Space.self, from: spaceJSONData) + jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = space.localizationContext - let localesContext = space.localizationContext - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("asset"), context: localesContext) - let asset = try Asset(map: map) + let assetJSONData = JSONDecodingTests.jsonData("asset") + let asset = try jsonDecoder.decode(Asset.self, from: assetJSONData) expect(asset.sys.id).to(equal("nyancat")) expect(asset.sys.type).to(equal("Asset")) @@ -54,8 +62,9 @@ class ObjectMappingTests: XCTestCase { func testDecodeSpaces() { do { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("space")) - let space = try Space(map: map) + let jsonDecoder = Client.jsonDecoderWithoutContext + let spaceJSONData = JSONDecodingTests.jsonData("space") + let space = try jsonDecoder.decode(Space.self, from: spaceJSONData) expect(space.sys.id).to(equal("cfexampleapi")) expect(space.name).to(equal("Contentful Example API")) @@ -71,13 +80,14 @@ class ObjectMappingTests: XCTestCase { func testDecodeLocalizedEntries() { do { // We must have a space first to pass in locale information. - let spaceMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("space")) - let space = try Space(map: spaceMap) - let localesContext = space.localizationContext - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("localized"), context: localesContext) + let jsonDecoder = Client.jsonDecoderWithoutContext + let spaceJSONData = JSONDecodingTests.jsonData("space") + let space = try! jsonDecoder.decode(Space.self, from: spaceJSONData) + jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = space.localizationContext - let entry = try Entry(map: map) + let localizedEntryJSONData = JSONDecodingTests.jsonData("localized") + let entry = try jsonDecoder.decode(Entry.self, from: localizedEntryJSONData) expect(entry.sys.id).to(equal("nyancat")) expect(entry.fields["name"] as? String).to(equal("Nyan Cat")) @@ -92,14 +102,14 @@ class ObjectMappingTests: XCTestCase { func testDecodeSyncResponses() { do { - // We must have a space first to pass in locale information. - let spaceMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("space")) - let space = try Space(map: spaceMap) - let localesContext = space.localizationContext + let jsonDecoder = Client.jsonDecoderWithoutContext + let spaceJSONData = JSONDecodingTests.jsonData("space") + let space = try! jsonDecoder.decode(Space.self, from: spaceJSONData) + jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = space.localizationContext - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("sync"), context: localesContext) - let syncSpace = try SyncSpace(map: map) + let syncSpaceJSONData = JSONDecodingTests.jsonData("sync") + let syncSpace = try jsonDecoder.decode(SyncSpace.self, from: syncSpaceJSONData) expect(syncSpace.assets.count).to(equal(4)) expect(syncSpace.entries.count).to(equal(11)) @@ -109,11 +119,11 @@ class ObjectMappingTests: XCTestCase { } } - func testDecodeSyncResponsesWithdeletedAssetIds() { + func testDecodeSyncResponsesWithDeletedAssetIds() { do { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("deleted-asset")) - - let syncSpace = try SyncSpace(map: map) + let jsonDecoder = Client.jsonDecoderWithoutContext + let syncDeletedAssetData = JSONDecodingTests.jsonData("deleted-asset") + let syncSpace = try jsonDecoder.decode(SyncSpace.self, from: syncDeletedAssetData) expect(syncSpace.assets.count).to(equal(0)) expect(syncSpace.entries.count).to(equal(0)) @@ -127,8 +137,9 @@ class ObjectMappingTests: XCTestCase { func testDecodeSyncResponsesWithdeletedEntryIds() { do { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("deleted")) - let syncSpace = try SyncSpace(map: map) + let jsonDecoder = Client.jsonDecoderWithoutContext + let syncDeletedEntryData = JSONDecodingTests.jsonData("deleted") + let syncSpace = try jsonDecoder.decode(SyncSpace.self, from: syncDeletedEntryData) expect(syncSpace.assets.count).to(equal(0)) expect(syncSpace.entries.count).to(equal(0)) @@ -138,10 +149,5 @@ class ObjectMappingTests: XCTestCase { fail("Decoding sync responses with deleted Entries should not throw an error") } } - - // MARK: Field parsing tests - - func testParseFieldsWithConvenienceMethods() { - - } } + diff --git a/Tests/ContentfulTests/LocalizationTests.swift b/Tests/ContentfulTests/LocalizationTests.swift index 0a0c23fb..0352a954 100644 --- a/Tests/ContentfulTests/LocalizationTests.swift +++ b/Tests/ContentfulTests/LocalizationTests.swift @@ -8,20 +8,19 @@ @testable import Contentful import XCTest -import ObjectMapper import Nimble import DVR struct LocaleFactory { static func enUSDefault() -> Contentful.Locale { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("en-US-locale")) - let locale = try! Locale(map: map) + let usLocaleJSONData = JSONDecodingTests.jsonData("en-US-locale") + let locale = try! Client.jsonDecoderWithoutContext.decode(Contentful.Locale.self, from: usLocaleJSONData) return locale } static func klingonWithUSFallback() -> Contentful.Locale { - let map = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("tlh-locale")) - let locale = try! Locale(map: map) + let tlhLocaleJSONData = JSONDecodingTests.jsonData("tlh-locale") + let locale = try! Client.jsonDecoderWithoutContext.decode(Contentful.Locale.self, from: tlhLocaleJSONData) return locale } } @@ -42,18 +41,21 @@ class LocalizationTests: XCTestCase { func testNormalizingFieldsDictionaryFormat() { - let singleLocaleMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("fields-for-default-locale")) + let singleLocaleJSONData = JSONDecodingTests.jsonData("fields-for-default-locale") + let singleLocaleJSON = try! JSONSerialization.jsonObject(with: singleLocaleJSONData, options: []) as! [String: Any] + let singleLocaleFields = singleLocaleJSON["fields"] as! [String: Any] let enUSLocale = LocaleFactory.enUSDefault() - let singleLocaleNormalizedFields = try! Localization.fieldsInMultiLocaleFormat(from: singleLocaleMap, selectedLocale: enUSLocale) + let singleLocaleNormalizedFields = try! Localization.fieldsInMultiLocaleFormat(from: singleLocaleFields, selectedLocale: enUSLocale, wasSelectedOnAPILevel: true) expect((singleLocaleNormalizedFields["name"]?["en-US"] as! String)).to(equal("Happy Cat")) expect(singleLocaleNormalizedFields["name"]?["tlh"]).to(beNil()) // Multi locale format. - let multiLocaleMap = Map(mappingType: .fromJSON, JSON: ObjectMappingTests.jsonData("fields-in-mulit-locale-format")) - - let multiLocaleNormalizedFields = try! Localization.fieldsInMultiLocaleFormat(from: multiLocaleMap, selectedLocale: enUSLocale) + let multiLocaleJSONData = JSONDecodingTests.jsonData("fields-in-mulit-locale-format") + let multiLocaleJSON = try! JSONSerialization.jsonObject(with: multiLocaleJSONData, options: []) as! [String: Any] + let multiLocaleFields = multiLocaleJSON["fields"] as! [String: Any] + let multiLocaleNormalizedFields = try! Localization.fieldsInMultiLocaleFormat(from: multiLocaleFields, selectedLocale: enUSLocale, wasSelectedOnAPILevel: false) expect((multiLocaleNormalizedFields["name"]?["en-US"] as! String)).to(equal("Happy Cat")) expect(multiLocaleNormalizedFields["name"]?["tlh"]).toNot(beNil()) @@ -90,3 +92,4 @@ class LocalizationTests: XCTestCase { waitForExpectations(timeout: 10.0, handler: nil) } } + From 241642a77613ca7bd04ed17259164734f13f4685 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Thu, 28 Sep 2017 14:04:36 +0200 Subject: [PATCH 05/10] Upgrade test dependencies to those with Swift 4 support --- Cartfile.private | 4 ++-- Cartfile.resolved | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cartfile.private b/Cartfile.private index 09c330ae..f29cf008 100644 --- a/Cartfile.private +++ b/Cartfile.private @@ -1,3 +1,3 @@ -github "Quick/Nimble" ~> 7.0.1 -github "venmo/DVR" ~> 1.0.0 +github "Quick/Nimble" ~> 7.0.2 +github "venmo/DVR" ~> 1.1.0 diff --git a/Cartfile.resolved b/Cartfile.resolved index bebcc79b..8ae50567 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,3 +1,3 @@ github "JensRavens/Interstellar" "2.1.0" -github "Quick/Nimble" "v7.0.1" -github "venmo/DVR" "v1.0.1" +github "Quick/Nimble" "v7.0.2" +github "venmo/DVR" "v1.1.0" From 5c47b6990ea7d7d8267369d183be5e999e750185 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Thu, 28 Sep 2017 15:37:04 +0200 Subject: [PATCH 06/10] Add travis build job to run swift build --- .travis.yml | 8 +------- Scripts/travis-build-test.sh | 5 +++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index e58bbdcd..1a8a651e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,6 @@ rvm: cache: bundler matrix: include: - # Xcode8 - - osx_image: xcode8.3 - env: SDK=iphonesimulator PLATFORM="iOS Simulator,name=iPhone 6s,OS=10.3.1" SCHEME=Contentful_iOS - - osx_image: xcode8.3 - env: SDK=appletvsimulator PLATFORM="tvOS Simulator,name=Apple TV 1080p,OS=10.2" SCHEME=Contentful_tvOS - - osx_image: xcode8.3 - env: SDK=macosx PLATFORM="OS X" SCHEME=Contentful_macOS # Xcode9 - osx_image: xcode9 env: SDK=iphonesimulator PLATFORM="iOS Simulator,name=iPhone 6s,OS=11.0" SCHEME=Contentful_iOS @@ -18,6 +11,7 @@ matrix: env: SDK=appletvsimulator PLATFORM="tvOS Simulator,name=Apple TV 1080p,OS=11.0" SCHEME=Contentful_tvOS - osx_image: xcode9 env: SDK=macosx PLATFORM="OS X" SCHEME=Contentful_macOS + - env: SWIFT_BUILD=true script: - ./Scripts/travis-build-test.sh diff --git a/Scripts/travis-build-test.sh b/Scripts/travis-build-test.sh index fdb62272..587f0c39 100755 --- a/Scripts/travis-build-test.sh +++ b/Scripts/travis-build-test.sh @@ -6,6 +6,11 @@ echo "Building" rm -rf ${HOME}/Library/Developer/Xcode/DerivedData/* +if [[ "$SWIFT_BUILD" == "true" ]]; then + swift build + exit 0 +fi + # -jobs -- specify the number of concurrent jobs # `sysctl -n hw.ncpu` -- fetch number of 'logical' cores in macOS machine xcodebuild -jobs `sysctl -n hw.ncpu` test -workspace Contentful.xcworkspace -scheme ${SCHEME} -sdk ${SDK} \ From d4efde7d26ce96170664f4ed4d2b866c92b8f757 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Thu, 28 Sep 2017 16:35:12 +0200 Subject: [PATCH 07/10] Update Package.swift to use swift tools-version 4.0 --- .swift-version | 2 +- Contentful.podspec | 1 - Package.pins | 18 ------------------ Package.resolved | 25 +++++++++++++++++++++++++ Package.swift | 17 ++++++++++++++--- 5 files changed, 40 insertions(+), 23 deletions(-) delete mode 100644 Package.pins create mode 100644 Package.resolved diff --git a/.swift-version b/.swift-version index 8c50098d..5186d070 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.1 +4.0 diff --git a/Contentful.podspec b/Contentful.podspec index d01fead5..921721d1 100644 --- a/Contentful.podspec +++ b/Contentful.podspec @@ -34,7 +34,6 @@ Pod::Spec.new do |spec| spec.watchos.deployment_target = '2.0' spec.tvos.deployment_target = '9.0' - spec.dependency 'ObjectMapper', '~> 2.2' spec.dependency 'Interstellar', '~> 2.1.0' end diff --git a/Package.pins b/Package.pins deleted file mode 100644 index ace1b8c2..00000000 --- a/Package.pins +++ /dev/null @@ -1,18 +0,0 @@ -{ - "autoPin": true, - "pins": [ - { - "package": "Interstellar", - "reason": null, - "repositoryURL": "https://github.com/jensravens/Interstellar", - "version": "2.0.0" - }, - { - "package": "ObjectMapper", - "reason": null, - "repositoryURL": "https://github.com/Hearst-DD/ObjectMapper", - "version": "2.2.6" - } - ], - "version": 1 -} \ No newline at end of file diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..8228ad3b --- /dev/null +++ b/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "Interstellar", + "repositoryURL": "https://github.com/jensravens/Interstellar", + "state": { + "branch": null, + "revision": "5fb8f1f2b7d869380555cca5f44a8cfdca390633", + "version": "2.1.0" + } + }, + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble", + "state": { + "branch": null, + "revision": "38c9ab0846a3fbec308eb2aa9ef68b10a7434eb4", + "version": "7.0.2" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index 35c2ff79..594fd7d9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,10 +1,21 @@ +// swift-tools-version:4.0 import PackageDescription let package = Package( name: "Contentful", + products: [ + .library( + name: "Contentful", + targets: ["Contentful"]) + ], dependencies: [ - .Package(url: "https://github.com/Hearst-DD/ObjectMapper", majorVersion: 2, minor: 2), - .Package(url: "https://github.com/jensravens/Interstellar", majorVersion: 2, minor: 1) + .package(url: "https://github.com/jensravens/Interstellar", .upToNextMinor(from: "2.1.0")) ], - exclude: ["Tests/"] + targets: [ + .target( + name: "Contentful", + dependencies: [ + "Interstellar" + ]) + ] ) From ee46f833a5990f92a73ac197b3b0abfbc6cefe90 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Mon, 2 Oct 2017 11:16:39 +0200 Subject: [PATCH 08/10] Update Changelog and Readme and bump version to 0.10.0 --- .env | 2 +- .envrc | 2 +- CHANGELOG.md | 17 ++++++++++++++--- Config.xcconfig | 2 +- README.md | 17 +++++++---------- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 7c5471b1..554cd5be 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -CONTENTFUL_SDK_VERSION=0.9.3 +CONTENTFUL_SDK_VERSION=0.10.0 diff --git a/.envrc b/.envrc index b7a27d24..c414bebb 100644 --- a/.envrc +++ b/.envrc @@ -1 +1 @@ -export CONTENFUL_SDK_VERSION=0.9.3 +export CONTENFUL_SDK_VERSION=0.10.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index feb6a44f..890e45c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,14 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/) starting from 1.x releases. ### Merged, but not yet released -> ~~All recent changes are published~~ -> #### Changed -> - **BREAKING:** [Interstellar](https://github.com/JensRavens/Interstellar) has been pruned, and therefore all method that previously returned an `Observable` are no longer part of the SDK. +> All recent changes are published + --- ## Table of contents #### 0.x Releases +- `0.10.x` Releases - [0.10.0](#0100) - `0.9.x` Releases - [0.9.0](#090) | [0.9.1](#091) | [0.9.2](#092) | [0.9.3](#093) - `0.8.x` Releases - [0.8.0](#080) - `0.7.x` Releases - [0.7.0](#070) | [0.7.1](#071) | [0.7.2](#072) | [0.7.3](#073) | [0.7.4](#074) | [0.7.5](#075) | [0.7.6](#076) | [0.7.7](#077) @@ -22,6 +22,17 @@ This project adheres to [Semantic Versioning](http://semver.org/) starting from --- +## [`0.10.0`](https://github.com/contentful/contentful.swift/releases/tag/0.10.0) +Released on 2017-10-02 + +#### Changed +- **BREAKING:** The project is now compiled using Swift 4 and therefore must be developed against with Xcode 9. Backwards compatibility with Swift 3 is not possible as the SDK now uses Swift 4 features like JSON decoding via the `Decodable` protocol in Foundation. +- **BREAKING:** `CLLocationCoordinate2D` has been replaced with `Location` type native to the SDK so that linking with CoreLocation is no longer necessary. If you have location enabled queries, you'll need to migrate your code. +• `Resource` is now a protocol and is no longer the base class for `Asset` and `Entry`. `LocalizableResource` is the new base class. +- [ObjectMapper](https://github.com/Hearst-DD/ObjectMapper) has been pruned and is no longer a dependency of the SDK. If managing your dependencies with Carthage, make sure to manually remove ObjectMapper if you aren't using it yourself. + +--- + ## [`0.9.3`](https://github.com/contentful/contentful.swift/releases/tag/0.9.3) Released on 2017-09-08 diff --git a/Config.xcconfig b/Config.xcconfig index 7c5471b1..554cd5be 100644 --- a/Config.xcconfig +++ b/Config.xcconfig @@ -1 +1 @@ -CONTENTFUL_SDK_VERSION=0.9.3 +CONTENTFUL_SDK_VERSION=0.10.0 diff --git a/README.md b/README.md index 1f8f6699..c2d57cee 100644 --- a/README.md +++ b/README.md @@ -75,17 +75,15 @@ Unless you're using Swift 2.x, the project is built and unit tested in Travis CI ## Swift Versioning -The Contentful Swift SDK requires, at minimum, Swift 2.2 and therefore Xcode 7.3. +It is recommended to use Swift 4, as older versions of the SDK will not have fixes backported. If you must use older Swift versions, see the compatible tags below. Swift version | Compatible Contentful tag | | --- | --- | -| Swift 3.0 | [`0.3.0` - `0.9.3`] | +| Swift 4.0 | [ > `0.10.0`] | +| Swift 3.x | [`0.3.0` - `0.9.3`] | | Swift 2.3 | `0.2.3` | | Swift 2.2 | `0.2.1` | -While there have been some patches applied to the [`Swift-2.3` branch][9], no future maintainence is intended on this branch. It is recommended to upgrade to Swift 3 and -use the newest version of contentful.swift. - ### CocoaPods installation [CocoaPods][2] is a dependency manager for Swift, which automates and simplifies the process of using 3rd-party libraries like the Contentful Delivery API in your projects. @@ -99,16 +97,16 @@ pod 'Contentful' You can specify a specific version of Contentful depending on your needs. To learn more about operators for dependency versioning within a Podfile, see the [CocoaPods doc on the Podfile][7]. ```ruby -pod 'Contentful', '~> 0.9.3' +pod 'Contentful', '~> 0.10.0' ``` -Note that for Swift 2.3 support (contentful.swift `v0.2.3`) you will need to add a post-install script to your Podfile if installing with Cocoapods: +Note that for Swift 3 support (contentful.swift versions [`0.3.0` - `0.9.3`]) you will need to add a post-install script to your Podfile if installing with Cocoapods: ```ruby post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| - config.build_settings['SWIFT_VERSION'] = '2.3' + config.build_settings['SWIFT_VERSION'] = '3.2' end end end @@ -119,7 +117,7 @@ end You can also use [Carthage][8] for integration by adding the following to your `Cartfile`: ``` -github "contentful/contentful.swift" ~> 0.9.3 +github "contentful/contentful.swift" ~> 0.10.0 ``` ## License @@ -134,7 +132,6 @@ Copyright (c) 2016 Contentful GmbH. See LICENSE for further details. [6]: https://github.com/contentful-labs/swiftful [7]: https://guides.cocoapods.org/using/the-podfile.html [8]: https://github.com/Carthage/Carthage -[9]: https://github.com/contentful/contentful.swift/tree/Swift-2.3 [10]: https://github.com/contentful/contentful.swift [11]: https://www.contentful.com/developers/docs/references/content-management-api/ [12]: https://cocoapods.org/pods/ContentfulManagementAPI From 72c7dc9c458c74ee38fc6fd846ea58af44c5f1b9 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Mon, 2 Oct 2017 13:39:17 +0200 Subject: [PATCH 09/10] Address PR review comments --- .travis.yml | 3 ++- Sources/Contentful/Client.swift | 8 ++---- Sources/Contentful/ImageOptions.swift | 1 + Tests/ContentfulTests/ImageTests.swift | 36 ++++++++++++++++++++++++++ Tests/ContentfulTests/QueryTests.swift | 4 +-- 5 files changed, 43 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1a8a651e..20024b20 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,8 @@ matrix: env: SDK=appletvsimulator PLATFORM="tvOS Simulator,name=Apple TV 1080p,OS=11.0" SCHEME=Contentful_tvOS - osx_image: xcode9 env: SDK=macosx PLATFORM="OS X" SCHEME=Contentful_macOS - - env: SWIFT_BUILD=true + - osx_image: xcode9 + env: SWIFT_BUILD=true script: - ./Scripts/travis-build-test.sh diff --git a/Sources/Contentful/Client.swift b/Sources/Contentful/Client.swift index 6e1741e0..fcd6491b 100644 --- a/Sources/Contentful/Client.swift +++ b/Sources/Contentful/Client.swift @@ -252,9 +252,8 @@ open class Client { let jsonDecoder = Client.jsonDecoderWithoutContext // JSONDecoder() jsonDecoder.userInfo[LocalizableResource.localizationContextKey] = localizationContext - // Use `Mappable` failable initialzer to optional rather throwing `ImmutableMappable` initializer + // Use failable initialzer to optional rather than initializer that throws, // because failure to find an error in the JSON should error should not throw an error that JSON is not parseable. - if let apiError = ContentfulError.error(with: jsonDecoder, and: data) { completion(Result.error(apiError)) return @@ -263,11 +262,8 @@ open class Client { // Locales will be injected via the JSONDecoder's userInfo property. let decodedObject = try jsonDecoder.decode(DecodableType.self, from: data) completion(Result.success(decodedObject)) - - } catch let error as DecodingError { + } catch { completion(Result.error(SDKError.unparseableJSON(data: data, errorMessage: "The SDK was unable to parse the JSON: \(error)"))) - } catch _ { - completion(Result.error(SDKError.unparseableJSON(data: data, errorMessage: ""))) } } } diff --git a/Sources/Contentful/ImageOptions.swift b/Sources/Contentful/ImageOptions.swift index b48599b4..c8e6aad7 100644 --- a/Sources/Contentful/ImageOptions.swift +++ b/Sources/Contentful/ImageOptions.swift @@ -361,6 +361,7 @@ internal extension CGColor { switch colorSpace.model { case .monochrome: + // In this case, we're assigning the single shade of gray to all of r, g, and b. r = Float(colorComponents[0]) g = Float(colorComponents[0]) b = Float(colorComponents[0]) diff --git a/Tests/ContentfulTests/ImageTests.swift b/Tests/ContentfulTests/ImageTests.swift index 1002e808..d1e6d221 100644 --- a/Tests/ContentfulTests/ImageTests.swift +++ b/Tests/ContentfulTests/ImageTests.swift @@ -12,6 +12,11 @@ import Foundation import XCTest import DVR import Nimble +#if os(iOS) || os(tvOS) || os(watchOS) + import UIKit +#elseif os(macOS) + import Cocoa +#endif class ImageTests: XCTestCase { @@ -39,6 +44,37 @@ class ImageTests: XCTestCase { (client.urlSession as? DVR.Session)?.endRecording() } + func testColorHexRepresenations() { + + #if os(iOS) || os(tvOS) || os(watchOS) + let blueColor = UIColor.blue + #elseif os(macOS) + let blueColor = NSColor.blue + #endif + expect(blueColor.cgColor.hexRepresentation()).to(equal("0000FF")) + + #if os(iOS) || os(tvOS) || os(watchOS) + let redColor = UIColor.red + #elseif os(macOS) + let redColor = NSColor.red + #endif + expect(redColor.cgColor.hexRepresentation()).to(equal("FF0000")) + + #if os(iOS) || os(tvOS) || os(watchOS) + let darkViolet = UIColor(red: 0.580, green: 0.00, blue: 0.830, alpha: 1.0) + #elseif os(macOS) + let darkViolet = NSColor(red: 0.580, green: 0.00, blue: 0.830, alpha: 1.0) + #endif + expect(darkViolet.cgColor.hexRepresentation()).to(equal("9400D4")) + + #if os(iOS) || os(tvOS) || os(watchOS) + let carmine = UIColor(red: 0.66274, green: 0.12549, blue: 0.243137, alpha: 1.0) + #elseif os(macOS) + let carmine = NSColor(red: 0.66274, green: 0.12549, blue: 0.243137, alpha: 1.0) + #endif + expect(carmine.cgColor.hexRepresentation()).to(equal("A9203E")) + } + // MARK: URL construction tests. func testURLIsPropertyConstructedForJPGWithQuality() { diff --git a/Tests/ContentfulTests/QueryTests.swift b/Tests/ContentfulTests/QueryTests.swift index fb14eb99..db27ffd8 100644 --- a/Tests/ContentfulTests/QueryTests.swift +++ b/Tests/ContentfulTests/QueryTests.swift @@ -571,8 +571,8 @@ class QueryTests: XCTestCase { QueryTests.client.fetchMappedEntries(with: query) { result in switch result { case .success(let citiesResponse): - let cities = citiesResponse.items - expect(cities.count).to(equal(4)) + let cities = citiesResponse.items + expect(cities.count).to(equal(4)) case .error(let error): fail("Should not throw an error \(error)") } From 9119e5507c2daa915a0e8e9767c7a0a685e5e000 Mon Sep 17 00:00:00 2001 From: JP Wright Date: Mon, 2 Oct 2017 13:58:16 +0200 Subject: [PATCH 10/10] Fix Playground markup --- .../Contents.swift | 44 +++++++++++-------- .../Contents.swift | 8 ++-- .../Contents.swift | 7 ++- .../Contents.swift | 4 +- .../timeline.xctimeline | 6 --- .../Contents.swift | 4 +- 6 files changed, 36 insertions(+), 37 deletions(-) delete mode 100644 Contentful.playground/Pages/Working with Assets.xcplaygroundpage/timeline.xctimeline diff --git a/Contentful.playground/Pages/Introduction.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Introduction.xcplaygroundpage/Contents.swift index 65d79d05..f3c5787b 100644 --- a/Contentful.playground/Pages/Introduction.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Introduction.xcplaygroundpage/Contents.swift @@ -1,62 +1,68 @@ import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true -/*: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK.*/ -//: ![](contentful-logo-white.png) +//: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK. +/*: + ![](contentful-logo-white.png) -/*: Contentful is an API-first CMS which helps developers deliver content to their apps with API calls, while offering content editors a familiar-looking [web app](https://app.contentful.com) for creating and managing content. This Playground shows how to make API calls to fetch content from Contentful's Content Delivery API (CDA) via the Swift SDK. It also explains what the API response looks like after it has been mapped to native Swift types, and suggests some relevant next steps. */ + Contentful is an API-first CMS which helps developers deliver content to their apps with API calls, while offering content editors a familiar-looking [web app](https://app.contentful.com) for creating and managing content. This Playground shows how to make API calls to fetch content from Contentful's Content Delivery API (CDA) via the Swift SDK. It also explains what the API response looks like after it has been mapped to native Swift types, and suggests some relevant next steps. + */ import Contentful import Interstellar -/*: This is the space identifer. A space is like a project folder in Contentful terms. */ +//: This is the space identifer. A space is like a project folder in Contentful terms. let spaceId = "developer_bookshelf" -/*: This is the access token for this space. You can find both the space id and your CDA access token in the Contentful web app. */ +//: This is the access token for this space. You can find both the space id and your CDA access token in the Contentful web app. let accessToken = "0b7f6x59a0" -/*: ## Make the first request -Create a `Client` object using those credentials, this type is used to make all API requests. */ +/*: + ## Make the first request +Create a `Client` object using those credentials, this type is used to make all API requests. + */ let client = Client(spaceId: spaceId, accessToken: accessToken) -/*: To request an entry with the specified ID: */ +//: To request an entry with the specified ID: client.fetchEntry(id: "5PeGS2SoZGSa4GuiQsigQu") { (result: Result) in switch result { case .error(let error): print("Oh no an error: \(error)!") case .success(let entry): -/*: All resources in Contentful have a variety of read-only, system-managed properties, stored in the “sys” property. This includes things like when the resource was last updated and how many revisions have been published. */ +//: All resources in Contentful have a variety of read-only, system-managed properties, stored in the “sys” property. This includes things like when the resource was last updated and how many revisions have been published. print("The system properties for this entry are: '\(entry.sys)'") -/*: Entries contain a collection of fields, key-value pairs containing the content created in the web app. */ +//: Entries contain a collection of fields, key-value pairs containing the content created in the web app. print("The fields for this entry are: '\(entry.fields)'") -/*: ## Custom content structures +/*: +## Custom content structures Contentful is built on the principle of structured content: a set of key-value pairs is not a great interface to program against if the keys and data types are always changing! Just the same way you can set up any content structure in a MySQL database, you can set up a custom content structure in Contentful, too. There are no presets, templates, or anything of the kind – you can (and should) set everything up depending on the logic of your project. -This structure is maintained by content types, which define what data fields are present in a content entry. */ +This structure is maintained by content types, which define what data fields are present in a content entry. +*/ guard let contentTypeId = entry.sys.contentTypeId else { return } print("The content type for this entry is: '\(contentTypeId)'") } } -/*: This is a link to the content type which defines the structure of "book" entries. Being API-first, we can of course fetch this content type from the API and inspect it to understand what it contains. */ +//: This is a link to the content type which defines the structure of "book" entries. Being API-first, we can of course fetch this content type from the API and inspect it to understand what it contains. client.fetchContentType(id: "book") { (result: Result) in switch result { case .error(let error): print("Oh no an error: \(error)!") case .success(let contentType): -/*: Like entries, content types have a set of read-only system managed properties. */ +//: Like entries, content types have a set of read-only system managed properties. print("The system properties for this content type are '\(contentType.sys)'") -/*: A content type is a container for a collection of fields: */ +//: A content type is a container for a collection of fields: guard let field = contentType.fields.first else { return } -/*: The field ID is used in API responses. */ +//: The field ID is used in API responses. print("The first field for the 'book' content type has an internal identifier: '\(field.id)'") -/*: The field name is shown to editors when changing content in the web app. */ +//: The field name is shown to editors when changing content in the web app. print("The name of the field when editing entries of this content type at app.contentful.com is: '\(field.name)'") -/*: Indicates whether the content in this field can be translated to another language. */ +//: Indicates whether the content in this field can be translated to another language. print("This field is localized, true or false? '\(field.localized)'") -/*: The field type determines what can be stored in the field, and how it is queried. See the [doc on Contentful concepts](https://www.contentful.com/developers/docs/concepts/data-model/) for more information on field types */ +//: The field type determines what can be stored in the field, and how it is queried. See the [doc on Contentful concepts](https://www.contentful.com/developers/docs/concepts/data-model/) for more information on field types print("The type of data that can be stored in this field is '\(field.type)'") } } diff --git a/Contentful.playground/Pages/Modellabe Protocols.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Modellabe Protocols.xcplaygroundpage/Contents.swift index 435d32d8..aa3a66ac 100644 --- a/Contentful.playground/Pages/Modellabe Protocols.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Modellabe Protocols.xcplaygroundpage/Contents.swift @@ -1,10 +1,10 @@ -/*: [Previous](@previous) */ +//: [Previous](@previous) import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true import Contentful import Interstellar -/*: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK.*/ -/*: Sometimes, it is more convenient to work directly with native Swift classes which define the model in our application and use methods which give us back our own types as opposed to `Entry`s. In order to do this, we will define our types to conform to the `EntryModellable` protocol. */ +//: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK. +//: Sometimes, it is more convenient to work directly with native Swift classes which define the model in our application and use methods which give us back our own types as opposed to `Entry`s. In order to do this, we will define our types to conform to the `EntryModellable` protocol. */ class Cat: EntryModellable { let name: String? @@ -46,7 +46,7 @@ let client = Client(spaceId: "cfexampleapi", let query = QueryOn(where: "sys.id", .equals("nyancat")) client.fetchMappedEntries(with: query).next { (mappedCatsArrayResponse: MappedArrayResponse) in guard let nyanCat = mappedCatsArrayResponse.items.first else { return } -/*: We can see that we have directly received a `Cat` via the SDK and we can access properties directly without the indirection of the `fields` dictionary that we would normally access data through on `Entry`. */ +//: We can see that we have directly received a `Cat` via the SDK and we can access properties directly without the indirection of the `fields` dictionary that we would normally access data through on `Entry`. */ guard let name = nyanCat.name else { return } print("The first cat's name is '\(name)'") diff --git a/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift index ba01dcfc..9883126b 100644 --- a/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Search Parameters.xcplaygroundpage/Contents.swift @@ -1,11 +1,10 @@ -/*: - [Previous](@previous) - -Before running this, please build the "Contentful_macOS" scheme to build the SDK. The following is some Playground specific setup. */ +//: [Previous](@previous) import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true import Contentful import Interstellar + +//: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK. //: As a first step, you have to create a client object again. let client = Client(spaceId: "cfexampleapi", accessToken: "b4c0n73n7fu1") /*: diff --git a/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/Contents.swift index 4e8935b4..b8d11ceb 100644 --- a/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/Contents.swift @@ -1,11 +1,11 @@ -/*: [Previous](@previous) */ +//: [Previous](@previous) import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true import Contentful import Interstellar import AppKit -/*: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK.*/ +//: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK. //: We again create an instance of `Client` connected to the space of interest. let client = Client(spaceId: "cfexampleapi", accessToken: "b4c0n73n7fu1") //: Assets represent any kind of media you are storing in Contentful. The API is similar to fetching entries. diff --git a/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/timeline.xctimeline b/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/timeline.xctimeline deleted file mode 100644 index bf468afe..00000000 --- a/Contentful.playground/Pages/Working with Assets.xcplaygroundpage/timeline.xctimeline +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/Contentful.playground/Pages/Working with Entries.xcplaygroundpage/Contents.swift b/Contentful.playground/Pages/Working with Entries.xcplaygroundpage/Contents.swift index 059439f3..28384f94 100644 --- a/Contentful.playground/Pages/Working with Entries.xcplaygroundpage/Contents.swift +++ b/Contentful.playground/Pages/Working with Entries.xcplaygroundpage/Contents.swift @@ -1,10 +1,10 @@ -/*: [Previous](@previous) */ +//: [Previous](@previous) import PlaygroundSupport PlaygroundPage.current.needsIndefiniteExecution = true import Contentful import Interstellar -/*: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK.*/ +//: In order to execute this playground, please build the "Contentful_macOS" scheme to build the SDK. //: As a first step, we again create an instance of `Client` connected to the space of interest. let client = Client(spaceId: "cfexampleapi", accessToken: "b4c0n73n7fu1")