Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swift-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
4.0
6.0
7 changes: 7 additions & 0 deletions .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 27 additions & 8 deletions JSONAPIModel/JSONAPIFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,30 @@
//

import Foundation

import SwiftyJSON

/// Factory for making JSON API models
public final class JSONAPIFactory {
private var models: [String: JSONAPIModelType.Type] = [:]

public init() {
}
public final class JSONAPIFactory: Collection {
public typealias Storage = [String: JSONAPIModelType.Type]
public typealias Index = Storage.Index
public typealias Element = Storage.Element
public typealias Indices = Storage.Indices

private var models: Storage = [:]

public init() {}

public var startIndex: Storage.Index { models.startIndex }
public var endIndex: Storage.Index { models.endIndex }
public var indices: Storage.Indices { models.indices }

public subscript(position: Storage.Index) -> Storage.Element {
models[position]
}

public func index(after i: Storage.Index) -> Storage.Index {
models.index(after: i)
}

/// Register a model type to `self` factory
/// - Parameters modelType: type of model to register
Expand All @@ -33,18 +48,22 @@ public final class JSONAPIFactory {
/// - Returns: created JSON API model, if the type does not exist, nil will be returned
public func createModel(id: String, type: String) -> JSONAPIModelType? {
guard let modelType = models[type] else {
assertionFailure("failed to create model for \(type). Make sure you registered the model type")
return nil
}
return modelType.init(id: id)
}

/// Create JSON API model from given JSON payload, different from creaeting model with just
/// Create JSON API model from given JSON payload, different from creating a model with just
/// id and type, this method also loads attributes and relationships for you.
/// - Parameters id: id of model to create
/// - Parameters type: type of model to create
/// - Returns: created JSON API model, if the type does not exist, nil will be returned
public func createModel(_ json: JSON) throws -> JSONAPIModelType? {
guard let id = json["id"].string, let modelType = json["type"].string else {
guard
let id = json["id"].string,
let modelType = json["type"].string
else {
return nil
}
guard let model = createModel(id: id, type: modelType) else {
Expand Down
1 change: 0 additions & 1 deletion JSONAPIModel/JSONAPIMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import Foundation

import SwiftyJSON

/// Metadata for JSON API model
Expand Down
106 changes: 67 additions & 39 deletions JSONAPIModel/JSONAPIModelType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import Foundation

import SwiftyJSON

/// JSON API model type
Expand All @@ -20,18 +19,18 @@ public protocol JSONAPIModelType {
func mapping(_ map: JSONAPIMap) throws
}

public extension JSONAPIModelType {
extension JSONAPIModelType {
/// Load attributes from given JSON payload into `self` model
/// - Parameters json: json payload to load
func loadAttributes(_ json: JSON) throws {
public func loadAttributes(_ json: JSON) throws {
let map = JSONAPIMap(json: json)
try mapping(map)
}

/// Load relationships from given JSON payload into `self` model
/// - Parameters factory: factory for creating JSON API model
/// - Parameters json: json payload to load
func loadRelationships(_ factory: JSONAPIFactory, json: JSON) {
public func loadRelationships(_ factory: JSONAPIFactory, json: JSON) {
let meta = Self.metadata
let relationships = json["relationships"]
for relationship in meta.relationships {
Expand Down Expand Up @@ -62,50 +61,79 @@ public extension JSONAPIModelType {
/// Load relationships from given JSON API store
/// - Parameters factory: factory for creating JSON API model
/// - Parameters store: JSON API store that contains included payload
func loadIncluded(_ factory: JSONAPIFactory, store: JSONAPIStore) throws {
let meta = Self.metadata
// load attributtes and relationships from the data we found in store
if let json = store.get(type: meta.type, id: id) {
try loadAttributes(json)
loadRelationships(factory, json: json)
}
public func loadIncluded(_ factory: JSONAPIFactory, store: JSONAPIStore) throws {
try loadIncludedIterative(factory, store: store)
}

for relationship in meta.relationships {
switch relationship.type {
case .singular(let getter, let setter):
guard let model = getter(self) else {
continue
}
let modelMeta = type(of: model).metadata
guard let modelJSON = store.get(type: modelMeta.type, id: model.id) else {
setter(self, nil)
continue
}
guard let newModel = try factory.createModel(modelJSON) else {
setter(self, nil)
continue
}
try newModel.loadIncluded(factory, store: store)
setter(self, newModel)
case .multiple(let getter, let setter):
let models = getter(self)
var newModels: [JSONAPIModelType] = []
for model in models {
/// Load relationships from given JSON API store using iterative approach (safer than recursive)
/// - Parameters factory: factory for creating JSON API model
/// - Parameters store: JSON API store that contains included payload
func loadIncludedIterative(_ factory: JSONAPIFactory, store: JSONAPIStore) throws {
// Use a queue to process models iteratively instead of recursively
var processingQueue: [JSONAPIModelType] = [self]
var processedModels: Set<String> = []

while !processingQueue.isEmpty {
let currentModel = processingQueue.removeFirst()
let currentMeta = type(of: currentModel).metadata
let modelKey = "\(currentMeta.type):\(currentModel.id)"

// Skip if already processed to avoid infinite loops
if processedModels.contains(modelKey) {
continue
}
processedModels.insert(modelKey)

// Load attributes and relationships from the data we found in store
if let json = store.get(type: currentMeta.type, id: currentModel.id) {
try currentModel.loadAttributes(json)
currentModel.loadRelationships(factory, json: json)
}

// Process relationships and add related models to queue
for relationship in currentMeta.relationships {
switch relationship.type {
case .singular(let getter, let setter):
guard let model = getter(currentModel) else {
continue
}
let modelMeta = type(of: model).metadata
guard let modelJSON = store.get(type: modelMeta.type, id: model.id) else {
// looks like we cannot find json in store, just add the old one
newModels.append(model)
try model.loadIncluded(factory, store: store)
setter(currentModel, nil)
continue
}
// try to create the model again from the included payload
guard let newModel = try factory.createModel(modelJSON) else {
setter(currentModel, nil)
continue
}
try newModel.loadIncluded(factory, store: store)
newModels.append(newModel)

// Add to processing queue instead of recursive call
processingQueue.append(newModel)
setter(currentModel, newModel)

case .multiple(let getter, let setter):
let models = getter(currentModel)
var newModels: [JSONAPIModelType] = []
for model in models {
let modelMeta = type(of: model).metadata
guard let modelJSON = store.get(type: modelMeta.type, id: model.id) else {
// looks like we cannot find json in store, just add the old one
newModels.append(model)
// Add to processing queue instead of recursive call
processingQueue.append(model)
continue
}
// try to create the model again from the included payload
guard let newModel = try factory.createModel(modelJSON) else {
continue
}

// Add to processing queue instead of recursive call
processingQueue.append(newModel)
newModels.append(newModel)
}
setter(currentModel, newModels)
}
setter(self, newModels)
}
}
}
Expand Down
8 changes: 3 additions & 5 deletions JSONAPIModel/JSONAPISerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@
//

import Foundation

import SwiftyJSON

/// JSONAPISerializer serialize given JSONAPIModelType into JSON
public struct JSONAPISerializer {
public init() {
}
public init() {}

/// Serialize JSON API model into `{"data": {...}, "included": [...]}` format payload
/// - Parameters model: JSONAPIModelType to be serialized
Expand Down Expand Up @@ -54,7 +52,7 @@ public struct JSONAPISerializer {
"type": meta.type
]

if let attributes = map.collectedAttributes, attributes.count > 0 {
if let attributes = map.collectedAttributes, !attributes.isEmpty {
dict["attributes"] = attributes
}

Expand All @@ -65,7 +63,7 @@ public struct JSONAPISerializer {
}
relationships[relationship.key] = value
}
if relationships.count > 0 {
if !relationships.isEmpty {
dict["relationships"] = relationships
}
return dict
Expand Down
30 changes: 27 additions & 3 deletions JSONAPIModel/JSONAPIStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
//

import Foundation

import SwiftyJSON

/// Storage for JSONAPI "included" records
@objc
public final class JSONAPIStore: NSObject {
fileprivate let models: [String: JSON]
public final class JSONAPIStore: NSObject, Collection {
public typealias Storage = [String: JSON]
public typealias Index = Storage.Index
public typealias Element = Storage.Element
public typealias Indeces = Storage.Indices

fileprivate let models: Storage

fileprivate static func mappingKey(type: String, id: String) -> String {
return "\(type)-\(id)"
Expand All @@ -35,6 +39,26 @@ public final class JSONAPIStore: NSObject {
super.init()
}

public var startIndex: Storage.Index {
models.startIndex
}

public var endIndex: Storage.Index {
models.endIndex
}

public var indices: Storage.Indices {
models.indices
}

public subscript(position: Storage.Index) -> Storage.Element {
models[position]
}

public func index(after i: Storage.Index) -> Storage.Index {
models.index(after: i)
}

/// Get model json for given type and id
/// - Parameters type: type of model
/// - Parameters id: id of model
Expand Down
7 changes: 4 additions & 3 deletions JSONAPIModel/JSONMappings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
// Copyright © 2016 Envoy Inc. All rights reserved.
//

import Foundation
import UIKit

import SwiftyJSON

#if canImport(UIKit)
import UIKit

public extension UIColor {
convenience init(hexString: String) {
let scanner = Scanner(string: hexString)
Expand Down Expand Up @@ -40,3 +40,4 @@ public extension JSON {
return UIColor(hexString: hex)
}
}
#endif
50 changes: 26 additions & 24 deletions JSONAPIModel/Transform.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
//

import Foundation
import UIKit

/// Transform is an object allows you to transform given JSON value into desired format,
/// like parsing datetime, hex color and etc
Expand All @@ -25,10 +24,34 @@ public protocol Transform {
func backward(_ value: ValueType?) -> Any?
}

/// Transform for transfroming string to NSURL
public struct URLTransform: Transform {
public typealias ValueType = URL

public init() {
}

public func forward(_ value: Any?) -> URL? {
guard let url = value as? String else {
return nil
}
return URL(string: url)
}

public func backward(_ value: ValueType?) -> Any? {
guard let url = value else {
return nil
}
return url.absoluteString
}
}

#if canImport(UIKit)
import UIKit
/// Transform for transfroming hex string color to UIColor
public struct HexColorTransform: Transform {
public typealias ValueType = UIColor

public init() {
}

Expand All @@ -53,25 +76,4 @@ public struct HexColorTransform: Transform {
}

}

/// Transform for transfroming string to NSURL
public struct URLTransform: Transform {
public typealias ValueType = URL

public init() {
}

public func forward(_ value: Any?) -> URL? {
guard let url = value as? String else {
return nil
}
return URL(string: url)
}

public func backward(_ value: ValueType?) -> Any? {
guard let url = value else {
return nil
}
return url.absoluteString
}
}
#endif
Loading