diff --git a/.travis.yml b/.travis.yml index dedce36..dd061ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: objective-c # xcode_project: SwiftyJSONAPI.xcodeproj # xcode_scheme: SwiftyJSONAPI -osx_image: xcode9.3beta +osx_image: xcode10.2 script: - - xcodebuild clean build test -project SwiftyJSONAPI.xcodeproj -scheme SwiftyJSONAPI -destination 'platform=iOS Simulator,name=iPhone 7,OS=11.3' + - xcodebuild clean build test -project SwiftyJSONAPI.xcodeproj -scheme SwiftyJSONAPI -destination 'platform=iOS Simulator,name=iPhone 7,OS=12.2' diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..b1d4c33 --- /dev/null +++ b/Package.swift @@ -0,0 +1,34 @@ +// swift-tools-version:5.3 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SwiftyJSONAPI", + platforms: [.macOS(.v10_10), + .iOS(.v9)], + products: [ + .library( + name: "SwiftyJSONAPI", + targets: ["SwiftyJSONAPI"]), + ], + targets: [ + .target( + name: "SwiftyJSONAPI", + dependencies: [], + path: "SwiftyJSONAPI", + exclude: ["Info.plist"]), + .testTarget( + name: "SwiftyJSONAPITests", + dependencies: ["SwiftyJSONAPI"], + path: "SwiftyJSONAPITests", + exclude: ["Info.plist"], + resources: [ + .copy("example-bidirectional-relationship.json"), + .copy("example-document-1.json"), + .copy("example-error.json") + ]), + + ], + swiftLanguageVersions: [.v5] +) diff --git a/SwiftyJSONAPI.podspec b/SwiftyJSONAPI.podspec index 24865c1..cb29321 100644 --- a/SwiftyJSONAPI.podspec +++ b/SwiftyJSONAPI.podspec @@ -16,9 +16,9 @@ Pod::Spec.new do |s| # s.name = "SwiftyJSONAPI" - s.version = "0.0.19" + s.version = "0.1.0" s.summary = "JSONAPI document representation and serializing in Swift." - s.swift_version = "4.0" + s.swift_version = "5.0" s.homepage = "https://github.com/thomassnielsen/SwiftyJSONAPI" # s.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif" diff --git a/SwiftyJSONAPI.xcodeproj/project.pbxproj b/SwiftyJSONAPI.xcodeproj/project.pbxproj index 7d54a36..3f89858 100644 --- a/SwiftyJSONAPI.xcodeproj/project.pbxproj +++ b/SwiftyJSONAPI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1C04AAC521370E2000547976 /* example-bidirectional-relationship.json in Resources */ = {isa = PBXBuildFile; fileRef = 1C04AAC421370E2000547976 /* example-bidirectional-relationship.json */; }; 217C53B61BD042E300E82E37 /* JSONAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 217C53B51BD042E300E82E37 /* JSONAPIError.swift */; }; 21B93C981C8E593F00F67D9C /* JSONAPIErrorSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21B93C971C8E593F00F67D9C /* JSONAPIErrorSource.swift */; }; 21E3BD5E1BD53DAE000E4E39 /* example-error.json in Resources */ = {isa = PBXBuildFile; fileRef = 21E3BD5D1BD53DAE000E4E39 /* example-error.json */; }; @@ -32,6 +33,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 1C04AAC421370E2000547976 /* example-bidirectional-relationship.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "example-bidirectional-relationship.json"; sourceTree = ""; }; 217C53B51BD042E300E82E37 /* JSONAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = JSONAPIError.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 21B93C971C8E593F00F67D9C /* JSONAPIErrorSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = JSONAPIErrorSource.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 21E3BD5D1BD53DAE000E4E39 /* example-error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "example-error.json"; sourceTree = ""; }; @@ -109,6 +111,7 @@ E9D236E31BA2D40A00286911 /* Info.plist */, E9D236F61BA2D7B900286911 /* example-document-1.json */, 21E3BD5D1BD53DAE000E4E39 /* example-error.json */, + 1C04AAC421370E2000547976 /* example-bidirectional-relationship.json */, ); path = SwiftyJSONAPITests; sourceTree = ""; @@ -169,25 +172,26 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1020; ORGANIZATIONNAME = "Thomas Sunde Nielsen"; TargetAttributes = { E9D236D11BA2D40A00286911 = { CreatedOnToolsVersion = 7.0; - LastSwiftMigration = 0930; + LastSwiftMigration = 1020; }; E9D236DB1BA2D40A00286911 = { CreatedOnToolsVersion = 7.0; - LastSwiftMigration = 0930; + LastSwiftMigration = 1020; }; }; }; buildConfigurationList = E9D236CC1BA2D40A00286911 /* Build configuration list for PBXProject "SwiftyJSONAPI" */; compatibilityVersion = "Xcode 3.2"; - developmentRegion = English; + developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, + Base, ); mainGroup = E9D236C81BA2D40A00286911; productRefGroup = E9D236D31BA2D40A00286911 /* Products */; @@ -212,6 +216,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1C04AAC521370E2000547976 /* example-bidirectional-relationship.json in Resources */, E9D236F71BA2D7B900286911 /* example-document-1.json in Resources */, 21E3BD5E1BD53DAE000E4E39 /* example-error.json in Resources */, ); @@ -258,6 +263,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -306,7 +312,6 @@ SDKROOT = macosx; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - VALID_ARCHS = "i386 x86_64 armv7s armv7 arm64"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -316,6 +321,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; @@ -357,7 +363,6 @@ SDKROOT = macosx; SUPPORTED_PLATFORMS = "iphonesimulator iphoneos macosx"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - VALID_ARCHS = "i386 x86_64 armv7s armv7 arm64"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; @@ -380,8 +385,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -401,8 +405,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.thomassnielsen.SwiftyJSONAPI; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; @@ -418,8 +421,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks @loader_path/Frameworks @executable_path/Frameworks "; PRODUCT_BUNDLE_IDENTIFIER = com.thomassnielsen.SwiftyJSONAPITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -435,8 +437,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks @loader_path/Frameworks @executable_path/Frameworks "; PRODUCT_BUNDLE_IDENTIFIER = com.thomassnielsen.SwiftyJSONAPITests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_SWIFT3_OBJC_INFERENCE = On; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 5.0; }; name = Release; }; diff --git a/SwiftyJSONAPI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SwiftyJSONAPI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SwiftyJSONAPI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SwiftyJSONAPI.xcodeproj/xcshareddata/xcschemes/SwiftyJSONAPI.xcscheme b/SwiftyJSONAPI.xcodeproj/xcshareddata/xcschemes/SwiftyJSONAPI.xcscheme index 889ee30..a3ab9ef 100644 --- a/SwiftyJSONAPI.xcodeproj/xcshareddata/xcschemes/SwiftyJSONAPI.xcscheme +++ b/SwiftyJSONAPI.xcodeproj/xcshareddata/xcschemes/SwiftyJSONAPI.xcscheme @@ -1,6 +1,6 @@ +typealias CachedResources = [ResourceType : ResourceIds] public class JSONAPIResource: JSONPrinter { public var id = "" @@ -93,7 +96,11 @@ public class JSONAPIResource: JSONPrinter { } func loadResources(withIncludedResources includedResources: ResourcesByTypeAndId) { - + var cachedResources = CachedResources() + _loadResources(withIncludedResources: includedResources, cachedResources: &cachedResources) + } + + fileprivate func _loadResources(withIncludedResources includedResources: ResourcesByTypeAndId, cachedResources: inout CachedResources) { for relationship in self.relationships { for resource in relationship.resources { @@ -103,12 +110,39 @@ public class JSONAPIResource: JSONPrinter { resource.attributes = includedResource.attributes resource.relationships = includedResource.relationships - if !resource.relationships.isEmpty { + + if !resource.relationships.isEmpty, resource.cacheIfNeeded(&cachedResources) { + resource.parent = self.parent - resource.loadResources(withIncludedResources: includedResources) + resource._loadResources(withIncludedResources: includedResources, cachedResources: &cachedResources) } + resource.loaded = .Loaded } } } } + +private extension JSONAPIResource { + + // Checks if a resource has been loaded already, prevents bidirectional relationships from being recursively called + @discardableResult func cacheIfNeeded(_ cache: inout CachedResources) -> Bool { + + if var cachedIds = cache[type] { + + if cachedIds.contains(id) { + + return false + } else { + + cachedIds.insert(id) + cache[type] = cachedIds + return true + } + } else { + + cache[type] = [id] + return true + } + } +} diff --git a/SwiftyJSONAPITests/JSONAPIDocumentTests.swift b/SwiftyJSONAPITests/JSONAPIDocumentTests.swift index f5424cf..1772275 100644 --- a/SwiftyJSONAPITests/JSONAPIDocumentTests.swift +++ b/SwiftyJSONAPITests/JSONAPIDocumentTests.swift @@ -9,80 +9,89 @@ import XCTest @testable import SwiftyJSONAPI -class JSONAPIDocumentTests: XCTestCase { - - var testData: Data! - - override func setUp() { - super.setUp() - // Put setup code here. This method is called before the invocation of each test method in the class. - if let file = Bundle(for: JSONAPIDocumentTests.self).path(forResource: "example-document-1", ofType: "json") { - self.testData = try? Data(contentsOf: URL(fileURLWithPath: file)) - } else { - XCTFail("Could not find test file") +final class JSONAPIDocumentTests: XCTestCase { + + private func fetchDocument(for resource: String) -> JSONAPIDocument? { + #if SWIFT_PACKAGE + guard let url = Bundle.module.url(forResource: resource, withExtension: nil) else { + XCTFail() + return nil } - } - - override func tearDown() { - // Put teardown code here. This method is called after the invocation of each test method in the class. - super.tearDown() + #else + guard let file = Bundle(for: JSONAPIDocumentTests.self).path(forResource: resource, ofType: nil) else { + XCTFail() + return nil + } + let url = URL(fileURLWithPath: file) + #endif + let testData = try! Data(contentsOf: url) + return try? JSONAPIDocument(testData) } func testImportingDocument() { - let document = try! JSONAPIDocument(self.testData) - + guard let document = fetchDocument(for: "example-document-1.json") else { + XCTFail() + return + } + XCTAssert(document.data.count == 1, "Expected number of data elements to be 1, was \(document.data.count)") XCTAssert(document.included.count == 3, "Expected number of included documents to be 2, was \(document.included.count)") XCTAssert(document.links.count == 3, "Expected number of links to be 3, was \(document.links.count)") - + XCTAssertNotNil(document.url, "Expected document to find its own URL from the provided links") } - + func testRelationships() { - let document = try! JSONAPIDocument(self.testData) + guard let document = fetchDocument(for: "example-document-1.json") else { + XCTFail() + return + } let resource = document.data.first! - + XCTAssertNotNil(resource.url, "Resources should have an URL") XCTAssert(resource.relationships.count == 2, "Expected number of relationships to be 2, was \(resource.relationships.count)") } - + func testErrors() { - - if let errorFile = Bundle(for: JSONAPIDocumentTests.self).path(forResource: "example-error", ofType: "json") { - self.testData = try? Data(contentsOf: URL(fileURLWithPath: errorFile)) - } else { - XCTFail("Could not find error test file") + + guard let document = fetchDocument(for: "example-error.json") else { + XCTFail() + return } - - let document = try! JSONAPIDocument(self.testData) let error = document.errors.first! - + XCTAssertNotNil(error.id, "Errors should have an id") XCTAssertEqual(error.status,"422", "Expected error code to be 422") - + } - - func testMeta(){ - let document = try! JSONAPIDocument(self.testData) + + func testMeta() { + guard let document = fetchDocument(for: "example-document-1.json") else { + XCTFail() + return + } let meta = document.meta! let keys = [String](meta.keys) - - XCTAssertNotNil(meta, "document should have meta information") + + XCTAssertNotNil(meta, "document should have meta information") XCTAssertTrue(keys.contains("authors"),"meta should contain a authors key") - + } - - func testResourcesLoadedFromInclude () { - let document = try! JSONAPIDocument(self.testData) + + func testResourcesLoadedFromInclude() { + guard let document = fetchDocument(for: "example-document-1.json") else { + XCTFail() + return + } var authorAttributesCount = 0 var commentsAttributesCount = 0 - + document.loadIncludedResources() - + guard let postResource = document.data.first else { XCTFail("Could not find resource of type Post"); return } - + postResource.relationships.forEach { relationship in switch relationship.type { case "author": @@ -94,17 +103,43 @@ class JSONAPIDocumentTests: XCTestCase { break } } - + XCTAssertTrue(authorAttributesCount == 3,"Author resource should contain 3 attributes") XCTAssertTrue(commentsAttributesCount == 1,"Comment resource should contain 1 attribute") } - -// - func testPerformanceExample() { - // This is an example of a performance test case. - self.measure { - _ = try! JSONAPIDocument(self.testData) + + func testBidirectionalRelationship() { + + guard let document = fetchDocument(for: "example-bidirectional-relationship.json") else { + XCTFail() + return } + var userHasAdressRelationship = false + var addressHasUserRelationship = false + + document.loadIncludedResources() + + let resource = document.data.first + + resource?.relationships.forEach { relationship in + switch relationship.type { + case "user": + userHasAdressRelationship = relationship.hasRelationship(toType: "address") + case "address": + addressHasUserRelationship = relationship.hasRelationship(toType: "user") + default: + // For now we are only handling these 2 cases + break + } + } + + XCTAssertTrue(userHasAdressRelationship, "Expected user to have a relationship to addess") + XCTAssertTrue(addressHasUserRelationship, "Expected address to have a relationship to a user") } +} +private extension JSONAPIRelationship { + func hasRelationship(toType type: String) -> Bool { + return resources.first?.relationships.contains(where: { $0.type == type }) == true + } } diff --git a/SwiftyJSONAPITests/example-bidirectional-relationship.json b/SwiftyJSONAPITests/example-bidirectional-relationship.json new file mode 100644 index 0000000..00339c2 --- /dev/null +++ b/SwiftyJSONAPITests/example-bidirectional-relationship.json @@ -0,0 +1,72 @@ +{ + "data":[ + { + "type":"profile", + "id":"1", + "attributes":{ + "type":"member" + }, + "relationships":{ + "user":{ + "data":[ + { + "type":"user", + "id":"32" + } + ] + }, + "address":{ + "data":[ + { + "type":"address", + "id":"5" + } + ] + } + } + } + ], + "included":[ + { + "type":"user", + "id":"32", + "attributes":{ + "first-name":"Johnny", + "last-name":"Appleseed", + "twitter":"jappleseed" + }, + "relationships":{ + "address":{ + "data":{ + "type":"address", + "id":"5" + } + } + } + }, + { + "type":"address", + "id":"5", + "attributes":{ + "street":"Apple tree lane" + }, + "relationships":{ + "user":{ + "data":{ + "type":"user", + "id":"32" + } + } + } + } + ], + "meta":{ + "copyright":"Copyright 2015 Example Corp.", + "authors":[ + "Yehuda Katz", + "Steve Klabnik", + "Dan Gebhardt", + "Tyler Kellen" + ] + } +}