diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..7a6ea55 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,44 @@ +# Report + +> The more information you provide, the faster we can help you. + +⚠️ Select what you want - **a feature request** or **report a bug**. Please remove the section you aren't interested in. + +## A feature request + +### What do you want to add? + +> Please describe what you want to add to the component. + +### How should it look like? + +> Please add images. + +## Report a bug + +### What did you do? + +> Please replace this with what you did. + +### What did you expect to happen? + +> Please replace this with what you expected to happen. + +### What happened instead? + +> Please replace this with what happened instead. + +### Your Environment + +- Version of the component: _insert here_ +- Swift version: _insert here_ +- iOS version: _insert here_ +- Device: _insert here_ +- Xcode version: _insert here_ +- If you use Cocoapods: _run `pod env | pbcopy` and insert here_ +- If you use Carthage: _run `carthage version | pbcopy` and insert here_ + +### Project that demonstrates the bug + +> Please add a link to a project we can download that reproduces the bug. + diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 0000000..89acd87 --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,59 @@ +## How to contribute to DBClient + +#### **Did you find a bug?** + +* **Ensure the bug was not already reported** by searching under [Issues](https://github.com/Yalantis/DBClient/issues). + +* If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Yalantis/DBClient/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **example project** demonstrating the expected behavior that is not occurring. + +* Fill appropriate section in issue template and remove the section you aren't interested in. + +#### **Did you write a patch that fixes a bug?** + +* Open a new GitHub pull request with the patch. + +* Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. + +* Ensure the PR doesn't extend the number of existing issues. + +#### **Did you fix whitespace, format code, or make a purely cosmetic patch?** + +* Changes that are **cosmetic** in nature and **do not add anything substantial** to the stability or functionality of DBClient will generally **not be accepted**. + +#### **Did you write patch that extends functionality?** + +* Ensure the functionality you trying to add needed not only for your case. + +#### Each issue will be labeled by it's `type`, `priority` and `status`. + +**Issue types:** +* Bug +* Enhancement + +**These are the available priority labels:** +* Critical +* High +* Medium +* Low + +**Status label will be assigned to your issue to keep the issue tracker easy to follow:** +* Queued (will be reviewed soon) +* Reviewed (assignee has read it) +* Pending (will work on it soon) +* Work in progress (is working on it now) +* On hold +* Invalid (if bug it's not reproducible) +* Need feedback (signal to get people to read and comment or provide help) + +#### **Coding Style** + +* Most importantly, match the existing code style as much as possible. + +#### **Do you have a question?** + +For any usage questions that are not specific to the project itself, please ask on [Stack Overflow](https://stackoverflow.com/). By doing so, you'll be more likely to quickly solve your problem, and you'll allow anyone else with the same question to find the answer. This also allows maintainers to focus on improving the project for others. + +## Thank you! + +#### [![Yalantis](https://raw.githubusercontent.com/Yalantis/PullToMakeSoup/master/PullToMakeSoupDemo/Resouces/badge_dark.png)](https://Yalantis.com/?utm_source=github) + diff --git a/.gitignore b/.gitignore index 20ba027..2a68483 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,36 @@ timeline.xctimeline playground.xcworkspace # CocoaPods -Pods/ \ No newline at end of file +Pods/ + +# Carthage +Carthage/Build +Carthage/Checkouts + +# fastlane +fastlane/report.xml + +# development + +logs/* + +# AppCode + +.idea/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the +# screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/README.md +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +## GitLab CI +.bundle/ +vendor/ \ No newline at end of file diff --git a/.swift-version b/.swift-version index f398a20..819e07a 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -3.0 \ No newline at end of file +5.0 diff --git a/DBClient.podspec b/DBClient.podspec index c44954b..6b2eb23 100644 --- a/DBClient.podspec +++ b/DBClient.podspec @@ -1,35 +1,31 @@ Pod::Spec.new do |s| - s.name = "DBClient" - s.version = "0.1" + s.name = 'DBClient' + s.version = '1.4.2' s.requires_arc = true - s.summary = "CoreData & Realm wrapper written on Swift" - s.homepage = "" - s.license = 'MIT' - s.author = { "Yalantis" => "mail@yalantis.com" } - s.source = { :git => "https://git.yalantis.com/roman.kyrylenko/DBClient.git" } + s.summary = 'CoreData & Realm wrapper written on Swift' + s.homepage = '' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'Yalantis' => 'mail@yalantis.com' } + s.source = { :git => 'https://github.com/Yalantis/DBClient.git', :tag => s.version } s.social_media_url = 'https://yalantis.com/' s.homepage = 'https://yalantis.com/' + s.ios.deployment_target = '10.0' + s.dependency 'YALResult', '1.4' + s.default_subspec = 'Core' - s.ios.deployment_target = "9.0" - - s.default_subspec = "Core" - - s.subspec "Core" do |spec| + s.subspec 'Core' do |spec| spec.source_files = ['DBClient/Core/*.swift'] - spec.dependency "Bolts-Swift", "~> 1.3.0" - spec.frameworks = ['Foundation'] end - s.subspec "CoreData" do |spec| - spec.dependency "DBClient/Core" + s.subspec 'CoreData' do |spec| + spec.dependency 'DBClient/Core' spec.source_files = ['DBClient/CoreData/*.swift'] spec.frameworks = ['CoreData'] end - s.subspec "Realm" do |spec| - spec.dependency "DBClient/Core" + s.subspec 'Realm' do |spec| + spec.dependency 'DBClient/Core' spec.source_files = ['DBClient/Realm/*.swift'] - spec.dependency "RealmSwift", "~> 2.1.1" + spec.dependency 'RealmSwift', '~> 3.15.0' end - -end \ No newline at end of file +end diff --git a/DBClient/Core/DBClient.swift b/DBClient/Core/DBClient.swift index 46ebf11..ee4a7e0 100644 --- a/DBClient/Core/DBClient.swift +++ b/DBClient/Core/DBClient.swift @@ -1,164 +1,261 @@ // // DBClient.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Yury Grinenko on 03.11.16. // Copyright © 2016 Yalantis. All rights reserved. // -import BoltsSwift +import Foundation +import YALResult -/** - Protocol for transaction restrictions in `DBClient`. - Used for transactions of all type. -*/ -public protocol Stored { - - /// Primary key for an object. - static var primaryKey: String? { get } +public typealias Result = YALResult +public enum DBClientError: Error { + + case missingPrimaryKey, missingData } -public extension Stored { - - public static var primaryKey: String? { return nil } - +/// Protocol for transaction restrictions in `DBClient`. +/// Used for transactions of all type. +public protocol Stored { + + /// Primary key for an object. + static var primaryKeyName: String? { get } + + /// Primary value for an instance + var valueOfPrimaryKey: CVarArg? { get } } /// Describes abstract database transactions, common for all engines. public protocol DBClient { - - /** - Executes given request and returns result wrapped in `Task`. - - Parameter request: request to execute. + /// Executes given request and calls completion result wrapped in `Result`. + /// + /// - Parameters: + /// - request: request to execute + /// - completion: `Result` with array of objects or error in case of failude. + func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) - - Returns: task with result or error in appropriate state. - */ - func execute(_ request: FetchRequest) -> Task<[T]> - - /** - Creates observable request from given `FetchRequest`. - - - Parameter request: fetch request to be observed - - - Returns: observable of for given request. - */ - func observable(for request: FetchRequest) -> RequestObservable - - /** - Saves objects to database. - - - Parameter objects: list of objects to be saved - - - Returns: `Task` with saved objects or appropriate error in case of failure. - */ - @discardableResult func save(_ objects: [T]) -> Task<[T]> - - /** - Updates changed performed with objects to database. - - - Parameter objects: list of objects to be updated - - - Returns: `Task` with updated objects or appropriate error in case of failure. - */ - @discardableResult func update(_ objects: [T]) -> Task<[T]> - - /** - Deletes objects from database. - - - Parameter objects: list of objects to be deleted - - - Returns: `Task` with deleted objects or appropriate error in case of failure. - */ - @discardableResult func delete(_ objects: [T]) -> Task<[T]> - + /// Creates observable request from given `FetchRequest`. + /// + /// - Parameter request: fetch request to be observed + /// - Returns: observable of for given request. + func observable(for request: FetchRequest) -> RequestObservable + + /// Inserts objects to database. + /// + /// - Parameters: + /// - objects: list of objects to be inserted + /// - completion: `Result` with inserted objects or appropriate error in case of failure. + func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) + + /// Updates changed performed with objects to database. + /// + /// - Parameters: + /// - objects: list of objects to be updated + /// - completion: `Result` with updated objects or appropriate error in case of failure. + func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) + + /// Deletes objects from database. + /// + /// - Parameters: + /// - objects: list of objects to be deleted + /// - completion: `Result` with appropriate error in case of failure. + func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) + + /// Removes all object of a given type from database. + func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) + + /// Iterates through given objects and updates existing in database instances or creates them + /// + /// - Parameters: + /// - objects: objects to be worked with + /// - completion: `Result` with inserted and updated instances. + func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) + + /// Synchronously executes given request and calls completion result wrapped in `Result`. + /// + /// - Parameters: + /// - request: request to execute + /// - Returns: `Result` with array of objects or error in case of failude. + func execute(_ request: FetchRequest) -> Result<[T]> + + /// Synchronously inserts objects to database. + /// + /// - Parameters: + /// - objects: list of objects to be inserted + /// - Returns: `Result` with inserted objects or appropriate error in case of failure. + @discardableResult + func insert(_ objects: [T]) -> Result<[T]> + + /// Synchronously updates changed performed with objects to database. + /// + /// - Parameters: + /// - objects: list of objects to be updated + /// - Returns: `Result` with updated objects or appropriate error in case of failure. + @discardableResult + func update(_ objects: [T]) -> Result<[T]> + + /// Synchronously deletes objects from database. + /// + /// - Parameters: + /// - objects: list of objects to be deleted + /// - Returns: `Result` with appropriate error in case of failure. + @discardableResult + func delete(_ objects: [T]) -> Result + + /// Synchronously iterates through given objects and updates existing in database instances or creates them + /// + /// - Parameters: + /// - objects: objects to be worked with + /// - Returns: `Result` with inserted and updated instances. + @discardableResult + func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> } public extension DBClient { - /// Fetch all entities from database - /// - /// - Returns: Task with array of objects - func fetchAll() -> Task<[T]> { - return execute(FetchRequest()) - } - - /** - Finds first element with given value as primary. - If no primary key specified for given type, or object with such value doesn't exist returns nil. - - - Parameters: - - type: Type of object to search for - - primaryValue: The value of primary key field to search for - - - Returns: `Task` with found object or nil. - */ - func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil) -> Task { - guard let primaryKey = type.primaryKey else { - return Task(nil) + /// Fetch all entities from database + /// + /// - Parameter completion: `Result` with array of objects + func findAll(completion: @escaping (Result<[T]>) -> Void) { + execute(FetchRequest(), completion: completion) } - let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) - var fetchPredicate: NSPredicate - if let predicate = predicate { - fetchPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [primaryKeyPredicate, predicate]) - } else { - fetchPredicate = primaryKeyPredicate + /// Synchronously fetch all entities from database + /// + /// - Returns: `Result` with array of objects + func findAll() -> Result<[T]> { + return execute(FetchRequest()) } - let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) - - return execute(request).continueWithTask { task -> Task in - if let first = task.result?.first { - return Task(first) - } - return Task(nil) + + /// Finds first element with given value as primary. + /// If no primary key specified for given type, or object with such value doesn't exist returns nil. + /// + /// - Parameters: + /// - type: type of object to search for + /// - primaryValue: the value of primary key field to search for + /// - predicate: predicate for request + /// - completion: `Result` with found object or nil + func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil, completion: @escaping (Result) -> Void) { + guard let primaryKey = type.primaryKeyName else { + completion(.failure(DBClientError.missingPrimaryKey)) + return + } + + let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) + let fetchPredicate: NSPredicate + if let predicate = predicate { + fetchPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [primaryKeyPredicate, predicate]) + } else { + fetchPredicate = primaryKeyPredicate + } + let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) + + execute(request) { result in + completion(result.map({ $0.first })) + } } - } - - /** - Deletes object from database. - - - Parameter object: object to be deleted - - - Returns: `Task` with deleted object or appropriate error in case of failure. - */ - @discardableResult func delete(_ object: T) -> Task { - return convertArrayTaskToSingleObject(delete([object])) - } - - /** - Updates changed performed with object to database. - - - Parameter object: object to be updated - - - Returns: `Task` with updated object or appropriate error in case of failure. - */ - @discardableResult func update(_ object: T) -> Task { - return convertArrayTaskToSingleObject(update([object])) - } - - /** - Saves object to database. - - - Parameter object: object to be saved - - - Returns: `Task` with saved object or appropriate error in case of failure. - */ - @discardableResult func save(_ object: T) -> Task { - return convertArrayTaskToSingleObject(save([object])) - } - - private func convertArrayTaskToSingleObject(_ task: Task<[T]>) -> Task { - return task.continueWithTask { task -> Task in - if let objects = task.result, let object = objects.first { - return Task(object) - } else if let error = task.error { - return Task(error: error) - } else { // no objects returned - return Task.cancelledTask() - } + + /// Synchronously finds first element with given value as primary. + /// If no primary key specified for given type, or object with such value doesn't exist returns nil. + /// + /// - Parameters: + /// - type: type of object to search for + /// - primaryValue: the value of primary key field to search for + /// - predicate: predicate for request + /// - Returns: `Result` with found object or nil + func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil) -> Result { + guard let primaryKey = type.primaryKeyName else { + return .failure(DBClientError.missingPrimaryKey) + } + + let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) + let fetchPredicate: NSPredicate + if let predicate = predicate { + fetchPredicate = NSCompoundPredicate(andPredicateWithSubpredicates: [primaryKeyPredicate, predicate]) + } else { + fetchPredicate = primaryKeyPredicate + } + let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) + + let result = execute(request) + return result.map({ $0.first }) + } + + /// Inserts object to database. + /// + /// - Parameters: + /// - object: object to be inserted + /// - completion: `Result` with inserted object or appropriate error in case of failure. + func insert(_ object: T, completion: @escaping (Result) -> Void) { + insert([object], completion: { completion($0.next(self.convertArrayTaskToSingleObject)) }) + } + + @discardableResult + func insert(_ object: T) -> Result { + return insert([object]).next(convertArrayTaskToSingleObject) + } + + /// Updates changed performed with object to database. + /// + /// - Parameters: + /// - object: object to be updated + /// - completion: `Result` with updated object or appropriate error in case of failure. + func update(_ object: T, completion: @escaping (Result) -> Void) { + update([object], completion: { completion($0.next(self.convertArrayTaskToSingleObject)) }) + } + + @discardableResult + func update(_ object: T) -> Result { + return update([object]).next(convertArrayTaskToSingleObject) + } + + /// Deletes object from database. + /// + /// - Parameters: + /// - object: object to be deleted + /// - completion: `Result` with appropriate error in case of failure. + func delete(_ object: T, completion: @escaping (Result<()>) -> Void) { + delete([object], completion: completion) + } + + @discardableResult + func delete(_ object: T) -> Result { + return delete([object]) + } + + /// Updates existing in database instances or creates them using upsert method defined in your model + /// + /// - Parameters: + /// - object: object to be worked with + /// - completion: `Result` with inserted or updated instance. + func upsert(_ object: T, completion: @escaping (Result<(object: T, isUpdated: Bool)>) -> Void) { + upsert([object]) { result in + completion(result.next { (updated: [T], inserted: [T]) -> Result<(object: T, isUpdated: Bool)> in + guard let object = updated.first ?? inserted.first else { + return Result.failure(DBClientError.missingData) + } + return Result.success((object: object, isUpdated: !updated.isEmpty)) + }) + } + } + + @discardableResult + func upsert(_ object: T) -> Result<(object: T, isUpdated: Bool)> { + return upsert([object]).next { (updated: [T], inserted: [T]) in + guard let object = updated.first ?? inserted.first else { + return Result.failure(DBClientError.missingData) + } + return Result.success((object: object, isUpdated: !updated.isEmpty)) + } + } + + private func convertArrayTaskToSingleObject(_ array: [T]) -> Result { + guard let first = array.first else { + return .failure(DBClientError.missingData) + } + return .success(first) } - } - } diff --git a/DBClient/Core/DatabaseError.swift b/DBClient/Core/DatabaseError.swift index ec76c89..2fd8f8e 100644 --- a/DBClient/Core/DatabaseError.swift +++ b/DBClient/Core/DatabaseError.swift @@ -1,6 +1,6 @@ // // DatabaseError.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 19/12/16. // Copyright © 2016 Yalantis. All rights reserved. @@ -13,6 +13,7 @@ import Foundation /// - write: For write transactions. /// - read: For read transactions. public enum DatabaseError: Error { - - case write, read + + case write, read + } diff --git a/DBClient/Core/FetchRequest.swift b/DBClient/Core/FetchRequest.swift index 638fef7..da7c6d8 100644 --- a/DBClient/Core/FetchRequest.swift +++ b/DBClient/Core/FetchRequest.swift @@ -1,6 +1,6 @@ // // FetchRequest.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 15/12/16. // Copyright © 2016 Yalantis. All rights reserved. @@ -10,114 +10,100 @@ import Foundation /// Describes a fetch request to get objects from a database. public struct FetchRequest { - - public let sortDescriptor: NSSortDescriptor? - public let predicate: NSPredicate? - public let fetchOffset: Int - public let fetchLimit: Int - - /// - Parameters: - /// - predicate: Predicate for objects filtering; nil by default. - /// - sortDescriptor: Sort descriptor; nil by default. - /// - fetchOffset: Offset of data for request; 0 by default (no offset). - /// - fetchLimit: Amount of objects to be fetched; no limit if zero given; 0 by default. - public init( - predicate: NSPredicate? = nil, - sortDescriptor: NSSortDescriptor? = nil, - fetchOffset: Int = 0, - fetchLimit: Int = 0) { - self.predicate = predicate - self.sortDescriptor = sortDescriptor - self.fetchOffset = fetchOffset - self.fetchLimit = fetchLimit - } - + + public let sortDescriptors: [NSSortDescriptor]? + public let predicate: NSPredicate? + public let fetchOffset: Int + public let fetchLimit: Int + + /// - Parameters: + /// - predicate: Predicate for objects filtering; nil by default. + /// - sortDescriptors: Sort descriptors; nil by default. + /// - fetchOffset: Offset of data for request; 0 by default (no offset). + /// - fetchLimit: Amount of objects to be fetched; no limit if zero given; 0 by default. + public init(predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil, fetchOffset: Int = 0, fetchLimit: Int = 0) { + self.predicate = predicate + self.sortDescriptors = sortDescriptors + self.fetchOffset = fetchOffset + self.fetchLimit = fetchLimit + } } // MARK: - Filtering public extension FetchRequest { - - /** - Filters all objects with given predicate - - - Returns: New instance - */ - func filtered(with predicate: NSPredicate) -> FetchRequest { - return request(withPredicate: predicate) - } - - /** - Filters all objects to match `key`=`value`. - - - Returns: New intance - */ - func filtered(with key: String, equalTo value: String) -> FetchRequest { - return request(withPredicate: NSPredicate(format: "\(key) == %@", value)) - } - - /** - Removes any object with value of `key` property not from given array of values from request. - - - Returns: New instance - */ - func filtered(with key: String, in value: [String]) -> FetchRequest { - return request(withPredicate: NSPredicate(format: "\(key) IN %@", value)) - } - - /** - Removes any object with value of `key` property from given array of values from request. - - - Returns: New instance - */ - func filtered(with key: String, notIn value: [String]) -> FetchRequest { - return request(withPredicate: NSPredicate(format: "NOT (\(key) IN %@)", value)) - } - + + /** + Filters all objects with given predicate + + - Returns: New instance + */ + func filtered(with predicate: NSPredicate) -> FetchRequest { + return request(withPredicate: predicate) + } + + /** + Filters all objects to match `key`=`value`. + + - Returns: New intance + */ + func filtered(with key: String, equalTo value: String) -> FetchRequest { + return request(withPredicate: NSPredicate(format: "\(key) == %@", value)) + } + + /** + Removes any object with value of `key` property not from given array of values from request. + + - Returns: New instance + */ + func filtered(with key: String, in value: [String]) -> FetchRequest { + return request(withPredicate: NSPredicate(format: "\(key) IN %@", value)) + } + + /** + Removes any object with value of `key` property from given array of values from request. + + - Returns: New instance + */ + func filtered(with key: String, notIn value: [String]) -> FetchRequest { + return request(withPredicate: NSPredicate(format: "NOT (\(key) IN %@)", value)) + } } // MARK: - Sorting public extension FetchRequest { - - func sorted(with sortDescriptor: NSSortDescriptor) -> FetchRequest { - return request(withSortDescriptor: sortDescriptor) - } - - func sorted(with key: String?, ascending: Bool, comparator cmptr: @escaping Comparator) -> FetchRequest { - return request(withSortDescriptor: NSSortDescriptor(key: key, ascending: ascending, comparator: cmptr)) - } - - func sorted(with key: String?, ascending: Bool) -> FetchRequest { - return request(withSortDescriptor: NSSortDescriptor(key: key, ascending: ascending)) - } - - func sorted(with key: String?, ascending: Bool, selector: Selector) -> FetchRequest { - return request(withSortDescriptor: NSSortDescriptor(key: key, ascending: ascending, selector: selector)) - } - + + func sorted(with sortDescriptor: NSSortDescriptor) -> FetchRequest { + return request(withSortDescriptors: [sortDescriptor]) + } + + func sorted(with sortDescriptors: [NSSortDescriptor]) -> FetchRequest { + return request(withSortDescriptors: sortDescriptors) + } + + func sorted(with key: String?, ascending: Bool, comparator cmptr: @escaping Comparator) -> FetchRequest { + return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, comparator: cmptr)]) + } + + func sorted(with key: String?, ascending: Bool) -> FetchRequest { + return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending)]) + } + + func sorted(with key: String?, ascending: Bool, selector: Selector) -> FetchRequest { + return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, selector: selector)]) + } } // MARK: - Private private extension FetchRequest { - - func request(withPredicate predicate: NSPredicate) -> FetchRequest { - return FetchRequest( - predicate: predicate, - sortDescriptor: sortDescriptor, - fetchOffset: fetchOffset, - fetchLimit: fetchLimit - ) - } - - func request(withSortDescriptor sortDescriptor: NSSortDescriptor) -> FetchRequest { - return FetchRequest( - predicate: predicate, - sortDescriptor: sortDescriptor, - fetchOffset: fetchOffset, - fetchLimit: fetchLimit - ) - } - + + func request(withPredicate predicate: NSPredicate) -> FetchRequest { + return FetchRequest(predicate: predicate, sortDescriptors: sortDescriptors, fetchOffset: fetchOffset, fetchLimit: fetchLimit) + } + + func request(withSortDescriptors sortDescriptors: [NSSortDescriptor]) -> FetchRequest { + return FetchRequest(predicate: predicate, sortDescriptors: sortDescriptors, fetchOffset: fetchOffset, fetchLimit: fetchLimit) + } } diff --git a/DBClient/Core/RequestObservable.swift b/DBClient/Core/RequestObservable.swift index 0ffd335..f234e43 100644 --- a/DBClient/Core/RequestObservable.swift +++ b/DBClient/Core/RequestObservable.swift @@ -1,6 +1,6 @@ // // RequestObservable.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 15/12/16. // Copyright © 2016 Yalantis. All rights reserved. @@ -10,31 +10,40 @@ import Foundation /// Describes changes in database: /// -/// - initial: initial storred entities. -/// - update: deletions, insertions, modifications. +/// - initial: initial storred entities; +/// - update: +/// -- objects: all objects in current version of the collection; +/// -- deletions: the indices in the previous version of the collection which were removed from this one; +/// -- insertions: the indices in the new collection and object which was added in this version; +/// -- modifications: the indices of the objects in the new collection and objects inself which was modified in this version; /// - error: an error occurred during fetch. public enum ObservableChange { - - case initial([T]) - case change(objects: [T], deletions: [Int], insertions: [(index: Int, element: T)], modifications: [(index: Int, element: T)]) - case error(Error) - + + public typealias ModelChange = ( + objects: [T], + deletions: [Int], + insertions: [(index: Int, element: T)], + modifications: [(index: Int, element: T)] + ) + + case initial([T]) + case change(ModelChange) + case error(Error) } public class RequestObservable { - - let request: FetchRequest - - init(request: FetchRequest) { - self.request = request - } - - /// Starts observing with a given fetch request. - /// - /// - Parameter closure: gets called once any changes in database are occurred. - /// - Warning: You cannot call the method only if you don't observe it now. - public func observe(_ closure: @escaping (ObservableChange) -> Void) { - assertionFailure("The observe method must be overriden") - } - + + let request: FetchRequest + + init(request: FetchRequest) { + self.request = request + } + + /// Starts observing with a given fetch request. + /// + /// - Parameter closure: gets called once any changes in database are occurred. + /// - Warning: You cannot call the method only if you don't observe it now. + public func observe(_ closure: @escaping (ObservableChange) -> Void) { + assertionFailure("The observe method must be overriden") + } } diff --git a/DBClient/CoreData/CoreDataChange.swift b/DBClient/CoreData/CoreDataChange.swift index 9d4816c..0e67796 100644 --- a/DBClient/CoreData/CoreDataChange.swift +++ b/DBClient/CoreData/CoreDataChange.swift @@ -1,6 +1,6 @@ // // CoreDataChange.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 19/12/16. // Copyright © 2016 Yalantis. All rights reserved. @@ -9,46 +9,46 @@ import Foundation enum CoreDataChange { - - case update(Int, T) - case delete(Int, T) - case insert(Int, T) - - func object() -> T { - switch self { - case .update(_, let object): return object - case .delete(_, let object): return object - case .insert(_, let object): return object + + case update(Int, T) + case delete(Int, T) + case insert(Int, T) + + func object() -> T { + switch self { + case .update(_, let object): return object + case .delete(_, let object): return object + case .insert(_, let object): return object + } } - } - - func index() -> Int { - switch self { - case .update(let index, _): return index - case .delete(let index, _): return index - case .insert(let index, _): return index + + func index() -> Int { + switch self { + case .update(let index, _): return index + case .delete(let index, _): return index + case .insert(let index, _): return index + } } - } - - var isDeletion: Bool { - switch self { - case .delete(_): return true - default: return false + + var isDeletion: Bool { + switch self { + case .delete(_): return true + default: return false + } } - } - - var isUpdate: Bool { - switch self { - case .update(_): return true - default: return false + + var isUpdate: Bool { + switch self { + case .update(_): return true + default: return false + } } - } - - var isInsertion: Bool { - switch self { - case .insert(_): return true - default: return false + + var isInsertion: Bool { + switch self { + case .insert(_): return true + default: return false + } } - } - + } diff --git a/DBClient/CoreData/CoreDataDBClient.swift b/DBClient/CoreData/CoreDataDBClient.swift index 6202fa5..c8799fb 100644 --- a/DBClient/CoreData/CoreDataDBClient.swift +++ b/DBClient/CoreData/CoreDataDBClient.swift @@ -1,209 +1,620 @@ // // CoreDataDBClient.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Yury Grinenko on 03.11.16. // Copyright © 2016 Yalantis. All rights reserved. // import CoreData -import BoltsSwift -/** - Describes type of model for CoreData database client. - Model should conform to CoreDataModelConvertible protocol - for ability to be fetched/saved/updated/deleted in CoreData -*/ +/// Describes type of model for CoreData database client. +/// Model should conform to CoreDataModelConvertible protocol +/// for ability to be fetched/saved/updated/deleted in CoreData public protocol CoreDataModelConvertible: Stored { - - /** - Returns type of object for model. - */ - static func managedObjectClass() -> NSManagedObject.Type - - /** - Executes mapping from `NSManagedObject` instance. - - - Parameter managedObject: Object to be mapped from - - - Returns: Mapped object. - */ - static func from(_ managedObject: NSManagedObject) -> Stored - - /** - Executes backward mapping to `NSManagedObject` from given context - - - Parameter context: Context, where object should be created. - - - Returns: Created instance - */ - func toManagedObject(in context: NSManagedObjectContext) -> NSManagedObject - - static var entityName: String { get } - + + /// Returns type of object for model. + static func managedObjectClass() -> NSManagedObject.Type + + /// Executes mapping from `NSManagedObject` instance. + /// + /// - Parameter managedObject: object to be mapped from. + /// - Returns: mapped object. + static func from(_ managedObject: NSManagedObject) -> Stored + + /// Executes backward mapping to `NSManagedObject` from given context + /// + /// - Parameters: + /// - context: context, where object should be created; + /// - existedInstance: if instance was already created it will be passed. + /// - Returns: created instance. + func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject + + /// The name of the entity from ".xcdatamodeld" + static var entityName: String { get } + + /// Decides whether primary value of object equal to given + func isPrimaryValueEqualTo(value: Any) -> Bool } -extension NSManagedObject: Stored {} +extension NSManagedObject: Stored { + + public static var primaryKeyName: String? { return nil } + + public var valueOfPrimaryKey: CVarArg? { return nil } +} -// TODO: If it is possible, need some way to avoid calling DBClient functions with objects -// which don't conform to CoreDataModelConvertible protocol - generate compile time error +public enum MigrationType { + + // provide persistent store constructor with appropriate options + case lightweight + // in case of failure old model file will be removed + case removeOnFailure + // perform progressive migration with delegate + case progressive(MigrationManagerDelegate?) + + public func isLightweight() -> Bool { + switch self { + case .lightweight: + return true + + default: + return false + } + } +} -/** - Implementation of database client for CoreData storage type. -*/ +/// Implementation of database client for CoreData storage type. public class CoreDataDBClient { - - private var modelName: String - private var bundle: Bundle - - public init(forModel modelName: String = "CoreData", in bundle: Bundle = Bundle.main) { - self.modelName = modelName - self.bundle = bundle - } - - // MARK: - CoreData stack - - fileprivate lazy var applicationDocumentsDirectory: URL = { - let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) - return urls[urls.count - 1] - }() - - fileprivate lazy var managedObjectModel: NSManagedObjectModel = { - let modelURL = self.bundle.url(/service/forresource: self.modelName, withExtension: "momd")! - return NSManagedObjectModel(contentsOf: modelURL)! - }() - - fileprivate lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { - let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) - let url = self.applicationDocumentsDirectory.appendingPathComponent("\(self.modelName).sqlite") - do { - try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: url, options: nil) - } catch { - var dict = [String: AnyObject]() - dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject? - var failureReason = "There was an error creating or loading the application's saved data." - dict[NSLocalizedFailureReasonErrorKey] = failureReason as AnyObject? - - dict[NSUnderlyingErrorKey] = error as NSError - let wrappedError = NSError(domain: "com.Yalantis.DBClient", code: 9999, userInfo: dict) - print("Unresolved error \(wrappedError), \(wrappedError.userInfo)") - abort() + + private let modelName: String + private let bundle: Bundle + private let migrationType: MigrationType + private let persistentStoreType = NSSQLiteStoreType + + /// Constructor for client + /// + /// - Parameters: + /// - modelName: the name of the model + /// - bundle: the bundle which contains the model; default is main + /// - migrationType: migration type (in case it needed) for model; default is `MigrationType.lightweight` + public init(forModel modelName: String, in bundle: Bundle = Bundle.main, migrationType: MigrationType = .lightweight) { + self.modelName = modelName + self.bundle = bundle + self.migrationType = migrationType } - - return coordinator - }() - - fileprivate lazy var managedObjectContext: NSManagedObjectContext = { - let coordinator = self.persistentStoreCoordinator - var managedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) - managedObjectContext.persistentStoreCoordinator = coordinator - return managedObjectContext - }() - - fileprivate func performBackgroundTask(closure: @escaping (NSManagedObjectContext) -> Void) { - let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) - context.parent = managedObjectContext - context.perform { - closure(context) - try? self.managedObjectContext.save() - } - } - - fileprivate func fetchRequest(for entity: CoreDataModelConvertible.Type) -> NSFetchRequest { - return NSFetchRequest(entityName: entity.entityName) - } - + + // MARK: - CoreData stack + + private lazy var storeURL: URL = { + let urls = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) + let applicationDocumentsDirectory = urls[urls.count - 1] + + return applicationDocumentsDirectory.appendingPathComponent("\(self.modelName).sqlite") + }() + + private lazy var managedObjectModel: NSManagedObjectModel = { + guard let modelURL = self.bundle.url(/service/forresource: self.modelName, withExtension: "momd"), + let objectModel = NSManagedObjectModel(contentsOf: modelURL) else { + fatalError("Can't find managedObjectModel named \(self.modelName) in \(self.bundle)") + } + + return objectModel + }() + + private lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { + let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) + + if !self.isMigrationNeeded() { + do { + try coordinator.addPersistentStore( + ofType: self.persistentStoreType, + configurationName: nil, + at: self.storeURL, + options: nil + ) + + return coordinator + } catch let error { + fatalError("\(error)") + } + } + + // need perform migration + do { + try self.performMigration(coordinator) + } catch let error { + fatalError("\(error)") + } + + return coordinator + }() + + private lazy var rootContext: NSManagedObjectContext = { + let coordinator = self.persistentStoreCoordinator + let parentContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + parentContext.persistentStoreCoordinator = coordinator + + return parentContext + }() + + fileprivate lazy var mainContext: NSManagedObjectContext = { + let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) + mainContext.parent = self.rootContext + + return mainContext + }() + + private lazy var readManagedContext: NSManagedObjectContext = { + let fetchContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + fetchContext.parent = self.mainContext + + return fetchContext + }() + + private lazy var writeManagedContext: NSManagedObjectContext = { + let fetchContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + fetchContext.parent = self.mainContext + + return fetchContext + }() + + // MARK: - Migration + + private func isMigrationNeeded() -> Bool { + do { + let metadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: persistentStoreType, + at: storeURL, + options: nil + ) + let model = self.managedObjectModel + + return !model.isConfiguration(withName: nil, compatibleWithStoreMetadata: metadata) + } catch { + return false + } + } + + private func performMigration(_ coordinator: NSPersistentStoreCoordinator) throws { + var options: [AnyHashable: Any]? + if self.migrationType.isLightweight() { + // trying to make it automatically + options = [ + NSMigratePersistentStoresAutomaticallyOption: true, + NSInferMappingModelAutomaticallyOption: true + ] + } + do { + try coordinator.addPersistentStore( + ofType: self.persistentStoreType, + configurationName: nil, + at: self.storeURL, + options: options + ) + } catch let error { + switch self.migrationType { + case .removeOnFailure: + // remove store and retry + try? FileManager.default.removeItem(at: self.storeURL) + try coordinator.addPersistentStore( + ofType: self.persistentStoreType, + configurationName: nil, + at: self.storeURL, + options: nil + ) + + case .progressive(let delegate): + try self.performHeavyMigration(coordinator, delegate: delegate) + + default: + throw error + } + } + } + + private func performHeavyMigration(_ coordinator: NSPersistentStoreCoordinator, delegate: MigrationManagerDelegate?) throws { + let manager = CoreDataMigrationManager() + manager.delegate = delegate + manager.bundle = self.bundle + try manager.progressivelyMigrate( + sourceStoreURL: self.storeURL, + of: self.persistentStoreType, + to: self.managedObjectModel + ) + + let options: [AnyHashable: Any] = [ + NSInferMappingModelAutomaticallyOption: true, + NSSQLitePragmasOption: ["journal_mode": "DELETE"] + ] + try coordinator.addPersistentStore( + ofType: self.persistentStoreType, + configurationName: nil, + at: self.storeURL, + options: options + ) + } + + // MARK: - Read/write + + private func performWriteTask(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { + let context = writeManagedContext + context.perform { + closure(context) { + try context.save(includingParent: true) + } + } + } + + private func performReadTask(closure: @escaping (NSManagedObjectContext) -> ()) { + let context = readManagedContext + context.perform { + closure(context) + } + } + + private func performWriteTaskAndWait(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { + let context = writeManagedContext + context.performAndWait { + closure(context) { + try context.save(includingParent: true) + } + } + } + + private func performReadTaskAndWait(closure: @escaping (NSManagedObjectContext) -> ()) { + let context = readManagedContext + context.performAndWait { + closure(context) + } + } + } // MARK: - DBClient methods extension CoreDataDBClient: DBClient { - - public func execute(_ request: FetchRequest) -> Task<[T]> { - guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { - fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") - } - - let taskCompletionSource = TaskCompletionSource<[T]>() - - performBackgroundTask { context in - let fetchRequest = self.fetchRequest(for: coreDataModelType) - fetchRequest.predicate = request.predicate - fetchRequest.sortDescriptors = [request.sortDescriptor].flatMap { $0 } - fetchRequest.fetchLimit = request.fetchLimit - fetchRequest.fetchOffset = request.fetchOffset - do { - let result = try context.fetch(fetchRequest) as! [NSManagedObject] - let resultModels = result.flatMap { coreDataModelType.from($0) as? T } - taskCompletionSource.set(result: resultModels) - } catch let error { - taskCompletionSource.set(error: error) - } + + public func observable(for request: FetchRequest) -> RequestObservable { + return CoreDataObservable(request: request, context: mainContext) } + + public func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) where T: Stored { + let coreDataModelType = checkType(T.self) + + performReadTask { context in + let fetchRequest = self.fetchRequest(for: coreDataModelType) + fetchRequest.predicate = request.predicate + fetchRequest.sortDescriptors = request.sortDescriptors + fetchRequest.fetchLimit = request.fetchLimit + fetchRequest.fetchOffset = request.fetchOffset + do { + let result = try context.fetch(fetchRequest) as! [NSManagedObject] + let resultModels = result.compactMap { coreDataModelType.from($0) as? T } + + completion(.success(resultModels)) + } catch let error { + completion(.failure(error)) + } + } + } + + /// Insert given objects into context and save it + /// If appropriate object already exists in DB it will be ignored and nothing will be inserted + public func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { + checkType(T.self) + + performWriteTask { context, savingClosure in + var insertedObjects = [T]() + let foundObjects = self.find(objects: objects, in: context) + for (object, storedObject) in foundObjects { + if storedObject != nil { + continue + } + + _ = object.upsertManagedObject(in: context, existedInstance: nil) + insertedObjects.append(object as! T) + } + + do { + try savingClosure() + completion(.success(insertedObjects)) + } catch let error { + completion(.failure(error)) + } + } + } + + /// Method to update existed in DB objects + /// if there is no such object in db nothing will happened + public func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { + checkType(T.self) + + performWriteTask { context, savingClosure in + var updatedObjects = [T]() + + let foundObjects = self.find(objects: objects, in: context) + for (object, storedObject) in foundObjects { + guard let storedObject = storedObject else { + continue + } + + _ = object.upsertManagedObject(in: context, existedInstance: storedObject) + updatedObjects.append(object as! T) + } + + do { + try savingClosure() + completion(.success(updatedObjects)) + } catch let error { + completion(.failure(error)) + } + } + } + + /// Update object if it exists or insert new one otherwise + public func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) where T: Stored { + checkType(T.self) + + performWriteTask { context, savingClosure in + var updatedObjects = [T]() + var insertedObjects = [T]() + let foundObjects = self.find(objects: objects, in: context) + + for (object, storedObject) in foundObjects { + _ = object.upsertManagedObject(in: context, existedInstance: storedObject) + if storedObject == nil { + insertedObjects.append(object as! T) + } else { + updatedObjects.append(object as! T) + } + } + + do { + try savingClosure() + completion(.success((updated: updatedObjects, inserted: insertedObjects))) + } catch let error { + completion(.failure(error)) + } + } + } + + /// For each element in collection: + /// After all deletes try to save context + public func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) where T: Stored { + checkType(T.self) + + performWriteTask { context, savingClosure in + let foundObjects = self.find(objects, in: context) + foundObjects.forEach { context.delete($0) } + + do { + try savingClosure() + completion(.success(())) + } catch let error { + completion(.failure(error)) + } + } + } + + public func execute(_ request: FetchRequest) -> Result<[T]> { + let coreDataModelType = checkType(T.self) + + var executeResult: Result<[T]>! + + performReadTaskAndWait { context in + let fetchRequest = self.fetchRequest(for: coreDataModelType) + fetchRequest.predicate = request.predicate + fetchRequest.sortDescriptors = request.sortDescriptors + fetchRequest.fetchLimit = request.fetchLimit + fetchRequest.fetchOffset = request.fetchOffset + do { + let result = try context.fetch(fetchRequest) as! [NSManagedObject] + let resultModels = result.compactMap { coreDataModelType.from($0) as? T } + + executeResult = .success(resultModels) + } catch let error { + executeResult = .failure(error) + } + } + + return executeResult + } + + @discardableResult + public func insert(_ objects: [T]) -> Result<[T]> { + checkType(T.self) + + var result: Result<[T]>! + + performWriteTaskAndWait { context, savingClosure in + var insertedObjects = [T]() + let foundObjects = self.find(objects: objects, in: context) + for (object, storedObject) in foundObjects { + if storedObject != nil { + continue + } + + _ = object.upsertManagedObject(in: context, existedInstance: nil) + insertedObjects.append(object as! T) + } + + do { + try savingClosure() + result = .success(insertedObjects) + } catch let error { + result = .failure(error) + } + } + + return result + } + + @discardableResult + public func update(_ objects: [T]) -> Result<[T]> { + checkType(T.self) + + var result: Result<[T]>! + + performWriteTaskAndWait { context, savingClosure in + var updatedObjects = [T]() + + let foundObjects = self.find(objects: objects, in: context) + for (object, storedObject) in foundObjects { + guard let storedObject = storedObject else { + continue + } + + _ = object.upsertManagedObject(in: context, existedInstance: storedObject) + updatedObjects.append(object as! T) + } + + do { + try savingClosure() + result = .success(updatedObjects) + } catch let error { + result = .failure(error) + } + } + + return result + } + + @discardableResult + public func delete(_ objects: [T]) -> Result<()> { + checkType(T.self) + + var result: Result<()>! + + performWriteTaskAndWait { context, savingClosure in + let foundObjects = self.find(objects, in: context) + foundObjects.forEach { context.delete($0) } + + do { + try savingClosure() + result = .success(()) + } catch let error { + result = .failure(error) + } + } + + return result + } + + public func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) where T : Stored { + let type = checkType(T.self) + + let fetchRequest = NSFetchRequest(entityName: type.entityName) + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + deleteRequest.resultType = .resultTypeObjectIDs + performWriteTask { [weak mainContext] context, savingClosure in + do { + let result = try context.execute(deleteRequest) as? NSBatchDeleteResult + if let objectIDs = result?.result as? [NSManagedObjectID] { + for objectID in objectIDs { + guard let object = mainContext?.object(with: objectID) else { continue } + mainContext?.delete(object) + } + } + try savingClosure() + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } + + @discardableResult + public func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> { + checkType(T.self) + + var result: Result<(updated: [T], inserted: [T])>! + + performWriteTaskAndWait { context, savingClosure in + var updatedObjects = [T]() + var insertedObjects = [T]() + let foundObjects = self.find(objects: objects, in: context) + + for (object, storedObject) in foundObjects { + _ = object.upsertManagedObject(in: context, existedInstance: storedObject) + if storedObject == nil { + insertedObjects.append(object as! T) + } else { + updatedObjects.append(object as! T) + } + } + + do { + try savingClosure() + result = .success((updated: updatedObjects, inserted: insertedObjects)) + } catch let error { + result = .failure(error) + } + } + + return result + } +} - return taskCompletionSource.task - } - - public func observable(for request: FetchRequest) -> RequestObservable { - return CoreDataObservable(request: request, context: managedObjectContext) - } - - public func save(_ objects: [T]) -> Task<[T]> { - // For each element in collection: - // 1. Cast T object to CoreDataModelConvertible if it is possible - // 2. Convert CoreDataModelConvertible object to CoreData object in given context - // After all inserts/updates try to save context - - let taskCompletionSource = TaskCompletionSource<[T]>() - performBackgroundTask { context in - for object in objects { - if let coreDataConvertibleObject = object as? CoreDataModelConvertible { - let _ = coreDataConvertibleObject.toManagedObject(in: context) - } - } - do { - try context.save() - taskCompletionSource.set(result: objects) - } catch let error { - taskCompletionSource.set(error: error) - } - } - return taskCompletionSource.task - } - - public func update(_ objects: [T]) -> Task<[T]> { - // For each element in collection: - // 1. Cast T object to CoreDataModelConvertible if it is possible - // 2. Convert CoreDataModelConvertible object to CoreData object in given context - // After all inserts/updates try to save context - - // The same logic as for Save actions - return save(objects) - } - - public func delete(_ objects: [T]) -> Task<[T]> { - // For each element in collection: - // 1. Cast T object to CoreDataModelConvertible if it is possible - // 2. Convert CoreDataModelConvertible object to CoreData object in given context - // 3. Delete CoreData object from context - // After all deletes try to save context - - let taskCompletionSource = TaskCompletionSource<[T]>() - performBackgroundTask { context in - for object in objects { - if let coreDataConvertibleObject = object as? CoreDataModelConvertible { - let coreDataObject = coreDataConvertibleObject.toManagedObject(in: context) - context.delete(coreDataObject) - } - } - do { - try context.save() - taskCompletionSource.set(result: objects) - } catch let error { - taskCompletionSource.set(error: error) - } - } - return taskCompletionSource.task - } - +private extension CoreDataDBClient { + + func fetchRequest(for entity: CoreDataModelConvertible.Type) -> NSFetchRequest { + return NSFetchRequest(entityName: entity.entityName) + } + + @discardableResult + func checkType(_ inputType: T) -> CoreDataModelConvertible.Type { + switch inputType { + case let type as CoreDataModelConvertible.Type: + return type + + default: + let modelType = String(describing: CoreDataDBClient.self) + let protocolType = String(describing: CoreDataModelConvertible.self) + let givenType = String(describing: inputType) + fatalError("`\(modelType)` can manage only types which conform to `\(protocolType)`. `\(givenType)` given.") + } + } + + func find(_ objects: [T], in context: NSManagedObjectContext) -> [NSManagedObject] { + let coreDataModelType = checkType(T.self) + guard let primaryKeyName = T.primaryKeyName else { + return [] + } + + let ids = objects.compactMap { $0.valueOfPrimaryKey } + let fetchRequest = self.fetchRequest(for: coreDataModelType) + fetchRequest.predicate = NSPredicate(format: "\(primaryKeyName) IN %@", ids) + guard let result = try? context.fetch(fetchRequest), let storedObjects = result as? [NSManagedObject] else { + return [] + } + + return storedObjects + } + + func find(objects: [T], in context: NSManagedObjectContext) -> [(object: CoreDataModelConvertible, storedObject: NSManagedObject?)] { + guard let primaryKeyName = T.primaryKeyName else { + return [] + } + + let storedObjects = find(objects, in: context) + + return convert(objects: objects).map { object -> (CoreDataModelConvertible, NSManagedObject?) in + let managedObject = storedObjects.first(where: { (obj: NSManagedObject) -> Bool in + if let value = obj.value(forKey: primaryKeyName) { + return object.isPrimaryValueEqualTo(value: value) + } + + return false + }) + + return (object, managedObject) + } + } + + func convert(objects: [T]) -> [CoreDataModelConvertible] { + checkType(T.self) + + return objects.compactMap { $0 as? CoreDataModelConvertible } + } } diff --git a/DBClient/CoreData/CoreDataMigrationManager.swift b/DBClient/CoreData/CoreDataMigrationManager.swift new file mode 100644 index 0000000..fa3db0e --- /dev/null +++ b/DBClient/CoreData/CoreDataMigrationManager.swift @@ -0,0 +1,151 @@ +// +// CoreDataMigrationManager.swift +// DBClient +// +// Created by Roman Kyrylenko on 2/17/17. +// Copyright © 2016 Yalantis. All rights reserved. +// + +import CoreData +import Foundation + +final class CoreDataMigrationManager: NSObject, MigrationManager { + + weak var delegate: MigrationManagerDelegate? = nil + var bundle: Bundle = .main + + func progressivelyMigrate(sourceStoreURL: URL, of type: String, to finalModel: NSManagedObjectModel) throws { + let sourceMetadata = try NSPersistentStoreCoordinator.metadataForPersistentStore( + ofType: type, + at: sourceStoreURL, + options: nil + ) + if finalModel.isConfiguration(withName: nil, compatibleWithStoreMetadata: sourceMetadata) { + return + } + guard let sourceModel = self.sourceModel(for: sourceMetadata) else { + throw MigrationError.modelsNotFound + } + + let data = try getDestinationModel(for: sourceModel) + let destinationModel = data.0 + let mappingModel = data.1 + let modelName = data.2 + let mappingModels: [NSMappingModel] + if let explicitMappingModels = delegate?.migrationManager(self, mappingModelsForSourceModel: sourceModel), + !explicitMappingModels.isEmpty { + mappingModels = explicitMappingModels + } else { + mappingModels = [mappingModel] + } + let destinationStoreURL = self.destinationStoreURL(with: sourceStoreURL, modelName: modelName) + let manager = NSMigrationManager(sourceModel: sourceModel, destinationModel: destinationModel) + manager.addObserver(self, forKeyPath: #keyPath(NSMigrationManager.migrationProgress), options: .new, context: nil) + var migrated = false + for mappingModel in mappingModels { + do { + try manager.migrateStore( + from: sourceStoreURL, + sourceType: type, + options: nil, + with: mappingModel, + toDestinationURL: destinationStoreURL, + destinationType: type, + destinationOptions: nil + ) + migrated = true + } catch { + migrated = false + } + } + manager.removeObserver(self, forKeyPath: #keyPath(NSMigrationManager.migrationProgress)) + + if !migrated { + return + } + // Migration was successful, move the files around to preserve the source in case things go bad + try backup(sourceStoreAtURL: sourceStoreURL, movingDestinationStoreAtURL: destinationStoreURL) + // We may not be at the "current" model yet, so recurse + try self.progressivelyMigrate(sourceStoreURL: sourceStoreURL, of: type, to: finalModel) + } + + func modelPaths() -> [String] { + // Find all of the mom and momd files in the Resources directory + var modelPaths: [String] = [] + let momdArray = bundle.paths(forResourcesOfType: "momd", inDirectory: nil) + for path in momdArray { + let resourceSubpath = (path as NSString).lastPathComponent + let array = bundle.paths(forResourcesOfType: "mom", inDirectory: resourceSubpath) + modelPaths.append(contentsOf: array) + } + let otherModels = bundle.paths(forResourcesOfType: "mom", inDirectory: nil) + modelPaths.append(contentsOf: otherModels) + + return modelPaths + } + + func sourceModel(for sourceMetadata: [String: Any]) -> NSManagedObjectModel? { + return NSManagedObjectModel.mergedModel(from: [bundle], forStoreMetadata: sourceMetadata) + } + + func getDestinationModel(for sourceModel: NSManagedObjectModel) throws -> (NSManagedObjectModel, NSMappingModel, String) { + let modelPaths = self.modelPaths() + if modelPaths.isEmpty { + throw MigrationError.modelsNotFound + } + // See if we can find a matching destination model + var model: NSManagedObjectModel? = nil + var mapping: NSMappingModel? = nil + var modelURL: URL? = nil + for modelPath in modelPaths { + let mURL = URL(fileURLWithPath: modelPath) + modelURL = mURL + model = NSManagedObjectModel(contentsOf: mURL) + mapping = NSMappingModel(from: [bundle], forSourceModel: sourceModel, destinationModel: model) + // If we found a mapping model then proceed + if mapping != nil { + break + } + } + // We have tested every model, if nil here we failed + if mapping == nil || mapping == nil || modelURL == nil { + throw MigrationError.mappingModelNotFound + } + + return (model!, mapping!, (modelURL!.lastPathComponent as NSString).deletingPathExtension) + } + + func destinationStoreURL(with sourceStoreURL: URL, modelName: String) -> URL { + // We have a mapping model, time to migrate + let storeExtension = sourceStoreURL.pathExtension + var storePath = sourceStoreURL.deletingPathExtension().path + // Build a path to write the new store + storePath = "\(storePath).\(modelName).\(storeExtension)" + + return URL(fileURLWithPath: storePath) + } + + func backup(sourceStoreAtURL: URL, movingDestinationStoreAtURL: URL) throws { + let guid = ProcessInfo.processInfo.globallyUniqueString + let backupPath = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true).appendingPathComponent(guid) + let fileManager = FileManager.default + try fileManager.moveItem(at: sourceStoreAtURL, to: backupPath) + // Move the destination to the source path + do { + try fileManager.moveItem(at: movingDestinationStoreAtURL, to: sourceStoreAtURL) + } catch { + // Try to back out the source move first, no point in checking it for errors + try fileManager.moveItem(at: backupPath, to: sourceStoreAtURL) + throw error + } + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "migrationProgress", let object = object as? NSMigrationManager { + delegate?.migrationManager(self, updateMigrationProgress: object.migrationProgress) + } else { + super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) + } + } + +} diff --git a/DBClient/CoreData/CoreDataObservable.swift b/DBClient/CoreData/CoreDataObservable.swift index 2b1ff58..d442047 100644 --- a/DBClient/CoreData/CoreDataObservable.swift +++ b/DBClient/CoreData/CoreDataObservable.swift @@ -1,6 +1,6 @@ // // CoreDataObservable.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 15/12/16. // Copyright © 2016 Yalantis. All rights reserved. @@ -10,110 +10,133 @@ import Foundation import CoreData class CoreDataObservable: RequestObservable { - - var observer: ((ObservableChange) -> Void)? - - let fetchRequest: NSFetchRequest - let fetchedResultsController: NSFetchedResultsController - - private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate - - init(request: FetchRequest, context: NSManagedObjectContext) { - guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { - fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") - } - fetchRequest = { - let fetchRequest = NSFetchRequest(entityName: coreDataModelType.entityName) - if let predicate = request.predicate { - fetchRequest.predicate = predicate - } - if let sortDescriptor = request.sortDescriptor { - fetchRequest.sortDescriptors = [sortDescriptor] - } else { - let defaultSortDescriptor = NSSortDescriptor(key: coreDataModelType.primaryKey, ascending: true) - fetchRequest.sortDescriptors = [defaultSortDescriptor] - } - fetchRequest.fetchLimit = request.fetchLimit - fetchRequest.fetchOffset = request.fetchOffset - - return fetchRequest - }() + var observer: ((ObservableChange) -> Void)? - fetchedResultsControllerDelegate = FetchedResultsControllerDelegate() - - fetchedResultsController = NSFetchedResultsController( - fetchRequest: fetchRequest, - managedObjectContext: context, - sectionNameKeyPath: nil, - cacheName: nil - ) - fetchedResultsController.delegate = fetchedResultsControllerDelegate + let fetchRequest: NSFetchRequest + let fetchedResultsController: NSFetchedResultsController - super.init(request: request) - } - - override func observe(_ closure: @escaping (ObservableChange) -> Void) { - assert(observer == nil, "Observable can be observed only once") + private let fetchedResultsControllerDelegate: FetchedResultsControllerDelegate - guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { - fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") + init(request: FetchRequest, context: NSManagedObjectContext) { + guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { + fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") + } + + fetchRequest = { + let fetchRequest = NSFetchRequest(entityName: coreDataModelType.entityName) + if let predicate = request.predicate { + fetchRequest.predicate = predicate + } + if let sortDescriptors = request.sortDescriptors { + fetchRequest.sortDescriptors = sortDescriptors + } else { + guard let primaryKeyName = coreDataModelType.primaryKeyName else { + fatalError("Fetch request shoud have sortDescriptor or core data model need implement primaryKeyName") + } + let defaultSortDescriptor = NSSortDescriptor(key: primaryKeyName, ascending: true) + fetchRequest.sortDescriptors = [defaultSortDescriptor] + } + fetchRequest.fetchLimit = request.fetchLimit + fetchRequest.fetchOffset = request.fetchOffset + + return fetchRequest + }() + + fetchedResultsControllerDelegate = FetchedResultsControllerDelegate() + + fetchedResultsController = NSFetchedResultsController( + fetchRequest: fetchRequest, + managedObjectContext: context, + sectionNameKeyPath: nil, + cacheName: nil + ) + fetchedResultsController.delegate = fetchedResultsControllerDelegate + + super.init(request: request) } - do { - let initial = try fetchedResultsController.managedObjectContext.fetch(fetchRequest) - let mapped = initial.map { coreDataModelType.from($0) as! T } - closure(.initial(mapped)) - observer = closure - - fetchedResultsControllerDelegate.observer = { [unowned self] change in - if case .change(objects: let objects, deletions: let deletions, insertions: let insertions, modifications: let modifications) = change { - let mappedInsertions = insertions.map { ($0, coreDataModelType.from($1) as! T) } - let mappedModifications = modifications.map { ($0, coreDataModelType.from($1) as! T) } - let mappedObjects = objects.map { coreDataModelType.from($0) as! T } - self.observer?(.change(objects: mappedObjects, deletions: deletions, insertions: mappedInsertions, modifications: mappedModifications)) + override func observe(_ closure: @escaping (ObservableChange) -> Void) { + assert(observer == nil, "Observable can be observed only once") + + guard let coreDataModelType = T.self as? CoreDataModelConvertible.Type else { + fatalError("CoreDataDBClient can manage only types which conform to CoreDataModelConvertible") + } + + do { + let initial = try fetchedResultsController.managedObjectContext.fetch(fetchRequest) + let mapped = initial.map { coreDataModelType.from($0) as! T } + closure(.initial(mapped)) + observer = closure + + fetchedResultsControllerDelegate.observer = { [unowned self] (change: ObservableChange) in + guard case .change(let change) = change else { return } + let mappedInsertions = change.insertions.map { ($0, coreDataModelType.from($1) as! T) } + let mappedModifications = change.modifications.map { ($0, coreDataModelType.from($1) as! T) } + let mappedObjects = change.objects.map { coreDataModelType.from($0) as! T } + let mappedChange: ObservableChange.ModelChange = ( + objects: mappedObjects, + deletions: change.deletions, + insertions: mappedInsertions, + modifications: mappedModifications + ) + self.observer?(.change(mappedChange)) + } + + try fetchedResultsController.performFetch() + } catch let error { + closure(.error(error)) } - } - - try fetchedResultsController.performFetch() - } catch let error { - closure(.error(error)) } - } - + } /// A separate class to avoid inherintace from NSObject private class FetchedResultsControllerDelegate: NSObject, NSFetchedResultsControllerDelegate { - - var observer: ((ObservableChange) -> Void)? - private var batchChanges: [CoreDataChange] = [] - - func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { - guard let object = anObject as? T else { return } - switch type { - case .delete: - batchChanges.append(.delete(indexPath!.row, object)) - case .insert: - batchChanges.append(.insert(newIndexPath!.row, object)) - case .update: - batchChanges.append(.update(indexPath!.row, object)) - default: break + var observer: ((ObservableChange) -> Void)? + private var batchChanges: [CoreDataChange] = [] + + func controller(_ controller: NSFetchedResultsController, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) { + guard let object = anObject as? T else { + return + } + + switch type { + case .delete: + batchChanges.append(.delete(indexPath!.row, object)) + + case .insert: + batchChanges.append(.insert(newIndexPath!.row, object)) + + case .update, .move: + batchChanges.append(.update(indexPath!.row, object)) + + @unknown + default: + assertionFailure("trying to handle unknown case \(type)") + } + } + + func controllerWillChangeContent(_ controller: NSFetchedResultsController) { + batchChanges = [] + } + + func controllerDidChangeContent(_ controller: NSFetchedResultsController) { + let deleted: [Int] = batchChanges.filter { $0.isDeletion }.map { $0.index() } + let inserted: [(index: Int, element: T)] = batchChanges.filter { $0.isInsertion }.map { (index: $0.index(), element: $0.object()) } + let updated: [(index: Int, element: T)] = batchChanges.filter { $0.isUpdate }.map { (index: $0.index(), element: $0.object()) } + let objects: [T] = controller.fetchedObjects as? [T] ?? [] + let mappedChange: ObservableChange.ModelChange = ( + objects: objects, + deletions: deleted, + insertions: inserted, + modifications: updated + ) + if let observer = observer { + observer(.change(mappedChange)) + } + batchChanges = [] } - } - - func controllerWillChangeContent(_ controller: NSFetchedResultsController) { - batchChanges = [] - } - - func controllerDidChangeContent(_ controller: NSFetchedResultsController) { - let deleted = batchChanges.filter { $0.isDeletion }.map { $0.index() } - let inserted = batchChanges.filter { $0.isInsertion }.map { (index: $0.index(), element: $0.object()) } - let updated = batchChanges.filter { $0.isUpdate }.map { (index: $0.index(), element: $0.object()) } - observer?(.change(objects: controller.fetchedObjects as? [T] ?? [], deletions: deleted, insertions: inserted, modifications: updated)) - batchChanges = [] - } - } diff --git a/DBClient/CoreData/MigrationManager.swift b/DBClient/CoreData/MigrationManager.swift new file mode 100644 index 0000000..bb9f978 --- /dev/null +++ b/DBClient/CoreData/MigrationManager.swift @@ -0,0 +1,41 @@ +// +// MigrationManager.swift +// DBClient +// +// Created by Roman Kyrylenko on 2/17/17. +// Copyright © 2016 Yalantis. All rights reserved. +// + +import CoreData + +public protocol MigrationManagerDelegate: class { + + func migrationManager(_ migrationManager: MigrationManager, updateMigrationProgress: Float) + func migrationManager(_ migrationManager: MigrationManager, mappingModelsForSourceModel: NSManagedObjectModel) -> [NSMappingModel] + +} + +public extension MigrationManagerDelegate { + + func migrationManager(_ migrationManager: MigrationManager, updateMigrationProgress: Float) { + } + + func migrationManager(_ migrationManager: MigrationManager, mappingModelsForSourceModel: NSManagedObjectModel) -> [NSMappingModel] { + return [] + } + +} + +public enum MigrationError: Error { + case modelsNotFound + case mappingModelNotFound +} + +public protocol MigrationManager { + + var delegate: MigrationManagerDelegate? { get set } + var bundle: Bundle { get set } + + func progressivelyMigrate(sourceStoreURL: URL, of type: String, to model: NSManagedObjectModel) throws + +} diff --git a/DBClient/CoreData/NSManagedObjectContext+Extension.swift b/DBClient/CoreData/NSManagedObjectContext+Extension.swift new file mode 100644 index 0000000..442fa85 --- /dev/null +++ b/DBClient/CoreData/NSManagedObjectContext+Extension.swift @@ -0,0 +1,44 @@ +// +// NSManagedObjectContext+Extension.swift +// DBClient +// +// Copyright © 2016 Yalantis. All rights reserved. +// + +import Foundation +import CoreData + +extension NSManagedObjectContext { + + func save(includingParent: Bool) throws { + guard hasChanges else { + return + } + + try save() + + if includingParent, let parent = parent { + try parent.safePerformAndWait { + try parent.save(includingParent: true) + } + } + } + + func safePerformAndWait(_ block: @escaping () throws -> Void) throws { + var outError: Error? + + performAndWait { + do { + try block() + } catch { + outError = error + } + } + + // fake rethrowing + if let outError = outError { + throw outError + } + } + +} diff --git a/DBClient/Realm/RealmDBClient.swift b/DBClient/Realm/RealmDBClient.swift index 65f93d3..aa80718 100644 --- a/DBClient/Realm/RealmDBClient.swift +++ b/DBClient/Realm/RealmDBClient.swift @@ -1,150 +1,262 @@ // // RealmDBClient.swift -// ArchitectureGuideTemplate +// DBClient // // Created by Serhii Butenko on 19/12/16. // Copyright © 2016 Yalantis. All rights reserved. // import Foundation -import BoltsSwift import RealmSwift -/** - Describes protocol to be implemented by model for `RealmDBClient` -*/ +/// Describes protocol to be implemented by model for `RealmDBClient` public protocol RealmModelConvertible: Stored { + + /// - Returns: type of object for model + static func realmClass() -> Object.Type + + /// Executes mapping from `Realm.Object` instance + /// + /// - Parameter realmObject: Object to be mapped from + /// - Returns: Fulfilled model instance + static func from(_ realmObject: Object) -> Stored + + /// Executes backward mapping from `Realm.Object` + func toRealmObject() -> Object +} - /** - Returns type of object for model - */ - static func realmClass() -> Object.Type - - /** - Executes mapping from `Realm.Object` instance - - - Parameter realmObject: Object to be mapped from - - - Returns: Fulfilled model instance - */ - static func from(_ realmObject: Object) -> Stored - - /** - Executes backward mapping from `Realm.Object` - */ - func toRealmObject() -> Object - +extension RealmModelConvertible { + + func realmClassForInstance() -> Object.Type { + return Self.realmClass() + } } -/** - Implementation of database client for Realm storage type. - Model for this client must conform `RealmModelConverible` protocol or error will be raised. -*/ +/// Implementation of database client for Realm storage type. +/// Model for this client must conform to `RealmModelConverible` protocol or error will be raised. public class RealmDBClient { - - let realm: Realm - - public init(realm: Realm) { - self.realm = realm - } - + + let realm: Realm + + public init(realm: Realm) { + self.realm = realm + } } +// MARK: DBClient + extension RealmDBClient: DBClient { - - public func save(_ objects: [T]) -> Task<[T]> { - let taskCompletionSource = TaskCompletionSource<[T]>() - - let realmObjects = objects.flatMap { $0 as? RealmModelConvertible }.map { $0.toRealmObject() } - do { - try realm.write { - realm.add(realmObjects, update: true) - } - taskCompletionSource.set(result: objects) - } catch let error { - taskCompletionSource.set(error: error) - } - - return taskCompletionSource.task - } - - public func update(_ objects: [T]) -> Task<[T]> { - return save(objects) - } - - public func delete(_ objects: [T]) -> Task<[T]> { - let taskCompletionSource = TaskCompletionSource<[T]>() - - do { - let realmObjects = objects.flatMap { $0 as? RealmModelConvertible }.map { $0.toRealmObject() } - try realm.write { - realm.delete(realmObjects) - taskCompletionSource.set(result: objects) - } - } catch let error { - taskCompletionSource.set(error: error) - } - - return taskCompletionSource.task - } - - public func execute(_ request: FetchRequest) -> Task<[T]> { - guard let modelType = T.self as? RealmModelConvertible.Type else { - fatalError("RealmDBClient can manage only types which conform to RealmModelConvertible") - } - let taskCompletionSource = TaskCompletionSource<[T]>() - let neededType = modelType.realmClass() - do { - var objects = realm - .objects(neededType) - .get(offset: request.fetchOffset, limit: request.fetchLimit) - if let descriptor = request.sortDescriptor { - let order: ComparisonResult = descriptor.ascending ? .orderedAscending : .orderedDescending - objects = objects.sorted(by: { (lhs, rhs) -> Bool in - return descriptor.compare(lhs, to: rhs) == order - }) - } - if let predicate = request.predicate { - objects = objects.filter { predicate.evaluate(with: $0) } - } - let mappedObjects = objects.flatMap { modelType.from($0) as? T } - taskCompletionSource.set(result: mappedObjects) - } catch let error { - taskCompletionSource.set(error: error) + + /// Executes given request. Fetches all entities and then applies all given restrictions + public func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) { + completion(execute(request)) + } + + /// Inserts new objects to database. If object with such `primaryKeyValue` already exists Realm'll throw an error + public func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T : Stored { + completion(insert(objects)) + } + + /// Updates objects which are already in db. + public func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T : Stored { + completion(update(objects)) + } + + /// Removes objects by it `primaryKeyValue`s + public func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) where T : Stored { + completion(delete(objects)) + } + + public func deleteAllObjects(of type: T.Type, completion: @escaping (Result<()>) -> Void) where T: Stored { + let type = checkType(T.self) + + let realmType = type.realmClass() + + do { + let realmObjects = realm.objects(realmType) + realm.beginWrite() + realm.delete(realmObjects) + try realm.commitWrite() + + completion(.success(())) + } catch { + completion(.failure(error)) + } } - return taskCompletionSource.task - } - - public func observable(for request: FetchRequest) -> RequestObservable{ - return RealmObservable(request: request, realm: realm) - } - + public func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) where T : Stored { + completion(upsert(objects)) + } + + public func observable(for request: FetchRequest) -> RequestObservable { + checkType(T.self) + + return RealmObservable(request: request, realm: realm) + } + + public func execute(_ request: FetchRequest) -> Result<[T]> { + let modelType = checkType(T.self) + let neededType = modelType.realmClass() + let objects = request + .applyTo(realmObjects: realm.objects(neededType)) + .map { $0 } + .slice(offset: request.fetchOffset, limit: request.fetchLimit) + .compactMap { modelType.from($0) as? T } + + return .success(objects) + } + + @discardableResult + public func insert(_ objects: [T]) -> Result<[T]> { + checkType(T.self) + + let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } + + do { + realm.beginWrite() + realm.add(realmObjects) + try realm.commitWrite() + return .success(objects) + } catch { + return .failure(error) + } + } + + @discardableResult + public func update(_ objects: [T]) -> Result<[T]> { + checkType(T.self) + + let realmObjects = separate(objects: objects) + .present + .compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } + do { + realm.beginWrite() + realm.add(realmObjects, update: true) + try realm.commitWrite() + + return .success(objects) + } catch let error { + return .failure(error) + } + } + + @discardableResult + public func delete(_ objects: [T]) -> Result<()> { + let type = checkType(T.self) + + let realmType = type.realmClass() + + do { + let primaryValues = objects.compactMap { $0.valueOfPrimaryKey } + let realmObjects = primaryValues.compactMap { realm.object(ofType: realmType, forPrimaryKey: $0) } + realm.beginWrite() + realm.delete(realmObjects) + try realm.commitWrite() + + return .success(()) + } catch { + return .failure(error) + } + } + + @discardableResult + public func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> { + checkType(T.self) + + let separatedObjects = separate(objects: objects) + let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } + do { + realm.beginWrite() + realm.add(realmObjects, update: true) + try realm.commitWrite() + return .success((updated: separatedObjects.present, inserted: separatedObjects.new)) + } catch { + return .failure(error) + } + } + } -extension Results { - - func get(offset: Int, limit: Int) -> [T] { - var lim = 0 - var off = 0 - var l: [T] = [] - let count = self.count - - if off <= offset && offset < count - 1 { - off = offset +private extension RealmDBClient { + + @discardableResult + func checkType(_ inputType: T) -> RealmModelConvertible.Type { + switch inputType { + case let type as RealmModelConvertible.Type: + return type + + default: + let model = String(describing: RealmDBClient.self) + let prot = String(describing: RealmModelConvertible.self) + let given = String(describing: inputType) + fatalError("`\(model)` can manage only types which conform to `\(prot)`. `\(given)` given.") + } + } + + func separate(objects: [T]) -> (present: [T], new: [T]) { + var presentObjects: [T] = [] + var notPresentObjects: [T] = [] + objects.forEach { object in + guard let convertedObject = object as? RealmModelConvertible, + let primaryValue = convertedObject.valueOfPrimaryKey else { + return + } + + let entry = self.realm.object(ofType: convertedObject.realmClassForInstance(), forPrimaryKey: primaryValue) + if entry != nil { + presentObjects.append(object) + } else { + notPresentObjects.append(object) + } + } + + return (present: presentObjects, new: notPresentObjects) } - if limit > count || limit == 0 { - lim = count - } else { - lim = limit +} + +internal extension FetchRequest { + + func applyTo(realmObjects: Results) -> Results { + var objects: Results = realmObjects + if let sortDescriptors = sortDescriptors?.compactMap(SortDescriptor.init), !sortDescriptors.isEmpty { + objects = realmObjects.sorted(by: sortDescriptors) + } + if let predicate = predicate { + objects = objects.filter(predicate) + } + + return objects } +} - for i in off..(offset: Int, limit: Int) -> [T] { + var lim = 0 + var off = 0 + let count = self.count + + if off <= offset && offset < count - 1 { + off = offset + } + if limit > count || limit == 0 { + lim = count + } else { + lim = offset + limit + } + + return (off..: RequestObservable { - - internal let realm: Realm - internal var notificationToken: NotificationToken? - - internal init(request: FetchRequest, realm: Realm) { - self.realm = realm - super.init(request: request) - } - - open override func observe(_ closure: @escaping (ObservableChange) -> Void) { - precondition(notificationToken == nil, "Observable can be observed only once") + public static var primaryKeyName: String? { return nil } - guard let realmModelType = T.self as? RealmModelConvertible.Type else { - fatalError("RealmDBClient can manage only types which conform to RealmModelConvertible") - } + public var valueOfPrimaryKey: CVarArg? { return nil } + +} + +internal class RealmObservable: RequestObservable { - var realmObjects = realm.objects(realmModelType.realmClass()) - if let predicate = request.predicate { - realmObjects = realmObjects.filter(predicate) - } - if let sortDescriptor = request.sortDescriptor, let key = sortDescriptor.key { - realmObjects = realmObjects.sorted(byProperty: key, ascending: sortDescriptor.ascending) + internal let realm: Realm + internal var notificationToken: NotificationToken? + + internal init(request: FetchRequest, realm: Realm) { + self.realm = realm + super.init(request: request) } - notificationToken = realmObjects.addNotificationBlock { changes in - switch changes { - case .initial(let initial): - let mapped = initial.map { realmModelType.from($0) as! T } - closure(.initial(Array(mapped))) - - case .update(let objects, let deletions, let insertions, let modifications): - let mappedObjects = objects.map { realmModelType.from($0) as! T } - let insertions = insertions.map { (index: $0, element: mappedObjects[$0]) } - let modifications = modifications.map { (index: $0, element: mappedObjects[$0]) } - closure(.change(objects: Array(mappedObjects), deletions: deletions, insertions: insertions, modifications: modifications)) + open override func observe(_ closure: @escaping (ObservableChange) -> Void) { + precondition(notificationToken == nil, "Observable can be observed only once") + + guard let realmModelType = T.self as? RealmModelConvertible.Type else { + fatalError("RealmDBClient can manage only types which conform to RealmModelConvertible") + } - case .error(let error): - closure(.error(error)) - } + let realmObjects = request.applyTo(realmObjects: realm.objects(realmModelType.realmClass())) + notificationToken = realmObjects.observe { changes in + switch changes { + case .initial(let initial): + let mapped = initial.map { realmModelType.from($0) as! T } + closure(.initial(Array(mapped))) + + case .update(let objects, let deletions, let insertions, let modifications): + let mappedObjects = objects.map { realmModelType.from($0) as! T } + let insertions = insertions.map { (index: $0, element: mappedObjects[$0]) } + let modifications = modifications.map { (index: $0, element: mappedObjects[$0]) } + let mappedChange: ObservableChange.ModelChange = ( + objects: Array(mappedObjects), + deletions: deletions, + insertions: insertions, + modifications: modifications + ) + closure(.change(mappedChange)) + + case .error(let error): + closure(.error(error)) + } + } + } + + public func stopObserving() { + notificationToken = nil } - } - - public func stopObserving() { - notificationToken = nil - } - } diff --git a/Example/DBClientTests/DBClientTest.swift b/Example/DBClientTests/DBClientTest.swift new file mode 100644 index 0000000..2bbd894 --- /dev/null +++ b/Example/DBClientTests/DBClientTest.swift @@ -0,0 +1,50 @@ +// +// DBClientTest.swift +// DBClientTests +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +import DBClient +@testable import Example + +class DBClientTest: XCTestCase { + + var dbClient: DBClient! { return nil } + + override func setUp() { + super.setUp() + + cleanUpDatabase() + } + + override func tearDown() { + cleanUpDatabase() + + super.tearDown() + } + + // removes all objects from the database + func cleanUpDatabase() { + guard dbClient != nil else { return } + let expectationDeleletion = expectation(description: "Deletion") + var isDeleted = false + + dbClient.findAll { (result: Result<[User]>) in + guard let objects = result.value else { + expectationDeleletion.fulfill() + return + } + self.dbClient.delete(objects) { _ in + isDeleted = true + expectationDeleletion.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } + } +} diff --git a/Example/DBClientTests/Info.plist b/Example/DBClientTests/Info.plist new file mode 100644 index 0000000..6c6c23c --- /dev/null +++ b/Example/DBClientTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataCreateTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataCreateTests.swift new file mode 100644 index 0000000..45c21fd --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataCreateTests.swift @@ -0,0 +1,64 @@ +// +// CoreDataCreateTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class CoreDataCreateTests: DBClientCoreDataTest { + + func test_SyncSingleInsertion_WhenSuccessful_ReturnsObject() { + let randomUser = User.createRandom() + let result = dbClient.insert(randomUser) + switch result { + case .failure(let error): XCTFail(error.localizedDescription) + case .success(let user): XCTAssertEqual(randomUser, user) + } + } + + func test_SyncBulkInsertion_WhenSuccessful_ReturnsObjects() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let result = dbClient.insert(randomUsers) + + switch result { + case .failure(let error): XCTFail(error.localizedDescription) + case .success(let users): XCTAssertEqual(users.sorted(), randomUsers.sorted()) + } + } + + func test_SingleInsertion_WhenSuccessful_ReturnsObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedObject: User? + + dbClient.insert(randomUser) { result in + expectedObject = result.value + expectationObject.fulfill() + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertNotNil(expectedObject) + } + } + + func test_BulkInsertion_WhenSuccessful_ReturnsBulk() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let expectationObjects = expectation(description: "Objects") + var expectedObjectsCount = 0 + + dbClient.insert(randomUsers) { result in + expectedObjectsCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedObjectsCount, randomUsers.count) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataDeleteTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataDeleteTests.swift new file mode 100644 index 0000000..e3efd44 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataDeleteTests.swift @@ -0,0 +1,69 @@ +// +// CoreDataDeleteTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class CoreDataDeleteTests: DBClientCoreDataTest { + + func test_SyncSingleDeletion_WhenSuccessful_ReturnsNil() { + let randomUser = User.createRandom() + + let result = dbClient.insert(randomUser) + let removalResult = dbClient.delete(result.require()) + + XCTAssertNotNil(removalResult.value) + } + + func test_SyncBulkDeletion_WhenSuccessful_ReturnsNil() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let insertionResult = dbClient.insert(randomUsers) + let removalResult = dbClient.delete(insertionResult.require()) + + XCTAssertNotNil(removalResult.value) + } + + func test_SingleDeletion_WhenSuccessful_ReturnsNil() { + let randomUser = User.createRandom() + let expectationHit = expectation(description: "Object") + var isDeleted = false + + dbClient.insert(randomUser) { result in + if let object = result.value { + self.dbClient.delete(object) { result in + isDeleted = result.value != nil + expectationHit.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } + } + + func test_BulkDeletion_WhenSuccessful_ReturnsNil() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + let expectationHit = expectation(description: "Object") + var isDeleted = false + + dbClient.insert(randomUsers) { result in + if let objects = result.value { + self.dbClient.delete(objects) { result in + isDeleted = result.value != nil + expectationHit.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataExecuteTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataExecuteTests.swift new file mode 100644 index 0000000..de23225 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataExecuteTests.swift @@ -0,0 +1,138 @@ +// +// CoreDataExecuteTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +import DBClient +@testable import Example + +final class CoreDataExecuteTests: DBClientCoreDataTest { + + func test_SingleSyncExecute_WhenSuccessful_ReturnsCount() { + let randomUser = User.createRandom() + + dbClient.insert(randomUser) + let request = FetchRequest() + let executionResult = dbClient.execute(request) + + XCTAssertEqual(executionResult.require().first!, randomUser) + } + + func test_SingleExecute_WhenSuccessful_ReturnsCount() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedCount = 0 + + self.dbClient.insert(randomUser) { result in + if result.value != nil { + let request = FetchRequest() + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObject.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, 1) + } + } + + func test_ExecuteWithOffset_WhenSuccessful_ReturnsCount() { + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let offset = 5 + let shiftedUsers = Array(randomUsers[offset..(fetchOffset: offset) + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, shiftedUsers.count) + } + } + + func test_ExecuteWithLimit_WhenSuccessful_ReturnsCount() { + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let limit = 3 + + let expectationObjects = expectation(description: "Object") + var expectedCount = 0 + + self.dbClient.insert(randomUsers) { result in + if result.value != nil { + let request = FetchRequest(fetchLimit: limit) + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, limit) + } + } + + func test_ExecuteWithSortDescriptor_WhenSuccessful_ReturnsCount() { + let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) + let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let sortedUsers = randomUsers.sorted { $0.name.compare($1.name) == order } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + + self.dbClient.insert(randomUsers) { result in + if result.value != nil { + let request = FetchRequest(sortDescriptor: sortDescriptor) + + self.dbClient.execute(request) { result in + expectedUsers = result.value ?? [] + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers, sortedUsers) + } + } + + func test_ExecuteWithPredicate_WhenSuccessful_ReturnsCount() { + let arg = "1" + let predicate = NSPredicate(format: "SELF.id ENDSWITH %@", arg) + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let preicatedUsers = randomUsers.filter { $0.id.hasSuffix(arg) } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + + self.dbClient.insert(randomUsers) { result in + guard result.value != nil else { + expectationObjects.fulfill() + return + } + let request = FetchRequest(predicate: predicate) + + self.dbClient.execute(request) { result in + expectedUsers = result.value ?? [] + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers.sorted(), preicatedUsers.sorted()) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataFetchTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataFetchTests.swift new file mode 100644 index 0000000..988ec78 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataFetchTests.swift @@ -0,0 +1,64 @@ +// +// CoreDataFetchTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import Foundation +import XCTest +import DBClient +@testable import Example + +class CoreDataFetchTests: DBClientCoreDataTest { + + func test_SyncFetch_WhenSuccessful_ReturnObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Inserting object") + + self.dbClient.insert(randomUser) { _ in expectationObject.fulfill() } + + waitForExpectations(timeout: 1) { _ in + let result = self.dbClient.execute(FetchRequest(predicate: NSPredicate(format: "id == %@", randomUser.id))) + XCTAssertEqual(result.require().count, 1) + let object = result.require().first + XCTAssertEqual(object, randomUser) + } + } + + func test_SingleFetch_WhenSuccessful_ReturnsObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedObject: User? + + self.dbClient.insert(randomUser) { result in + self.dbClient.findFirst(User.self, primaryValue: randomUser.id) { result in + expectedObject = result.require() + expectationObject.fulfill() + } + } + + waitForExpectations(timeout: 5) { _ in + XCTAssertNotNil(expectedObject) + } + } + + func test_BulkFetch_WhenSuccessful_ReturnsBulk() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let expectationObjects = expectation(description: "Objects") + var expectedObjectsCount = 0 + + self.dbClient.insert(randomUsers) { result in + self.dbClient.findAll { (result: Result<[User]>) in + expectedObjectsCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedObjectsCount, randomUsers.count) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataObservableTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataObservableTests.swift new file mode 100644 index 0000000..8dec9b0 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataObservableTests.swift @@ -0,0 +1,95 @@ +// +// CoreDataObservableTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/13/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +import DBClient +@testable import Example + +final class CoreDataObservableTests: DBClientCoreDataTest { + + func test_InsertionObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Object") + var expectedInsertedObjects = [User]() + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + expectedInsertedObjects = change.insertions.map { $0.element } + expectationObject.fulfill() + default: break + } + } + + dbClient.insert(objectsToCreate) { _ in } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedInsertedObjects.sorted(), objectsToCreate.sorted()) + } + } + + func test_UpdationObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Changes observe") + var expectedUpdatedObjects = [User]() + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + if !change.modifications.isEmpty { + expectedUpdatedObjects = change.modifications.map { $0.element } + expectationObject.fulfill() + } + default: break + } + } + + let updateExpectation = expectation(description: "Insert and update") + dbClient.insert(objectsToCreate) { _ in + objectsToCreate.forEach { $0.mutate() } + self.dbClient.update(objectsToCreate) { _ in + updateExpectation.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUpdatedObjects.sorted(), objectsToCreate.sorted()) + } + } + + func test_DeletionObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Object") + var expectedDeletedObjectsCount = 0 + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + if !change.deletions.isEmpty { + expectedDeletedObjectsCount = change.deletions.count + expectationObject.fulfill() + } + default: break + } + } + + dbClient.insert(objectsToCreate) { _ in + self.dbClient.delete(objectsToCreate) { _ in } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedDeletedObjectsCount, objectsToCreate.count) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataUpdateTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataUpdateTests.swift new file mode 100644 index 0000000..568f123 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataUpdateTests.swift @@ -0,0 +1,41 @@ +// +// CoreDataUpdateTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class CoreDataUpdateTests: DBClientCoreDataTest { + + func test_SyncUpdateUserName_WhenSuccessful_SetsCorrectName() { + let randomUser = User.createRandom() + + dbClient.insert(randomUser) + randomUser.name = "Bob" + let updationResult = dbClient.update(randomUser) + + XCTAssertEqual(randomUser, updationResult.value) + } + + func test_UpdateUserName_WhenSuccessful_SetsCorrectName() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedUser: User? + + self.dbClient.insert(randomUser) { result in + randomUser.name = "Bob" + self.dbClient.update(randomUser) { result in + expectedUser = result.value + expectationObject.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUser?.name, "Bob") + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/CoreDataUpsertTests.swift b/Example/DBClientTests/Interface/CoreData/CoreDataUpsertTests.swift new file mode 100644 index 0000000..be6e124 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/CoreDataUpsertTests.swift @@ -0,0 +1,44 @@ +// +// CoreDataUpsertTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/15/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class CoreDataUpsertTests: DBClientCoreDataTest { + + func test_SyncUpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { + let newUsers: [User] = (0...5).map { _ in User.createRandom() } + let savedUsers: [User] = (0...5).map { _ in User.createRandom() } + let combinedUsers = savedUsers + newUsers + + dbClient.insert(savedUsers) + let result = dbClient.upsert(combinedUsers) + + let expectedUsers = result.require().updated + result.require().inserted + XCTAssertEqual(expectedUsers.sorted(), combinedUsers.sorted()) + } + + func test_UpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { + let newUsers: [User] = (0...5).map { _ in User.createRandom() } + let savedUsers: [User] = (0...5).map { _ in User.createRandom() } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + let combinedUsers = savedUsers + newUsers + + self.dbClient.insert(savedUsers) { _ in + self.dbClient.upsert(combinedUsers) { result in + expectedUsers = result.require().updated + result.require().inserted + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers, combinedUsers) + } + } +} diff --git a/Example/DBClientTests/Interface/CoreData/DBClientCoreDataTest.swift b/Example/DBClientTests/Interface/CoreData/DBClientCoreDataTest.swift new file mode 100644 index 0000000..81b7da5 --- /dev/null +++ b/Example/DBClientTests/Interface/CoreData/DBClientCoreDataTest.swift @@ -0,0 +1,18 @@ +// +// DBClientCoreDataTest.swift +// DBClientTests +// +// Created by Roman Kyrylenko on 10/23/18. +// Copyright © 2018 Yalantis. All rights reserved. +// + +import XCTest +import DBClient + +class DBClientCoreDataTest: DBClientTest { + + private let client = CoreDataDBClient(forModel: "Users") + override var dbClient: DBClient! { + return client + } +} diff --git a/Example/DBClientTests/Interface/Realm/DBClientRealmTest.swift b/Example/DBClientTests/Interface/Realm/DBClientRealmTest.swift new file mode 100644 index 0000000..88930fd --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/DBClientRealmTest.swift @@ -0,0 +1,19 @@ +// +// DBClientRealmTest.swift +// DBClientTests +// +// Created by Roman Kyrylenko on 10/23/18. +// Copyright © 2018 Yalantis. All rights reserved. +// + +import XCTest +import DBClient +import RealmSwift + +class DBClientRealmTest: DBClientTest { + + private let client = RealmDBClient(realm: try! Realm()) + override var dbClient: DBClient! { + return client + } +} diff --git a/Example/DBClientTests/Interface/Realm/RealmCreateTests.swift b/Example/DBClientTests/Interface/Realm/RealmCreateTests.swift new file mode 100644 index 0000000..8e2f9c5 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmCreateTests.swift @@ -0,0 +1,45 @@ +// +// RealmCreateTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class RealmCreateTests: DBClientRealmTest { + + func test_SingleInsertion_WhenSuccessful_ReturnsObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedObject: User? + + self.dbClient.insert(randomUser) { result in + expectedObject = result.value + expectationObject.fulfill() + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertNotNil(expectedObject) + } + } + + func test_BulkInsertion_WhenSuccessful_ReturnsBulk() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let expectationObjects = expectation(description: "Objects") + var expectedObjectsCount = 0 + + self.dbClient.insert(randomUsers) { result in + expectedObjectsCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedObjectsCount, randomUsers.count) + } + } + +} diff --git a/Example/DBClientTests/Interface/Realm/RealmDeleteTests.swift b/Example/DBClientTests/Interface/Realm/RealmDeleteTests.swift new file mode 100644 index 0000000..cf65b54 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmDeleteTests.swift @@ -0,0 +1,52 @@ +// +// RealmDeleteTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class RealmDeleteTests: DBClientRealmTest { + + func test_SingleDeletion_WhenSuccessful_ReturnsNil() { + let randomUser = User.createRandom() + let expectationHit = expectation(description: "Object") + var isDeleted = false + + self.dbClient.insert(randomUser) { result in + if let object = result.value { + self.dbClient.delete(object) { result in + isDeleted = result.value != nil + expectationHit.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } + } + + func test_BulkDeletion_WhenSuccessful_ReturnsNil() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + let expectationHit = expectation(description: "Object") + var isDeleted = false + + self.dbClient.insert(randomUsers) { result in + if let objects = result.value { + self.dbClient.delete(objects) { result in + isDeleted = result.value != nil + expectationHit.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } + } + +} diff --git a/Example/DBClientTests/Interface/Realm/RealmExecuteTests.swift b/Example/DBClientTests/Interface/Realm/RealmExecuteTests.swift new file mode 100644 index 0000000..3ac71c6 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmExecuteTests.swift @@ -0,0 +1,129 @@ +// +// RealmExecuteTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +import DBClient +@testable import Example + +final class RealmExecuteTests: DBClientRealmTest { + + func test_SingleExecute_WhenSuccessful_ReturnsCount() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedCount = 0 + + self.dbClient.insert(randomUser) { result in + if result.value != nil { + let request = FetchRequest() + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObject.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, 1) + } + } + + func test_ExecuteWithOffset_WhenSuccessful_ReturnsCount() { + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let offset = 5 + let shiftedUsers = Array(randomUsers[offset..(fetchOffset: offset) + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, shiftedUsers.count) + } + } + + func test_ExecuteWithLimit_WhenSuccessful_ReturnsCount() { + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let limit = 3 + + let expectationObjects = expectation(description: "Object") + var expectedCount = 0 + + self.dbClient.insert(randomUsers) { result in + if result.value != nil { + let request = FetchRequest(fetchLimit: limit) + self.dbClient.execute(request) { result in + expectedCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedCount, limit) + } + } + + func test_ExecuteWithSortDescriptor_WhenSuccessful_ReturnsCount() { + let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) + let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let sortedUsers = randomUsers.sorted { $0.name.compare($1.name) == order } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + + self.dbClient.insert(randomUsers) { result in + if result.value != nil { + let request = FetchRequest(sortDescriptor: sortDescriptor) + + self.dbClient.execute(request) { result in + expectedUsers = result.value ?? [] + expectationObjects.fulfill() + } + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers, sortedUsers) + } + } + + func test_ExecuteWithPredicate_WhenSuccessful_ReturnsCount() { + let arg = "1" + let predicate = NSPredicate(format: "SELF.id ENDSWITH %@", arg) + let randomUsers: [User] = (0...10).map { _ in User.createRandom() } + let preicatedUsers = randomUsers.filter { $0.id.hasSuffix(arg) } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + + self.dbClient.insert(randomUsers) { result in + guard result.value != nil else { + expectationObjects.fulfill() + return + } + let request = FetchRequest(predicate: predicate) + + self.dbClient.execute(request) { result in + expectedUsers = result.value ?? [] + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers, preicatedUsers) + } + } + +} diff --git a/Example/DBClientTests/Interface/Realm/RealmFetchTests.swift b/Example/DBClientTests/Interface/Realm/RealmFetchTests.swift new file mode 100644 index 0000000..727a8af --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmFetchTests.swift @@ -0,0 +1,64 @@ +// +// RealmFetchTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import Foundation +import XCTest +import DBClient +@testable import Example + +class RealmFetchTests: DBClientRealmTest { + + func test_SyncFetch_WhenSuccessful_ReturnObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Inserting object") + + self.dbClient.insert(randomUser) { _ in expectationObject.fulfill() } + + waitForExpectations(timeout: 1) { _ in + let result = self.dbClient.execute(FetchRequest(predicate: NSPredicate(format: "id == %@", randomUser.id))) + XCTAssertEqual(result.require().count, 1) + let object = result.require().first + XCTAssertEqual(object, randomUser) + } + } + + func test_SingleFetch_WhenSuccessful_ReturnsObject() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedObject: User? + + self.dbClient.insert(randomUser) { result in + self.dbClient.findFirst(User.self, primaryValue: randomUser.id) { result in + expectedObject = result.require() + expectationObject.fulfill() + } + } + + waitForExpectations(timeout: 5) { _ in + XCTAssertNotNil(expectedObject) + } + } + + func test_BulkFetch_WhenSuccessful_ReturnsBulk() { + let randomUsers: [User] = (0...100).map { _ in User.createRandom() } + + let expectationObjects = expectation(description: "Objects") + var expectedObjectsCount = 0 + + self.dbClient.insert(randomUsers) { result in + self.dbClient.findAll { (result: Result<[User]>) in + expectedObjectsCount = result.value?.count ?? 0 + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedObjectsCount, randomUsers.count) + } + } +} diff --git a/Example/DBClientTests/Interface/Realm/RealmObservableTests.swift b/Example/DBClientTests/Interface/Realm/RealmObservableTests.swift new file mode 100644 index 0000000..43a1345 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmObservableTests.swift @@ -0,0 +1,95 @@ +//// +//// RealmObservableTests.swift +//// DBClient-Example +//// +//// Created by Roman Kyrylenko on 2/13/17. +//// Copyright © 2017 Yalantis. All rights reserved. +//// + +import XCTest +import DBClient +@testable import Example + +final class RealmObservableTests: DBClientRealmTest { + + func test_InsertionObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Object") + var expectedInsertedObjects = [User]() + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + expectedInsertedObjects = change.insertions.map { $0.element } + expectationObject.fulfill() + default: break + } + } + + dbClient.insert(objectsToCreate) { _ in } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedInsertedObjects.sorted(), objectsToCreate.sorted()) + } + } + + func test_UpdationObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Changes observe") + var expectedUpdatedObjects = [User]() + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + if !change.modifications.isEmpty { + expectedUpdatedObjects = change.modifications.map { $0.element } + expectationObject.fulfill() + } + default: break + } + } + + let updateExpectation = expectation(description: "Insert and update") + dbClient.insert(objectsToCreate) { _ in + objectsToCreate.forEach { $0.mutate() } + self.dbClient.update(objectsToCreate) { _ in + updateExpectation.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUpdatedObjects.sorted(), objectsToCreate.sorted()) + } + } + + func test_DeletionObservation_WhenSuccessful_InvokesChnages() { + let request = FetchRequest() + let observable = dbClient.observable(for: request) + let objectsToCreate: [User] = (0...100).map { _ in User.createRandom() } + let expectationObject = expectation(description: "Object") + var expectedDeletedObjectsCount = 0 + + observable.observe { (change: ObservableChange) in + switch change { + case .change(let change): + if !change.deletions.isEmpty { + expectedDeletedObjectsCount = change.deletions.count + expectationObject.fulfill() + } + default: break + } + } + + dbClient.insert(objectsToCreate) { _ in + self.dbClient.delete(objectsToCreate) { _ in } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedDeletedObjectsCount, objectsToCreate.count) + } + } +} diff --git a/Example/DBClientTests/Interface/Realm/RealmUpdateTests.swift b/Example/DBClientTests/Interface/Realm/RealmUpdateTests.swift new file mode 100644 index 0000000..82b3928 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmUpdateTests.swift @@ -0,0 +1,32 @@ +// +// RealmUpdateTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class RealmUpdateTests: DBClientRealmTest { + + func test_UpdateUserName_WhenSuccessful_SetsCorrectName() { + let randomUser = User.createRandom() + let expectationObject = expectation(description: "Object") + var expectedUser: User? + + self.dbClient.insert(randomUser) { result in + randomUser.name = "Bob" + self.dbClient.update(randomUser) { result in + expectedUser = result.value + expectationObject.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUser?.name, "Bob") + } + } + +} diff --git a/Example/DBClientTests/Interface/Realm/RealmUpsertTests.swift b/Example/DBClientTests/Interface/Realm/RealmUpsertTests.swift new file mode 100644 index 0000000..f3b1f42 --- /dev/null +++ b/Example/DBClientTests/Interface/Realm/RealmUpsertTests.swift @@ -0,0 +1,33 @@ +// +// RealmUpsertTests.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/15/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +import XCTest +@testable import Example + +final class RealmUpsertTests: DBClientRealmTest { + + func test_UpsertUsers_WhenSuccessful_ReturnsUpsertedUsers() { + let newUsers: [User] = (0...5).map { _ in User.createRandom() } + let savedUsers: [User] = (0...5).map { _ in User.createRandom() } + let expectationObjects = expectation(description: "Object") + var expectedUsers = [User]() + let combinedUsers = savedUsers + newUsers + + self.dbClient.insert(savedUsers) { _ in + self.dbClient.upsert(combinedUsers) { result in + expectedUsers = result.require().updated + result.require().inserted + expectationObjects.fulfill() + } + } + + waitForExpectations(timeout: 1) { _ in + XCTAssertEqual(expectedUsers, combinedUsers) + } + } + +} diff --git a/Example/DBClientTests/User+Comparable.swift b/Example/DBClientTests/User+Comparable.swift new file mode 100644 index 0000000..9e87ec7 --- /dev/null +++ b/Example/DBClientTests/User+Comparable.swift @@ -0,0 +1,30 @@ +// +// User+Comparable.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/9/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +@testable import Example + +// allows us to use `.sorted()` on the array of `User objects +extension User: Comparable { + + public static func < (lhs: User, rhs: User) -> Bool { + return lhs.id < rhs.id + } + + public static func <= (lhs: User, rhs: User) -> Bool { + return lhs.id <= rhs.id + } + + public static func >= (lhs: User, rhs: User) -> Bool { + return lhs.id >= rhs.id + } + + public static func > (lhs: User, rhs: User) -> Bool { + return rhs.id > rhs.id + } + +} diff --git a/Example/DBClientTests/User+Equtable.swift b/Example/DBClientTests/User+Equtable.swift new file mode 100644 index 0000000..374c77e --- /dev/null +++ b/Example/DBClientTests/User+Equtable.swift @@ -0,0 +1,18 @@ +// +// User+Equtable.swift +// DBClient-Example +// +// Created by Roman Kyrylenko on 2/8/17. +// Copyright © 2017 Yalantis. All rights reserved. +// + +@testable import Example + +// allows us to use `XCAssertEqual` on `User` objects +extension User: Equatable { + + public static func == (lhs: User, rhs: User) -> Bool { + return lhs.id == rhs.id && lhs.name == rhs.name + } + +} diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 99faf38..742e25d 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -8,6 +8,26 @@ /* Begin PBXBuildFile section */ 6544BEF6B0A326E74A0930F3 /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8EE3B934F48958491C32E38F /* Pods_Example.framework */; }; + 91146657A7C9C72AEB2CA0A0 /* Pods_DBClientTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */; }; + B8275B001E4B6D2600232EE4 /* DBClientTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */; }; + B8477EA11E4DC0EA00608B78 /* User+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */; }; + B8477EA21E4DC0EA00608B78 /* User+Equtable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */; }; + B8D2D430217F35200069CC57 /* CoreDataUpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */; }; + B8D2D431217F35200069CC57 /* CoreDataExecuteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */; }; + B8D2D432217F35200069CC57 /* CoreDataDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */; }; + B8D2D433217F35200069CC57 /* CoreDataUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */; }; + B8D2D434217F35200069CC57 /* CoreDataObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */; }; + B8D2D435217F35200069CC57 /* CoreDataFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */; }; + B8D2D436217F35200069CC57 /* CoreDataCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */; }; + B8D2D43E217F35260069CC57 /* RealmUpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */; }; + B8D2D43F217F35260069CC57 /* RealmCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D438217F35260069CC57 /* RealmCreateTests.swift */; }; + B8D2D440217F35260069CC57 /* RealmDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */; }; + B8D2D441217F35260069CC57 /* RealmUpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */; }; + B8D2D442217F35260069CC57 /* RealmExecuteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */; }; + B8D2D443217F35260069CC57 /* RealmFetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */; }; + B8D2D444217F35260069CC57 /* RealmObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */; }; + B8D2D446217F36B40069CC57 /* DBClientRealmTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */; }; + B8D2D448217F36DC0069CC57 /* DBClientCoreDataTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */; }; C533725A1E26155D004ECBCF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372591E26155D004ECBCF /* AppDelegate.swift */; }; C533725C1E26155D004ECBCF /* MasterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C533725B1E26155D004ECBCF /* MasterViewController.swift */; }; C533725E1E26155D004ECBCF /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C533725D1E26155D004ECBCF /* DetailViewController.swift */; }; @@ -24,10 +44,43 @@ C53372801E261A30004ECBCF /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = C53372781E261A30004ECBCF /* User.swift */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + B8275B021E4B6D2600232EE4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = C533724E1E26155D004ECBCF /* Project object */; + proxyType = 1; + remoteGlobalIDString = C53372551E26155D004ECBCF; + remoteInfo = Example; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXFileReference section */ + 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DBClientTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-DBClientTests/Pods-DBClientTests.release.xcconfig"; sourceTree = ""; }; 8EE3B934F48958491C32E38F /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9A5965F4590A17BA658C0DF4 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = ""; }; 9D0BE2A63D58FAA10DB1082A /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Pods/Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = ""; }; + A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DBClientTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-DBClientTests/Pods-DBClientTests.debug.xcconfig"; sourceTree = ""; }; + B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DBClientTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientTest.swift; sourceTree = ""; }; + B8275B011E4B6D2600232EE4 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Comparable.swift"; sourceTree = ""; }; + B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Equtable.swift"; sourceTree = ""; }; + B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataUpsertTests.swift; sourceTree = ""; }; + B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataExecuteTests.swift; sourceTree = ""; }; + B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataDeleteTests.swift; sourceTree = ""; }; + B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataUpdateTests.swift; sourceTree = ""; }; + B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataObservableTests.swift; sourceTree = ""; }; + B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataFetchTests.swift; sourceTree = ""; }; + B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataCreateTests.swift; sourceTree = ""; }; + B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmUpdateTests.swift; sourceTree = ""; }; + B8D2D438217F35260069CC57 /* RealmCreateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmCreateTests.swift; sourceTree = ""; }; + B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmDeleteTests.swift; sourceTree = ""; }; + B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmUpsertTests.swift; sourceTree = ""; }; + B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmExecuteTests.swift; sourceTree = ""; }; + B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmFetchTests.swift; sourceTree = ""; }; + B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmObservableTests.swift; sourceTree = ""; }; + B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientRealmTest.swift; sourceTree = ""; }; + B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBClientCoreDataTest.swift; sourceTree = ""; }; C53372561E26155D004ECBCF /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; C53372591E26155D004ECBCF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C533725B1E26155D004ECBCF /* MasterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterViewController.swift; sourceTree = ""; }; @@ -44,9 +97,18 @@ C53372761E261A30004ECBCF /* ObjectUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectUser.swift; sourceTree = ""; }; C53372771E261A30004ECBCF /* User+Realm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "User+Realm.swift"; sourceTree = ""; }; C53372781E261A30004ECBCF /* User.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; + F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DBClientTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B8275AFA1E4B6D2500232EE4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 91146657A7C9C72AEB2CA0A0 /* Pods_DBClientTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C53372531E26155D004ECBCF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -63,6 +125,8 @@ children = ( 9A5965F4590A17BA658C0DF4 /* Pods-Example.debug.xcconfig */, 9D0BE2A63D58FAA10DB1082A /* Pods-Example.release.xcconfig */, + A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */, + 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -71,17 +135,70 @@ isa = PBXGroup; children = ( 8EE3B934F48958491C32E38F /* Pods_Example.framework */, + F5FF85D02C1E34B0EF6E75EB /* Pods_DBClientTests.framework */, ); name = Frameworks; sourceTree = ""; }; + B8275AFE1E4B6D2600232EE4 /* DBClientTests */ = { + isa = PBXGroup; + children = ( + B8275AFF1E4B6D2600232EE4 /* DBClientTest.swift */, + B8275B011E4B6D2600232EE4 /* Info.plist */, + B8477E941E4DC0EA00608B78 /* Interface */, + B8477E9A1E4DC0EA00608B78 /* User+Comparable.swift */, + B8477E9B1E4DC0EA00608B78 /* User+Equtable.swift */, + ); + path = DBClientTests; + sourceTree = ""; + }; + B8477E941E4DC0EA00608B78 /* Interface */ = { + isa = PBXGroup; + children = ( + B8D2D427217F35010069CC57 /* CoreData */, + B8D2D428217F350B0069CC57 /* Realm */, + ); + path = Interface; + sourceTree = ""; + }; + B8D2D427217F35010069CC57 /* CoreData */ = { + isa = PBXGroup; + children = ( + B8D2D42F217F35200069CC57 /* CoreDataCreateTests.swift */, + B8D2D42B217F35200069CC57 /* CoreDataDeleteTests.swift */, + B8D2D42A217F35200069CC57 /* CoreDataExecuteTests.swift */, + B8D2D42E217F35200069CC57 /* CoreDataFetchTests.swift */, + B8D2D42D217F35200069CC57 /* CoreDataObservableTests.swift */, + B8D2D42C217F35200069CC57 /* CoreDataUpdateTests.swift */, + B8D2D429217F35200069CC57 /* CoreDataUpsertTests.swift */, + B8D2D447217F36DC0069CC57 /* DBClientCoreDataTest.swift */, + ); + path = CoreData; + sourceTree = ""; + }; + B8D2D428217F350B0069CC57 /* Realm */ = { + isa = PBXGroup; + children = ( + B8D2D445217F36B40069CC57 /* DBClientRealmTest.swift */, + B8D2D438217F35260069CC57 /* RealmCreateTests.swift */, + B8D2D439217F35260069CC57 /* RealmDeleteTests.swift */, + B8D2D43B217F35260069CC57 /* RealmExecuteTests.swift */, + B8D2D43C217F35260069CC57 /* RealmFetchTests.swift */, + B8D2D43D217F35260069CC57 /* RealmObservableTests.swift */, + B8D2D437217F35260069CC57 /* RealmUpdateTests.swift */, + B8D2D43A217F35260069CC57 /* RealmUpsertTests.swift */, + ); + path = Realm; + sourceTree = ""; + }; C533724D1E26155D004ECBCF = { isa = PBXGroup; children = ( + B8275AFE1E4B6D2600232EE4 /* DBClientTests */, C53372581E26155D004ECBCF /* Example */, - C53372571E26155D004ECBCF /* Products */, - 508C283B1E64110854D66E0E /* Pods */, 779BEA8364EDA4F88D3C7A7B /* Frameworks */, + 508C283B1E64110854D66E0E /* Pods */, + C53372571E26155D004ECBCF /* Products */, ); sourceTree = ""; }; @@ -89,6 +206,7 @@ isa = PBXGroup; children = ( C53372561E26155D004ECBCF /* Example.app */, + B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */, ); name = Products; sourceTree = ""; @@ -97,14 +215,14 @@ isa = PBXGroup; children = ( C53372591E26155D004ECBCF /* AppDelegate.swift */, + C53372621E26155D004ECBCF /* Assets.xcassets */, C533726D1E261A30004ECBCF /* DBClientInjector.swift */, - C533726E1E261A30004ECBCF /* Models */, - C533725B1E26155D004ECBCF /* MasterViewController.swift */, C533725D1E26155D004ECBCF /* DetailViewController.swift */, - C533725F1E26155D004ECBCF /* Main.storyboard */, - C53372621E26155D004ECBCF /* Assets.xcassets */, - C53372641E26155D004ECBCF /* LaunchScreen.storyboard */, C53372671E26155D004ECBCF /* Info.plist */, + C53372641E26155D004ECBCF /* LaunchScreen.storyboard */, + C533725F1E26155D004ECBCF /* Main.storyboard */, + C533725B1E26155D004ECBCF /* MasterViewController.swift */, + C533726E1E261A30004ECBCF /* Models */, ); path = Example; sourceTree = ""; @@ -112,9 +230,9 @@ C533726E1E261A30004ECBCF /* Models */ = { isa = PBXGroup; children = ( - C53372781E261A30004ECBCF /* User.swift */, C533726F1E261A30004ECBCF /* CoreData */, C53372751E261A30004ECBCF /* Realm */, + C53372781E261A30004ECBCF /* User.swift */, ); path = Models; sourceTree = ""; @@ -142,6 +260,26 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + B8275AFC1E4B6D2500232EE4 /* DBClientTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B8275B061E4B6D2600232EE4 /* Build configuration list for PBXNativeTarget "DBClientTests" */; + buildPhases = ( + BF151D4AF0AD5B6D29F45416 /* [CP] Check Pods Manifest.lock */, + B8275AF91E4B6D2500232EE4 /* Sources */, + B8275AFA1E4B6D2500232EE4 /* Frameworks */, + B8275AFB1E4B6D2500232EE4 /* Resources */, + 3C04B97117E537708993B649 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + B8275B031E4B6D2600232EE4 /* PBXTargetDependency */, + ); + name = DBClientTests; + productName = DBClientTests; + productReference = B8275AFD1E4B6D2500232EE4 /* DBClientTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; C53372551E26155D004ECBCF /* Example */ = { isa = PBXNativeTarget; buildConfigurationList = C533726A1E26155D004ECBCF /* Build configuration list for PBXNativeTarget "Example" */; @@ -151,7 +289,7 @@ C53372531E26155D004ECBCF /* Frameworks */, C53372541E26155D004ECBCF /* Resources */, 5DD0265501157C9DFB18D487 /* [CP] Embed Pods Frameworks */, - 3E7D40D093AC662DD66ED84E /* [CP] Copy Pods Resources */, + 8839DF70215C08D90021A85A /* ShellScript */, ); buildRules = ( ); @@ -169,11 +307,18 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0820; - LastUpgradeCheck = 0820; + LastUpgradeCheck = 1000; ORGANIZATIONNAME = Yalantis; TargetAttributes = { + B8275AFC1E4B6D2500232EE4 = { + CreatedOnToolsVersion = 8.2.1; + LastSwiftMigration = 1000; + ProvisioningStyle = Automatic; + TestTargetID = C53372551E26155D004ECBCF; + }; C53372551E26155D004ECBCF = { CreatedOnToolsVersion = 8.2.1; + LastSwiftMigration = 1000; ProvisioningStyle = Automatic; }; }; @@ -183,6 +328,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -192,11 +338,19 @@ projectRoot = ""; targets = ( C53372551E26155D004ECBCF /* Example */, + B8275AFC1E4B6D2500232EE4 /* DBClientTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + B8275AFB1E4B6D2500232EE4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; C53372541E26155D004ECBCF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -210,19 +364,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3E7D40D093AC662DD66ED84E /* [CP] Copy Pods Resources */ = { + 3C04B97117E537708993B649 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DBClientTests/Pods-DBClientTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/DBClient/DBClient.framework", + "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", + "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", + "${BUILT_PRODUCTS_DIR}/YALResult/YALResult.framework", ); - name = "[CP] Copy Pods Resources"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DBClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YALResult.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example/Pods-Example-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DBClientTests/Pods-DBClientTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 5DD0265501157C9DFB18D487 /* [CP] Embed Pods Frameworks */ = { @@ -231,13 +394,22 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/DBClient/DBClient.framework", + "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", + "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", + "${BUILT_PRODUCTS_DIR}/YALResult/YALResult.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DBClient.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YALResult.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 7A7AA963D82D9DF854F879D4 /* [CP] Check Pods Manifest.lock */ = { @@ -246,18 +418,82 @@ files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Example-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 8839DF70215C08D90021A85A /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\n"; + }; + BF151D4AF0AD5B6D29F45416 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DBClientTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_ROOT}/../Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B8275AF91E4B6D2500232EE4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B8D2D434217F35200069CC57 /* CoreDataObservableTests.swift in Sources */, + B8D2D442217F35260069CC57 /* RealmExecuteTests.swift in Sources */, + B8D2D435217F35200069CC57 /* CoreDataFetchTests.swift in Sources */, + B8D2D444217F35260069CC57 /* RealmObservableTests.swift in Sources */, + B8D2D441217F35260069CC57 /* RealmUpsertTests.swift in Sources */, + B8275B001E4B6D2600232EE4 /* DBClientTest.swift in Sources */, + B8D2D43F217F35260069CC57 /* RealmCreateTests.swift in Sources */, + B8D2D436217F35200069CC57 /* CoreDataCreateTests.swift in Sources */, + B8D2D448217F36DC0069CC57 /* DBClientCoreDataTest.swift in Sources */, + B8D2D431217F35200069CC57 /* CoreDataExecuteTests.swift in Sources */, + B8D2D443217F35260069CC57 /* RealmFetchTests.swift in Sources */, + B8D2D433217F35200069CC57 /* CoreDataUpdateTests.swift in Sources */, + B8D2D440217F35260069CC57 /* RealmDeleteTests.swift in Sources */, + B8D2D432217F35200069CC57 /* CoreDataDeleteTests.swift in Sources */, + B8477EA21E4DC0EA00608B78 /* User+Equtable.swift in Sources */, + B8D2D446217F36B40069CC57 /* DBClientRealmTest.swift in Sources */, + B8D2D43E217F35260069CC57 /* RealmUpdateTests.swift in Sources */, + B8D2D430217F35200069CC57 /* CoreDataUpsertTests.swift in Sources */, + B8477EA11E4DC0EA00608B78 /* User+Comparable.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; C53372521E26155D004ECBCF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -278,6 +514,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + B8275B031E4B6D2600232EE4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = C53372551E26155D004ECBCF /* Example */; + targetProxy = B8275B021E4B6D2600232EE4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ C533725F1E26155D004ECBCF /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -298,6 +542,34 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + B8275B041E4B6D2600232EE4 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A17286BF14E3A7198D1175A3 /* Pods-DBClientTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = DBClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Debug; + }; + B8275B051E4B6D2600232EE4 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 75617BC79816F39A066B228E /* Pods-DBClientTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = DBClientTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; + }; + name = Release; + }; C53372681E26155D004ECBCF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -307,15 +579,23 @@ CLANG_CXX_LIBRARY = "libc++"; 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 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_IMPLICIT_RETAIN_SELF = 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; @@ -357,15 +637,23 @@ CLANG_CXX_LIBRARY = "libc++"; 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_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 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_IMPLICIT_RETAIN_SELF = 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; @@ -400,7 +688,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.dbclient.Example; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Debug; }; @@ -413,13 +701,22 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.dbclient.Example; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B8275B061E4B6D2600232EE4 /* Build configuration list for PBXNativeTarget "DBClientTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B8275B041E4B6D2600232EE4 /* Debug */, + B8275B051E4B6D2600232EE4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; C53372511E26155D004ECBCF /* Build configuration list for PBXProject "Example" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index 32d2ef6..7c82da7 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -1,6 +1,6 @@ // // AppDelegate.swift -// Example +// DBClient-Example // // Created by Serhii Butenko on 11/1/17. // Copyright © 2017 Yalantis. All rights reserved. diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json index 36d2c80..1d060ed 100644 --- a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,5 +1,15 @@ { "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, { "idiom" : "iphone", "size" : "29x29", @@ -30,6 +40,16 @@ "size" : "60x60", "scale" : "3x" }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, { "idiom" : "ipad", "size" : "29x29", @@ -59,6 +79,11 @@ "idiom" : "ipad", "size" : "76x76", "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" } ], "info" : { diff --git a/Example/Example/Base.lproj/Main.storyboard b/Example/Example/Base.lproj/Main.storyboard index a1442e6..ad01c38 100644 --- a/Example/Example/Base.lproj/Main.storyboard +++ b/Example/Example/Base.lproj/Main.storyboard @@ -1,5 +1,5 @@ - + @@ -57,7 +57,7 @@ - + @@ -69,22 +69,30 @@ - + + + - - - + + + + + + + + + + + - + diff --git a/Example/Example/DBClientInjector.swift b/Example/Example/DBClientInjector.swift index 90aa64e..28a2558 100644 --- a/Example/Example/DBClientInjector.swift +++ b/Example/Example/DBClientInjector.swift @@ -11,24 +11,17 @@ import DBClient import RealmSwift private struct DBClientInjector { - - static var coreDataClient: DBClient = CoreDataDBClient(forModel: "Users") - - static var realmClient: DBClient = { - let realm = try! Realm() - return RealmDBClient(realm: realm) - }() - + + static let coreDataClient: DBClient = CoreDataDBClient(forModel: "Users") + static let realmClient: DBClient = RealmDBClient(realm: try! Realm()) } protocol DBClientInjectable {} extension DBClientInjectable { - + var dbClient: DBClient { - get { - return DBClientInjector.coreDataClient -// return DBClientInjector.realmClient - } + // return DBClientInjector.coreDataClient + return DBClientInjector.realmClient } } diff --git a/Example/Example/DetailViewController.swift b/Example/Example/DetailViewController.swift index a35aa47..aaf4903 100644 --- a/Example/Example/DetailViewController.swift +++ b/Example/Example/DetailViewController.swift @@ -1,6 +1,6 @@ // // DetailViewController.swift -// Example +// DBClient-Example // // Created by Serhii Butenko on 11/1/17. // Copyright © 2017 Yalantis. All rights reserved. @@ -8,16 +8,24 @@ import UIKit -class DetailViewController: UIViewController { +class DetailViewController: UIViewController, DBClientInjectable { var detailItem: User! - @IBOutlet private weak var detailLabel: UILabel! + @IBOutlet private weak var userNameTextField: UITextField! override func viewDidLoad() { super.viewDidLoad() - detailLabel.text = detailItem.name + userNameTextField.text = detailItem.name + title = detailItem.id + } + + @IBAction private func saveButtonAction() { + detailItem.name = userNameTextField.text ?? "" + dbClient.update(detailItem) { [weak self] _ in + self?.navigationController?.popViewController(animated: true) + } } } diff --git a/Example/Example/MasterViewController.swift b/Example/Example/MasterViewController.swift index fdaa106..de99bcf 100644 --- a/Example/Example/MasterViewController.swift +++ b/Example/Example/MasterViewController.swift @@ -1,6 +1,6 @@ // // MasterViewController.swift -// Example +// DBClient-Example // // Created by Serhii Butenko on 11/1/17. // Copyright © 2017 Yalantis. All rights reserved. @@ -9,55 +9,30 @@ import UIKit import DBClient -class MasterViewController: UITableViewController, DBClientInjectable { +final class MasterViewController: UITableViewController, DBClientInjectable { fileprivate var objects = [User]() - - var observable: RequestObservable! - + + private var userChangesObservable: RequestObservable? + override func viewDidLoad() { super.viewDidLoad() - - observable = dbClient.observable(for: FetchRequest()) - observable.observe { changeSet in - switch changeSet { - case .initial(let initial): - self.objects.append(contentsOf: initial) - self.tableView.reloadData() - - case .change(objects: let objects, deletions: let deletions, insertions: let insertions, modifications: let modifications): - self.objects = objects - self.tableView.beginUpdates() - - let insertedIndexPaths = insertions.map { IndexPath(row: $0.index, section: 0) } - self.tableView.insertRows(at: insertedIndexPaths, with: .automatic) - - let deletedIndexPaths = deletions.map { IndexPath(row: $0, section: 0) } - self.tableView.deleteRows(at: deletedIndexPaths, with: .automatic) - - let updatedIndexPaths = modifications.map { IndexPath(row: $0.index, section: 0) } - self.tableView.reloadRows(at: updatedIndexPaths, with: .automatic) - - self.tableView.endUpdates() - - case .error(let error): - print("Got an error: \(error)") - } + let observable = dbClient.observable(for: FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])) + userChangesObservable = observable + observable.observe { [weak self] changeSet in + self?.observeChanges(changeSet) } navigationItem.leftBarButtonItem = editButtonItem } - @IBAction func addObject(_ sender: Any) { - dbClient.save(User.createRandom()) - } - // MARK: - Segues override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - guard segue.identifier == "showDetail", let controller = segue.destination as? DetailViewController else { - fatalError() + guard segue.identifier == "showDetail", + let controller = segue.destination as? DetailViewController else { + fatalError() } if let indexPath = tableView.indexPathForSelectedRow { @@ -66,7 +41,42 @@ class MasterViewController: UITableViewController, DBClientInjectable { } } - // MARK: - Table View + // MARK: - Actions + + @IBAction private func addObject(_ sender: Any) { + dbClient.insert(User.createRandom()) { _ in } + } + + private func observeChanges(_ changeSet: ObservableChange) { + switch changeSet { + case .initial(let initial): + objects.append(contentsOf: initial) + tableView.reloadData() + + case .change(let change): + self.objects = change.objects + tableView.beginUpdates() + + let insertedIndexPaths = change.insertions.map { IndexPath(row: $0.index, section: 0) } + tableView.insertRows(at: insertedIndexPaths, with: .automatic) + + let deletedIndexPaths = change.deletions.map { IndexPath(row: $0, section: 0) } + tableView.deleteRows(at: deletedIndexPaths, with: .automatic) + + let updatedIndexPaths = change.modifications.map { IndexPath(row: $0.index, section: 0) } + tableView.reloadRows(at: updatedIndexPaths, with: .automatic) + + tableView.endUpdates() + + case .error(let error): + print("Got an error: \(error)") + } + } +} + +// MARK: - Table View + +extension MasterViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return objects.count @@ -84,11 +94,12 @@ class MasterViewController: UITableViewController, DBClientInjectable { return true } - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { - guard editingStyle == .delete else { return } + override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + guard editingStyle == .delete else { + return + } let user = objects[indexPath.row] - dbClient.delete(user) + dbClient.delete(user) { _ in } } - } diff --git a/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift b/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift index 0c9f842..1c40091 100644 --- a/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift +++ b/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift @@ -1,6 +1,6 @@ // // ManagedUser+CoreDataClass.swift -// YChat +// DBClient-Example // // Created by Roman Kyrylenko on 01/06/17. // Copyright © 2016 Yalantis. All rights reserved. diff --git a/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift b/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift index b90df31..e449d79 100644 --- a/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift +++ b/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift @@ -1,6 +1,6 @@ // // ManagedUser+CoreDataProperties.swift -// YChat +// DBClient-Example // // Created by Roman Kyrylenko on 01/06/17. // Copyright © 2016 Yalantis. All rights reserved. @@ -11,7 +11,8 @@ import CoreData extension ManagedUser { - @nonobjc class func fetchRequest() -> NSFetchRequest { + @nonobjc + class func fetchRequest() -> NSFetchRequest { return NSFetchRequest(entityName: User.entityName) } diff --git a/Example/Example/Models/CoreData/User+CoreData.swift b/Example/Example/Models/CoreData/User+CoreData.swift index acb7f13..1e69103 100644 --- a/Example/Example/Models/CoreData/User+CoreData.swift +++ b/Example/Example/Models/CoreData/User+CoreData.swift @@ -1,6 +1,6 @@ // // User+CoreData.swift -// YChat +// DBClient-Example // // Created by Roman Kyrylenko on 01/06/17. // Copyright © 2016 Yalantis. All rights reserved. @@ -11,21 +11,18 @@ import DBClient import CoreData extension User: CoreDataModelConvertible { - + public static var entityName: String { return String(describing: self) } - + public static func managedObjectClass() -> NSManagedObject.Type { return ManagedUser.self } - - public func toManagedObject(in context: NSManagedObjectContext) -> NSManagedObject { - let fetchRequest: NSFetchRequest = ManagedUser.fetchRequest() - fetchRequest.predicate = NSPredicate(format: "id = %@", id) - let result = try? context.fetch(fetchRequest) - let user: ManagedUser - if let result = result?.first { // fetch existing + + public func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject { + var user: ManagedUser + if let result = existedInstance as? ManagedUser { // fetch existing user = result } else { // or create new user = NSEntityDescription.insertNewObject( @@ -38,7 +35,7 @@ extension User: CoreDataModelConvertible { return user } - + public static func from(_ managedObject: NSManagedObject) -> Stored { guard let managedUser = managedObject as? ManagedUser else { fatalError("can't create User object from object \(managedObject)") @@ -47,7 +44,16 @@ extension User: CoreDataModelConvertible { let name = managedUser.name else { fatalError("can't get required properties for user \(managedObject)") } - + return User(id: id, name: name) } + + func isPrimaryValueEqualTo(value: Any) -> Bool { + if let value = value as? String { + return value == id + } + + return false + } + } diff --git a/Example/Example/Models/Realm/ObjectUser.swift b/Example/Example/Models/Realm/ObjectUser.swift index 92fc3d8..ad3b715 100644 --- a/Example/Example/Models/Realm/ObjectUser.swift +++ b/Example/Example/Models/Realm/ObjectUser.swift @@ -12,10 +12,10 @@ import RealmSwift class ObjectUser: Object { override class func primaryKey() -> String? { - return "id" + return #keyPath(ObjectUser.id) } - dynamic var id: String = "" - dynamic var name: String = "" + @objc dynamic var id: String = "" + @objc dynamic var name: String = "" } diff --git a/Example/Example/Models/Realm/User+Realm.swift b/Example/Example/Models/Realm/User+Realm.swift index e6b0b02..fa46363 100644 --- a/Example/Example/Models/Realm/User+Realm.swift +++ b/Example/Example/Models/Realm/User+Realm.swift @@ -1,6 +1,6 @@ // // User+Realm.swift -// YChat +// DBClient-Example // // Created by Roman Kyrylenko on 01/06/17. // Copyright © 2016 Yalantis. All rights reserved. @@ -11,7 +11,7 @@ import DBClient import RealmSwift extension User: RealmModelConvertible { - + static func from(_ realmObject: Object) -> Stored { guard let objectUser = realmObject as? ObjectUser else { fatalError("Can't create `User` from \(realmObject)") @@ -25,27 +25,11 @@ extension User: RealmModelConvertible { } func toRealmObject() -> Object { -// let realm = try! Realm() -// -// let object: Object -// if let existingUser = realm.object(ofType: User.realmClass(), forPrimaryKey: id) { -// object = existingUser -// } else { -// let user = ObjectUser() -// user.id = id -// user.name = name -// object = user -// } -// let user = ObjectUser() user.id = id user.name = name - return user -// object = user - - -// return object + return user } } diff --git a/Example/Example/Models/User.swift b/Example/Example/Models/User.swift index a564eb0..c84d59e 100644 --- a/Example/Example/Models/User.swift +++ b/Example/Example/Models/User.swift @@ -10,23 +10,29 @@ import Foundation import DBClient class User { - - let name: String - let id: String - + + var name: String + var id: String + init(id: String, name: String) { self.id = id self.name = name } - + + func mutate() { + name = String(name.reversed()) + } } extension User: Stored { - public static var primaryKey: String? { + public static var primaryKeyName: String? { return "id" } - + + public var valueOfPrimaryKey: CVarArg? { + return id + } } extension User { diff --git a/Example/Podfile b/Example/Podfile index c2c413b..1707261 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,9 +1,14 @@ -platform :ios, '9.0' +platform :ios, '10.0' use_frameworks! target 'Example' do pod 'DBClient/Realm', :path => '../' pod 'DBClient/CoreData', :path => '../' +end + +target 'DBClientTests' do + pod 'DBClient/Realm', :path => '../' + pod 'DBClient/CoreData', :path => '../' end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 404cdb4..65ae243 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,32 +1,40 @@ PODS: - - Bolts-Swift (1.3.0) - - DBClient/Core (0.1): - - Bolts-Swift (~> 1.3.0) - - DBClient/CoreData (0.1): + - DBClient/Core (1.4.2): + - YALResult (= 1.4) + - DBClient/CoreData (1.4.2): - DBClient/Core - - DBClient/Realm (0.1): + - YALResult (= 1.4) + - DBClient/Realm (1.4.2): - DBClient/Core - - RealmSwift (~> 2.1.1) - - Realm (2.1.2): - - Realm/Headers (= 2.1.2) - - Realm/Headers (2.1.2) - - RealmSwift (2.1.2): - - Realm (= 2.1.2) + - RealmSwift (~> 3.15.0) + - YALResult (= 1.4) + - Realm (3.15.0): + - Realm/Headers (= 3.15.0) + - Realm/Headers (3.15.0) + - RealmSwift (3.15.0): + - Realm (= 3.15.0) + - YALResult (1.4) DEPENDENCIES: - DBClient/CoreData (from `../`) - DBClient/Realm (from `../`) +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - Realm + - RealmSwift + - YALResult + EXTERNAL SOURCES: DBClient: - :path: ../ + :path: "../" SPEC CHECKSUMS: - Bolts-Swift: fa98d1b59fc1acea9b21a21306dcdca1c85e3737 - DBClient: fcf29df338854a8509d00cad7356ab3190be4006 - Realm: efe855f4d977c8ce5a82d3116d9f1ff155a6550c - RealmSwift: 17d6ee30b6f9df86364408c2197492e33bfea567 + DBClient: 6833b6f3abb9bbd90dc3289621179f3fad102c0c + Realm: 9b834e1be6062f544805252c812348872dc5d4ed + RealmSwift: 8a41886f8ab6efef9eb8df97de2f2bb911561a79 + YALResult: 26915691cdd19269936336d6f28e1a015c64175e -PODFILE CHECKSUM: 179eda0e5897e0216cdd26b5cd14adae089b4671 +PODFILE CHECKSUM: 01e2cf56f70348c45b9e7248a505591dd86d210b -COCOAPODS: 1.2.0 +COCOAPODS: 1.6.1 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3466917 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Yalantis + +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. \ No newline at end of file diff --git a/README.md b/README.md index a79668d..4bc75a3 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,151 @@ # DBClient -## Requirements +[![cocoapods](https://img.shields.io/cocoapods/v/DBClient.svg)](https://img.shields.io/cocoapods/v/DBClient.svg) ![swift](https://img.shields.io/badge/Swift-5.0-orange.svg) ![Platform](http://img.shields.io/badge/platform-iOS-blue.svg?style=flat) [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/Yalantis/DBClient/blob/master/LICENSE) -- Xcode 8 -- Swift 3 -- iOS 9+ +## Integration (Cocoapods) -## Installation +There're three podspecs: -### Cocoapods +- `DBClient/Core` contains pure (CoreData/Realm-free) interface / types used to abstract from implementation. Use it only in case you're about to provide custom implementation of any available storage types. +- `DBClient/CoreData` contains CoreData implementation. +- `DBClient/Realm` contains Realm implementation. -There're 3 podspecs: +## Usage -Core, common classes for any database: +Depending on DataBase type you need to create a client: +`let client: DBClient = RealmDBClient(realm: realm)` +or +`let client: DBClient = CoreDataDBClient(forModel: "Users")` -```ruby -pod 'DBClient', '~> 1.0' +Base methods (`CRUD`, `observe`) are the same for each type and could be found documented in [`DBClient.swift`](https://github.com/Yalantis/DBClient/blob/master/DBClient/Core/DBClient.swift) + +Each model you create required to conform `Stored` protocol with two properties: +``` +extension User: Stored { + + public static var primaryKeyName: String? { + return "id" + } + + public var valueOfPrimaryKey: CVarArg? { + return id + } +} +``` + +For each model you create you need to define associated database model described below. + +### Realm + +To adopt Realm, you need to provide `RealmModelConvertible` protocol implementation for each model you want to support. +`extension User: RealmModelConvertible` + +The protocol contains three required methods. + +The first one provides a class (decendant of realm's `Object`) to be associated with your model: +``` +static func realmClass() -> Object.Type { + return ObjectUser.self +} +``` + +The second one converts abstract realm's `Object` to your model: +``` +static func from(_ realmObject: Object) -> Stored { + guard let objectUser = realmObject as? ObjectUser else { + fatalError("Can't create `User` from \(realmObject)") + } + + return User(id: objectUser.id, name: objectUser.name) +} +``` + +The last one converts your model to realm's object: +``` +func toRealmObject() -> Object { + let user = ObjectUser() + user.id = id + user.name = name + + return user +} ``` -Wrapper for CoreData: +### CoreData -```ruby -pod 'DBClient/CoreData', '~> 1.0' +To adopt CoreData, you need to create your model and provide appropriate file name to client's constructor (bundle could also be specified) and for each your model provide implementation of the `CoreDataModelConvertible` protocol. +`extension User: CoreDataModelConvertible` + +The protocol requires four methods and one field to be implemented. Documentation for each method/field could be found in [`CoreDataDBClient.swift`](https://github.com/Yalantis/DBClient/blob/master/DBClient/CoreData/CoreDataDBClient.swift) + +In the field `entityName` you should provide entity name (equal to one specified in your model): +``` +public static var entityName: String { + return String(describing: self) +} +``` + +The next method used to determine associated `NSManagedObject` ancestor to your model: +``` +public static func managedObjectClass() -> NSManagedObject.Type { + return ManagedUser.self +} ``` -Wrapper for Realm: +The next method determines whether given object equal to current: +``` +func isPrimaryValueEqualTo(value: Any) -> Bool { + if let value = value as? String { + return value == id + } -```ruby -pod 'DBClient/Realm', '~> 1.0' + return false +} ``` -⚠️ It's not ready yet (there're problems with deletion and observation) \ No newline at end of file + +Next method used to convert `NSManagedObject` to your model. Feel free to fail with `fatalError` here since it's developer's issue. +``` +public static func from(_ managedObject: NSManagedObject) -> Stored { + guard let managedUser = managedObject as? ManagedUser else { + fatalError("can't create User object from object \(managedObject)") + } + guard let id = managedUser.id, + let name = managedUser.name else { + fatalError("can't get required properties for user \(managedObject)") + } + + return User(id: id, name: name) +} +``` + +The last method used to create/update `NSManagedObject` in given context based on your model: +``` +public func upsertManagedObject(in context: NSManagedObjectContext, existedInstance: NSManagedObject?) -> NSManagedObject { + var user: ManagedUser + if let result = existedInstance as? ManagedUser { // fetch existing + user = result + } else { // or create new + user = NSEntityDescription.insertNewObject( + forEntityName: User.entityName, + into: context + ) as! ManagedUser + } + user.id = id + user.name = name + + return user +} + +``` + +## Version history + + +| Version | Swift | Dependencies | iOS | +|----------|-------|----------------------------------------|------| +| `1.4.2` | 5 | RealmSwift 3.15.0, YALResult 1.4 | 10 | +| `1.3` | 4.2 | RealmSwift 3.11.1, YALResult 1.1 | 10 | +| `1.0` | 4.2 | RealmSwift 2.10.1, YALResult 1.0 | 10 | +| `0.7` | 4.0 | RealmSwift 2.10.1, BoltsSwift 1.4 | 9 | +| `0.6` | 4 | RealmSwift 2.10.1, BoltsSwift 1.3 | 9 | +| `0.4.2` | 3.2 | RealmSwift 2.1.1, BoltsSwift 1.3 | 9 |