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 2ae503b..6b2eb23 100644 --- a/DBClient.podspec +++ b/DBClient.podspec @@ -1,35 +1,31 @@ Pod::Spec.new do |s| - s.name = "DBClient" - s.version = "0.4.2" + 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 diff --git a/DBClient/Core/DBClient.swift b/DBClient/Core/DBClient.swift index 6db9398..ee4a7e0 100644 --- a/DBClient/Core/DBClient.swift +++ b/DBClient/Core/DBClient.swift @@ -6,7 +6,15 @@ // Copyright © 2016 Yalantis. All rights reserved. // -import BoltsSwift +import Foundation +import YALResult + +public typealias Result = YALResult + +public enum DBClientError: Error { + + case missingPrimaryKey, missingData +} /// Protocol for transaction restrictions in `DBClient`. /// Used for transactions of all type. @@ -17,60 +25,108 @@ public protocol Stored { /// 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`. + /// Executes given request and calls completion result wrapped in `Result`. /// - /// - Parameter request: request to execute - /// - Returns: `Task` with array of objects or error in case of failude. - func execute(_ request: FetchRequest) -> Task<[T]> + /// - 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) /// 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 + func observable(for request: FetchRequest) -> RequestObservable /// Inserts objects to database. /// - /// - Parameter objects: list of objects to be inserted - /// - Returns: `Task` with inserted objects or appropriate error in case of failure. - @discardableResult - func insert(_ objects: [T]) -> Task<[T]> + /// - 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. /// - /// - 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]> + /// - 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. /// - /// - Parameter objects: list of objects to be deleted - /// - Returns: `Task` with appropriate error in case of failure. - @discardableResult - func delete(_ objects: [T]) -> Task + /// - 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 /// - /// - Parameter objects: objects to be worked with - /// - Returns: A `Task` with inserted and updated instances + /// - 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 upsert(_ objects: [T]) -> Task<(updated: [T], inserted: [T])> + 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 findAll() -> Task<[T]> { + /// - Parameter completion: `Result` with array of objects + func findAll(completion: @escaping (Result<[T]>) -> Void) { + execute(FetchRequest(), completion: completion) + } + + /// Synchronously fetch all entities from database + /// + /// - Returns: `Result` with array of objects + func findAll() -> Result<[T]> { return execute(FetchRequest()) } @@ -81,10 +137,11 @@ public extension DBClient { /// - type: type of object to search for /// - primaryValue: the value of primary key field to search for /// - predicate: predicate for request - /// - Returns: `Task` with found object or nil. - func findFirst(_ type: T.Type, primaryValue: String, predicate: NSPredicate? = nil) -> Task { + /// - 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 { - return Task(nil) + completion(.failure(DBClientError.missingPrimaryKey)) + return } let primaryKeyPredicate = NSPredicate(format: "\(primaryKey) == %@", primaryValue) @@ -96,45 +153,109 @@ public extension DBClient { } let request = FetchRequest(predicate: fetchPredicate, fetchLimit: 1) - return execute(request).continueWithTask { task -> Task in - return Task(task.result?.first) + execute(request) { result in + completion(result.map({ $0.first })) + } + } + + /// 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. /// - /// - Parameter object: object to be inserted - /// - Returns: `Task` with inserted object or appropriate error in case of failure. - @discardableResult func insert(_ object: T) -> Task { - return convertArrayTaskToSingleObject(insert([object])) + /// - 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. /// - /// - 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])) + /// - 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. /// - /// - Parameter object: object to be deleted - /// - Returns: `Task` with appropriate error in case of failure. - @discardableResult func delete(_ object: T) -> Task { + /// - 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]) } - 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() + /// 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/FetchRequest.swift b/DBClient/Core/FetchRequest.swift index 45560d2..da7c6d8 100644 --- a/DBClient/Core/FetchRequest.swift +++ b/DBClient/Core/FetchRequest.swift @@ -11,28 +11,22 @@ import Foundation /// Describes a fetch request to get objects from a database. public struct FetchRequest { - public let sortDescriptor: NSSortDescriptor? + 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. - /// - sortDescriptor: Sort descriptor; 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, - sortDescriptor: NSSortDescriptor? = nil, - fetchOffset: Int = 0, - fetchLimit: Int = 0 - ) { + public init(predicate: NSPredicate? = nil, sortDescriptors: [NSSortDescriptor]? = nil, fetchOffset: Int = 0, fetchLimit: Int = 0) { self.predicate = predicate - self.sortDescriptor = sortDescriptor + self.sortDescriptors = sortDescriptors self.fetchOffset = fetchOffset self.fetchLimit = fetchLimit } - } // MARK: - Filtering @@ -74,7 +68,6 @@ public extension FetchRequest { func filtered(with key: String, notIn value: [String]) -> FetchRequest { return request(withPredicate: NSPredicate(format: "NOT (\(key) IN %@)", value)) } - } // MARK: - Sorting @@ -82,21 +75,24 @@ public extension FetchRequest { public extension FetchRequest { func sorted(with sortDescriptor: NSSortDescriptor) -> FetchRequest { - return request(withSortDescriptor: sortDescriptor) + 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(withSortDescriptor: NSSortDescriptor(key: key, ascending: ascending, comparator: cmptr)) + return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, comparator: cmptr)]) } func sorted(with key: String?, ascending: Bool) -> FetchRequest { - return request(withSortDescriptor: NSSortDescriptor(key: key, ascending: ascending)) + return request(withSortDescriptors: [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)) + return request(withSortDescriptors: [NSSortDescriptor(key: key, ascending: ascending, selector: selector)]) } - } // MARK: - Private @@ -104,21 +100,10 @@ public extension FetchRequest { private extension FetchRequest { func request(withPredicate predicate: NSPredicate) -> FetchRequest { - return FetchRequest( - predicate: predicate, - sortDescriptor: sortDescriptor, - fetchOffset: fetchOffset, - fetchLimit: fetchLimit - ) + return FetchRequest(predicate: predicate, sortDescriptors: sortDescriptors, fetchOffset: fetchOffset, fetchLimit: fetchLimit) } - func request(withSortDescriptor sortDescriptor: NSSortDescriptor) -> FetchRequest { - return FetchRequest( - predicate: predicate, - sortDescriptor: sortDescriptor, - 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 031901e..f234e43 100644 --- a/DBClient/Core/RequestObservable.swift +++ b/DBClient/Core/RequestObservable.swift @@ -29,7 +29,6 @@ public enum ObservableChange { case initial([T]) case change(ModelChange) case error(Error) - } public class RequestObservable { @@ -47,5 +46,4 @@ public class RequestObservable { public func observe(_ closure: @escaping (ObservableChange) -> Void) { assertionFailure("The observe method must be overriden") } - } diff --git a/DBClient/CoreData/CoreDataDBClient.swift b/DBClient/CoreData/CoreDataDBClient.swift index 9d0aa7e..c8799fb 100644 --- a/DBClient/CoreData/CoreDataDBClient.swift +++ b/DBClient/CoreData/CoreDataDBClient.swift @@ -7,7 +7,6 @@ // import CoreData -import BoltsSwift /// Describes type of model for CoreData database client. /// Model should conform to CoreDataModelConvertible protocol @@ -36,15 +35,13 @@ public protocol CoreDataModelConvertible: Stored { /// Decides whether primary value of object equal to given func isPrimaryValueEqualTo(value: Any) -> Bool - } extension NSManagedObject: Stored { - + public static var primaryKeyName: String? { return nil } public var valueOfPrimaryKey: CVarArg? { return nil } - } public enum MigrationType { @@ -65,7 +62,6 @@ public enum MigrationType { return false } } - } /// Implementation of database client for CoreData storage type. @@ -241,63 +237,73 @@ public class CoreDataDBClient { // MARK: - Read/write - fileprivate func performWriteTask(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { + private func performWriteTask(_ closure: @escaping (NSManagedObjectContext, (() throws -> ())) -> ()) { let context = writeManagedContext context.perform { closure(context) { - try context.save() - try self.mainContext.save() - try self.rootContext.save() + try context.save(includingParent: true) } } } - fileprivate func performReadTask(closure: @escaping (NSManagedObjectContext) -> ()) { + 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 observable(for request: FetchRequest) -> RequestObservable { + + public func observable(for request: FetchRequest) -> RequestObservable { return CoreDataObservable(request: request, context: mainContext) } - public func execute(_ request: FetchRequest) -> Task<[T]> { + public func execute(_ request: FetchRequest, completion: @escaping (Result<[T]>) -> Void) where T: Stored { let coreDataModelType = checkType(T.self) - let taskCompletionSource = TaskCompletionSource<[T]>() - performReadTask { context in let fetchRequest = self.fetchRequest(for: coreDataModelType) fetchRequest.predicate = request.predicate - fetchRequest.sortDescriptors = [request.sortDescriptor].flatMap { $0 } + fetchRequest.sortDescriptors = request.sortDescriptors 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) + let resultModels = result.compactMap { coreDataModelType.from($0) as? T } + + completion(.success(resultModels)) } catch let error { - taskCompletionSource.set(error: error) + completion(.failure(error)) } } - - return taskCompletionSource.task } /// 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]) -> Task<[T]> { + public func insert(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<[T]>() performWriteTask { context, savingClosure in var insertedObjects = [T]() let foundObjects = self.find(objects: objects, in: context) @@ -309,23 +315,21 @@ extension CoreDataDBClient: DBClient { _ = object.upsertManagedObject(in: context, existedInstance: nil) insertedObjects.append(object as! T) } - + do { try savingClosure() - taskCompletionSource.set(result: insertedObjects) + completion(.success(insertedObjects)) } catch let error { - taskCompletionSource.set(error: error) + completion(.failure(error)) } } - return taskCompletionSource.task } /// Method to update existed in DB objects /// if there is no such object in db nothing will happened - public func update(_ objects: [T]) -> Task<[T]> { + public func update(_ objects: [T], completion: @escaping (Result<[T]>) -> Void) where T: Stored { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<[T]>() performWriteTask { context, savingClosure in var updatedObjects = [T]() @@ -341,19 +345,17 @@ extension CoreDataDBClient: DBClient { do { try savingClosure() - taskCompletionSource.set(result: updatedObjects) + completion(.success(updatedObjects)) } catch let error { - taskCompletionSource.set(error: error) + completion(.failure(error)) } } - return taskCompletionSource.task } /// Update object if it exists or insert new one otherwise - public func upsert(_ objects: [T]) -> Task<(updated: [T], inserted: [T])> { + public func upsert(_ objects: [T], completion: @escaping (Result<(updated: [T], inserted: [T])>) -> Void) where T: Stored { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<(updated: [T], inserted: [T])>() performWriteTask { context, savingClosure in var updatedObjects = [T]() var insertedObjects = [T]() @@ -370,36 +372,188 @@ extension CoreDataDBClient: DBClient { do { try savingClosure() - taskCompletionSource.set(result: (updated: updatedObjects, inserted: insertedObjects)) + completion(.success((updated: updatedObjects, inserted: insertedObjects))) } catch let error { - taskCompletionSource.set(error: error) + completion(.failure(error)) } } - - return taskCompletionSource.task } /// For each element in collection: /// After all deletes try to save context - public func delete(_ objects: [T]) -> Task { + public func delete(_ objects: [T], completion: @escaping (Result<()>) -> Void) where T: Stored { checkType(T.self) - let taskCompletionSource = TaskCompletionSource() performWriteTask { context, savingClosure in let foundObjects = self.find(objects, in: context) foundObjects.forEach { context.delete($0) } do { try savingClosure() - taskCompletionSource.set(result: ()) + 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 { - taskCompletionSource.set(error: error) + result = .failure(error) } } - return taskCompletionSource.task + 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 + } } private extension CoreDataDBClient { @@ -428,7 +582,7 @@ private extension CoreDataDBClient { return [] } - let ids = objects.flatMap { $0.valueOfPrimaryKey } + 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 { @@ -461,7 +615,6 @@ private extension CoreDataDBClient { func convert(objects: [T]) -> [CoreDataModelConvertible] { checkType(T.self) - return objects.flatMap { $0 as? CoreDataModelConvertible } + return objects.compactMap { $0 as? CoreDataModelConvertible } } - } diff --git a/DBClient/CoreData/CoreDataObservable.swift b/DBClient/CoreData/CoreDataObservable.swift index 08914a9..d442047 100644 --- a/DBClient/CoreData/CoreDataObservable.swift +++ b/DBClient/CoreData/CoreDataObservable.swift @@ -28,8 +28,8 @@ class CoreDataObservable: RequestObservable { if let predicate = request.predicate { fetchRequest.predicate = predicate } - if let sortDescriptor = request.sortDescriptor { - fetchRequest.sortDescriptors = [sortDescriptor] + 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") @@ -111,6 +111,10 @@ private class FetchedResultsControllerDelegate: NSObject, NS case .update, .move: batchChanges.append(.update(indexPath!.row, object)) + + @unknown + default: + assertionFailure("trying to handle unknown case \(type)") } } @@ -129,7 +133,9 @@ private class FetchedResultsControllerDelegate: NSObject, NS insertions: inserted, modifications: updated ) - observer?(.change(mappedChange)) + if let observer = observer { + observer(.change(mappedChange)) + } batchChanges = [] } 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 eb273b1..aa80718 100644 --- a/DBClient/Realm/RealmDBClient.swift +++ b/DBClient/Realm/RealmDBClient.swift @@ -7,7 +7,6 @@ // import Foundation -import BoltsSwift import RealmSwift /// Describes protocol to be implemented by model for `RealmDBClient` @@ -24,7 +23,6 @@ public protocol RealmModelConvertible: Stored { /// Executes backward mapping from `Realm.Object` func toRealmObject() -> Object - } extension RealmModelConvertible { @@ -32,7 +30,6 @@ extension RealmModelConvertible { func realmClassForInstance() -> Object.Type { return Self.realmClass() } - } /// Implementation of database client for Realm storage type. @@ -44,7 +41,6 @@ public class RealmDBClient { public init(realm: Realm) { self.realm = realm } - } // MARK: DBClient @@ -52,103 +48,131 @@ public class RealmDBClient { extension RealmDBClient: DBClient { /// Executes given request. Fetches all entities and then applies all given restrictions - public func execute(_ request: FetchRequest) -> Task<[T]> { - let modelType = checkType(T.self) + 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 taskCompletionSource = TaskCompletionSource<[T]>() + let realmType = type.realmClass() + + do { + let realmObjects = realm.objects(realmType) + realm.beginWrite() + realm.delete(realmObjects) + try realm.commitWrite() + + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + + 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 } - .get(offset: request.fetchOffset, limit: request.fetchLimit) - .flatMap { modelType.from($0) as? T } - taskCompletionSource.set(result: objects) + .slice(offset: request.fetchOffset, limit: request.fetchLimit) + .compactMap { modelType.from($0) as? T } - return taskCompletionSource.task + return .success(objects) } - /// Inserts new objects to database. If object with such `primaryKeyValue` already exists Realm'll throw an error - public func insert(_ objects: [T]) -> Task<[T]> { + @discardableResult + public func insert(_ objects: [T]) -> Result<[T]> { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<[T]>() + let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } - let realmObjects = objects.flatMap { ($0 as? RealmModelConvertible)?.toRealmObject() } do { realm.beginWrite() realm.add(realmObjects) try realm.commitWrite() - taskCompletionSource.set(result: objects) - } catch let error { - taskCompletionSource.set(error: error) + return .success(objects) + } catch { + return .failure(error) } - - return taskCompletionSource.task } - /// Updates objects which are already in db. - public func update(_ objects: [T]) -> Task<[T]> { + @discardableResult + public func update(_ objects: [T]) -> Result<[T]> { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<[T]>() let realmObjects = separate(objects: objects) .present - .flatMap { ($0 as? RealmModelConvertible)?.toRealmObject() } + .compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } do { realm.beginWrite() realm.add(realmObjects, update: true) try realm.commitWrite() - taskCompletionSource.set(result: objects) + + return .success(objects) } catch let error { - taskCompletionSource.set(error: error) + return .failure(error) } - - return taskCompletionSource.task } - /// Removes objects by it `primaryKeyValue`s - public func delete(_ objects: [T]) -> Task { + @discardableResult + public func delete(_ objects: [T]) -> Result<()> { let type = checkType(T.self) - let taskCompletionSource = TaskCompletionSource() let realmType = type.realmClass() do { - let primaryValues = objects.flatMap { $0.valueOfPrimaryKey } - let realmObjects = primaryValues.flatMap { realm.object(ofType: realmType, forPrimaryKey: $0) } + let primaryValues = objects.compactMap { $0.valueOfPrimaryKey } + let realmObjects = primaryValues.compactMap { realm.object(ofType: realmType, forPrimaryKey: $0) } realm.beginWrite() realm.delete(realmObjects) try realm.commitWrite() - taskCompletionSource.set(result: ()) - } catch let error { - taskCompletionSource.set(error: error) + + return .success(()) + } catch { + return .failure(error) } - - return taskCompletionSource.task } - public func upsert(_ objects: [T]) -> Task<(updated: [T], inserted: [T])> { + @discardableResult + public func upsert(_ objects: [T]) -> Result<(updated: [T], inserted: [T])> { checkType(T.self) - let taskCompletionSource = TaskCompletionSource<(updated: [T], inserted: [T])>() let separatedObjects = separate(objects: objects) - let realmObjects = objects.flatMap { ($0 as? RealmModelConvertible)?.toRealmObject() } + let realmObjects = objects.compactMap { ($0 as? RealmModelConvertible)?.toRealmObject() } do { realm.beginWrite() realm.add(realmObjects, update: true) try realm.commitWrite() - taskCompletionSource.set(result: (updated: separatedObjects.present, inserted: separatedObjects.new)) - } catch let error { - taskCompletionSource.set(error: error) + return .success((updated: separatedObjects.present, inserted: separatedObjects.new)) + } catch { + return .failure(error) } - - return taskCompletionSource.task - } - - public func observable(for request: FetchRequest) -> RequestObservable { - checkType(T.self) - - return RealmObservable(request: request, realm: realm) } } @@ -188,15 +212,14 @@ private extension RealmDBClient { return (present: presentObjects, new: notPresentObjects) } - } internal extension FetchRequest { - func applyTo(realmObjects: Results) -> Results { + func applyTo(realmObjects: Results) -> Results { var objects: Results = realmObjects - if let sortDescriptor = sortDescriptor, let key = sortDescriptor.key { - objects = realmObjects.sorted(byProperty: key, ascending: sortDescriptor.ascending) + if let sortDescriptors = sortDescriptors?.compactMap(SortDescriptor.init), !sortDescriptors.isEmpty { + objects = realmObjects.sorted(by: sortDescriptors) } if let predicate = predicate { objects = objects.filter(predicate) @@ -204,12 +227,23 @@ internal extension FetchRequest { return objects } +} + +private extension SortDescriptor { + + init?(_ descriptor: NSSortDescriptor) { + if let key = descriptor.key { + self = SortDescriptor(keyPath: key, ascending: descriptor.ascending) + } else { + return nil + } + } } private extension Array { - func get(offset: Int, limit: Int) -> [T] { + func slice(offset: Int, limit: Int) -> [T] { var lim = 0 var off = 0 let count = self.count @@ -223,7 +257,6 @@ private extension Array { lim = offset + limit } - return (off..: RequestObservable { } let realmObjects = request.applyTo(realmObjects: realm.objects(realmModelType.realmClass())) - notificationToken = realmObjects.addNotificationBlock { changes in + notificationToken = realmObjects.observe { changes in switch changes { case .initial(let initial): let mapped = initial.map { realmModelType.from($0) as! T } @@ -62,5 +62,4 @@ internal class RealmObservable: RequestObservable { public func stopObserving() { notificationToken = nil } - } diff --git a/Example/DBClientTests/DBClientTest.swift b/Example/DBClientTests/DBClientTest.swift index 3860fae..2bbd894 100644 --- a/Example/DBClientTests/DBClientTest.swift +++ b/Example/DBClientTests/DBClientTest.swift @@ -8,53 +8,11 @@ import XCTest import DBClient -import BoltsSwift -import RealmSwift @testable import Example -enum StorageType { - - case realm - case coreData - -} - -let storageType: StorageType = .realm - class DBClientTest: XCTestCase { - lazy var dbClient: DBClient = { - switch storageType { - case .realm: - let realm = try! Realm() - return RealmDBClient(realm: realm) - - case .coreData: - return CoreDataDBClient(forModel: "Users") - } - }() - - var expectationTimeout: TimeInterval { - return 25 - } - - // execute given closure asynchronously with expectation - func execute(_ closure: @escaping (XCTestExpectation) -> ()) { - let exp = expectation(description: "DBClientTestExpectation") - switch storageType { - // because realm transactions should be perfomrmed in the same thread where realm created - case .realm: - closure(exp) - - default: - DispatchQueue.global(qos: .background).async { - closure(exp) - } - } - waitForExpectations(timeout: expectationTimeout) { (error) in - XCTAssert(error == nil, "\(error)") - } - } + var dbClient: DBClient! { return nil } override func setUp() { super.setUp() @@ -63,56 +21,30 @@ class DBClientTest: XCTestCase { } override func tearDown() { - super.tearDown() - cleanUpDatabase() + + super.tearDown() } // removes all objects from the database func cleanUpDatabase() { - print("[DBClientTest]: Cleaning database") - var count = 0 - let request: Task<[User]> = dbClient.findAll() - execute { (expectation) in - request - .continueOnSuccessWithTask { users -> Task in - count = users.count - return self.dbClient.delete(users) - } - .continueOnSuccessWith { objects in - print("[DBClientTest]: Removed \(count) objects") - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - @discardableResult func createRandomUser() -> User { - let randomUser = User.createRandom() - execute { expectation in - self.dbClient - .insert(randomUser) - .continueOnSuccessWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } + guard dbClient != nil else { return } + let expectationDeleletion = expectation(description: "Deletion") + var isDeleted = false - return randomUser - } - - @discardableResult func createRandomUsers(_ count: Int) -> [User] { - let randomUsers = (0..) in + guard let objects = result.value else { + expectationDeleletion.fulfill() + return + } + self.dbClient.delete(objects) { _ in + isDeleted = true + expectationDeleletion.fulfill() + } } - return randomUsers + waitForExpectations(timeout: 1) { _ in + XCTAssert(isDeleted) + } } - } 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/CreateTests.swift b/Example/DBClientTests/Interface/CreateTests.swift deleted file mode 100644 index 7ea89f2..0000000 --- a/Example/DBClientTests/Interface/CreateTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// CreateTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/8/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import XCTest -import BoltsSwift -@testable import Example - -final class CreateTests: DBClientTest { - - func testSingleInsertion() { - let randomUser = User.createRandom() - execute { expectation in - self.dbClient - .insert(randomUser) - .continueOnSuccessWith { savedUser in - XCTAssertEqual(randomUser, savedUser) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testBulkInsertions() { - let randomUsers: [User] = (0...100).map { _ in User.createRandom() } - execute { expectation in - self.dbClient - .insert(randomUsers) - .continueOnSuccessWith { savedUsers in - XCTAssertEqual(randomUsers, savedUsers) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testAsyncInsertions() { - let randomUsers: [User] = (0...100).map { _ in User.createRandom() } - var tasks: [Task] = [] - - execute { expectation in - for user in randomUsers { - tasks.append(self.dbClient.insert(user)) - } - Task.whenAll(tasks) - .continueOnSuccessWith { createdTasks in - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} - diff --git a/Example/DBClientTests/Interface/DeleteTests.swift b/Example/DBClientTests/Interface/DeleteTests.swift deleted file mode 100644 index 5dc6181..0000000 --- a/Example/DBClientTests/Interface/DeleteTests.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// DeleteTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/9/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import XCTest -import BoltsSwift -@testable import Example - -final class DeleteTests: DBClientTest { - - func testSingleDeletion() { - let randomUser = createRandomUser() - // remove user from db - execute { expectation in - self.dbClient - .delete(randomUser) - .continueOnSuccessWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } - // check if it has been removed - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: randomUser.id) - .continueOnSuccessWith { user in - XCTAssert(user == nil) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testBulkDeletions() { - let randomUsers: [User] = createRandomUsers(100) - // remove users from db - execute { expectation in - self.dbClient - .delete(randomUsers) - .continueOnSuccessWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } - // check if they have been removed - execute { expectation in - let request: Task<[User]> = self.dbClient.findAll() - request - .continueOnSuccessWith { users in - XCTAssert(users.isEmpty) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testAsyncDeletions() { - let randomUsers: [User] = createRandomUsers(100) - - var tasks: [Task] = [] - - execute { expectation in - for user in randomUsers { - tasks.append(self.dbClient.delete(user)) - } - Task.whenAll(tasks) - .continueOnSuccessWith { createdTasks in - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} - diff --git a/Example/DBClientTests/Interface/ExecuteTests.swift b/Example/DBClientTests/Interface/ExecuteTests.swift deleted file mode 100644 index 1564157..0000000 --- a/Example/DBClientTests/Interface/ExecuteTests.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// ExecuteTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/9/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import XCTest -import BoltsSwift -import DBClient -@testable import Example - -final class ExecuteTests: DBClientTest { - - func testNakedExecute() { - let user = createRandomUser() - - let request = FetchRequest() - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { users in - XCTAssertEqual(users.count, 1) - XCTAssertEqual(user, users.first!) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testOffset() { - let randomUsers = createRandomUsers(10) - let offset = 5 - let shiftedUsers = Array(randomUsers[offset..(fetchOffset: offset) - execute { expectation in - self.dbClient - .execute(request) - .continueWith { task in - guard let users = task.result else { - XCTFail("\(task.error)") - return - } - - // check only count of arrays beacause we haven't specified sorting - XCTAssertEqual(shiftedUsers.count, users.count) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testLimit() { - createRandomUsers(10) - let limit = 3 - - let request = FetchRequest(fetchLimit: limit) - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { users in - XCTAssertEqual(limit, users.count) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testOrder() { - let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) - let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending - - let randomUsers = createRandomUsers(10).sorted { $0.0.name.compare($0.1.name) == order } - - let request = FetchRequest(sortDescriptor: sortDescriptor) - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { users in - XCTAssertEqual(randomUsers, users) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testPredicate() { - let arg = "1" - let predicate = NSPredicate(format: "SELF.id ENDSWITH %@", arg) - - let randomUsers = createRandomUsers(10).filter { - $0.id.hasSuffix(arg) - } - - let request = FetchRequest(predicate: predicate) - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { users in - XCTAssertEqual(randomUsers, users) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testOffsetWithOrder() { - let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) - let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending - - let randomUsers = createRandomUsers(10) - let offset = 5 - let limit = 2 - let sortedUsers = randomUsers.sorted { $0.0.name.compare($0.1.name) == order } - let shiftedUsers = Array(sortedUsers[offset..(sortDescriptor: sortDescriptor, fetchOffset: offset, fetchLimit: limit) - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { users in - XCTAssertEqual(shiftedUsers, users) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testCombinedRequest() { - let sortDescriptor = NSSortDescriptor(key: "name", ascending: true) - let order: ComparisonResult = sortDescriptor.ascending ? .orderedAscending : .orderedDescending - - let arg = "1" - let predicate = NSPredicate(format: "SELF.id BEGINSWITH %@", arg) - - let randomUsers = createRandomUsers(50) - let offset = 2 - let limit = 5 - var users = randomUsers.filter { $0.id.hasPrefix(arg) } - users = users.sorted { $0.0.name.compare($0.1.name) == order } - users = Array(users[offset..( - predicate: predicate, - sortDescriptor: sortDescriptor, - fetchOffset: offset, - fetchLimit: limit - ) - execute { expectation in - self.dbClient - .execute(request) - .continueOnSuccessWith { fetchedUsers in - XCTAssertEqual(users, fetchedUsers) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} diff --git a/Example/DBClientTests/Interface/FetchTests.swift b/Example/DBClientTests/Interface/FetchTests.swift deleted file mode 100644 index a4eb305..0000000 --- a/Example/DBClientTests/Interface/FetchTests.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// FetchTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/8/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import Foundation -import BoltsSwift -import XCTest -@testable import Example - -class FetchTests: DBClientTest { - - func testSingleFetch() { - let user = createRandomUser() - // check if it has been successfully saved - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: user.id) - .continueOnSuccessWith { fetchedUser in - XCTAssertEqual(user, fetchedUser) - expectation.fulfill() - } - } - } - - func testBulkFetch() { - let randomUsers: [User] = createRandomUsers(10).sorted() - - // check if generated users have been successfully saved - let request: Task<[User]> = dbClient.findAll() - execute { expectation in - request - .continueOnSuccessWith { fetchedUsers in - // use sort to match users order - XCTAssert(randomUsers == fetchedUsers.sorted()) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - - func testAsyncFetches() { - let randomUsers: [User] = createRandomUsers(100) - let userIds: [String] = randomUsers.map { $0.id } - var tasks: [Task] = [] - - // async fetch them - execute { expectation in - for userId in userIds { - tasks.append(self.dbClient.findFirst(User.self, primaryValue: userId)) - } - Task.whenAll(tasks) - .continueOnSuccessWith { - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} diff --git a/Example/DBClientTests/Interface/ObservableTests.swift b/Example/DBClientTests/Interface/ObservableTests.swift deleted file mode 100644 index 527af50..0000000 --- a/Example/DBClientTests/Interface/ObservableTests.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// ObservableTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/13/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import BoltsSwift -import XCTest -import DBClient -@testable import Example - -final class ObservableTests: DBClientTest { - - func testInsertionObservations() { - let request = FetchRequest() - let observable = dbClient.observable(for: request) - - let objectsToCreate = 50 - - observable.observe { (change: ObservableChange) in - switch change { - - case .change(let change): - XCTAssertEqual(change.insertions.count, objectsToCreate) - XCTAssertEqual(change.objects.count, objectsToCreate) - - case .initial(let objects): - XCTAssert(objects.isEmpty) - - case .error(let error): - XCTFail("\(error)") - } - } - - createRandomUsers(objectsToCreate) - } - - func testUpdationObservations() { - let request = FetchRequest() - let observable = dbClient.observable(for: request) - - let numberOfUsers = 50 - createRandomUsers(numberOfUsers) - let numberOfUsersToUpdate = 10 - - observable.observe { (change: ObservableChange) in - switch change { - - case .change(let change): - XCTAssertEqual(change.objects.count, numberOfUsers) - XCTAssertEqual(change.modifications.count, numberOfUsersToUpdate) - - case .initial(let objects): - XCTAssertEqual(objects.count, numberOfUsers) - - case .error(let error): - XCTFail("\(error)") - } - } - - execute { expectation in - let request = FetchRequest(fetchLimit: numberOfUsersToUpdate) - self.dbClient.execute(request) - .continueOnSuccessWithTask { users -> Task<[User]> in - return self.dbClient.update(users) - } - .continueOnSuccessWith { _ in - expectation.fulfill() - } - } - } - - func testDeletionObservations() { - let request = FetchRequest() - let observable = dbClient.observable(for: request) - - let numberOfUsers = 50 - createRandomUsers(numberOfUsers) - let numberOfUsersToDelete = 10 - - observable.observe { (change: ObservableChange) in - switch change { - - case .change(let change): - XCTAssertEqual(change.objects.count, numberOfUsers - numberOfUsersToDelete) - XCTAssertEqual(change.deletions.count, numberOfUsersToDelete) - - case .initial(let objects): - XCTAssertEqual(objects.count, numberOfUsers) - - case .error(let error): - XCTFail("\(error)") - } - } - - execute { expectation in - let request = FetchRequest(fetchLimit: numberOfUsersToDelete) - self.dbClient.execute(request) - .continueOnSuccessWithTask { users -> Task in - return self.dbClient.delete(users) - } - .continueOnSuccessWith { _ in - expectation.fulfill() - } - } - } - - func testComplexRequestObservations() { - let users = createRandomUsers(100) - let offset = 5 - let suffix = "1" - let numberOfMatchedUsers = users.filter { $0.id.hasSuffix(suffix) }.count - - let request = FetchRequest( - predicate: NSPredicate(format: "SELF.id ENDSWITH %@", suffix), - fetchOffset: offset - ) - let observable = dbClient.observable(for: request) - - observable.observe { (change: ObservableChange) in - switch change { - - case .change(let change): - XCTAssertEqual(numberOfMatchedUsers - offset, change.objects.count) - - case .initial(let objects): - XCTAssertEqual(objects.count, numberOfMatchedUsers - offset) - - case .error(let error): - XCTFail("\(error)") - } - } - } - -} 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/Interface/UpdateTests.swift b/Example/DBClientTests/Interface/UpdateTests.swift deleted file mode 100644 index baecca4..0000000 --- a/Example/DBClientTests/Interface/UpdateTests.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// UpdateTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/9/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import XCTest -import BoltsSwift -@testable import Example - -final class UpdateTests: DBClientTest { - - func testSingleUpdate() { - let randomUser = createRandomUser() - let userId = randomUser.id - let userName = "named \(randomUser.name)" - // update user's name - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: userId) - .continueOnSuccessWithTask { user -> Task in - XCTAssert(user != nil) - user?.name = userName - return self.dbClient.update(user!) - } - .continueOnSuccessWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } - // check it - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: userId) - .continueOnSuccessWith { user in - XCTAssert(user != nil) - XCTAssert(user!.name == userName) - expectation.fulfill() - } - } - } - - func testPrimaryValueUpdate() { - let randomUser = createRandomUser() - let userId = randomUser.id - let newUserId = "n\(userId)" - // update user's id - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: userId) - .continueOnSuccessWithTask { user -> Task in - XCTAssert(user != nil) - user?.id = newUserId - return self.dbClient.update(user!) - } - .continueWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } - // check if it exists - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: userId) - .continueOnSuccessWith { user in - // old value should exists - XCTAssert(user != nil) - expectation.fulfill() - } - } - execute { expectation in - self.dbClient - .findFirst(User.self, primaryValue: newUserId) - .continueOnSuccessWith { user in - // new value shouldn't - XCTAssert(user == nil) - expectation.fulfill() - } - } - - } - - func testBulkUpdates() { - // sort users by id to be sure that each arrays contains the same user at the same index - let randomUsers: [User] = createRandomUsers(100).sorted() - let userNames: [String] = randomUsers.map { "awesome \($0.name)" } - - // fetch and update them - execute { expectation in - let task: Task<[User]> = self.dbClient.findAll() - task - .continueOnSuccessWithTask { users -> Task<[User]> in - let sortedUsers = users.sorted() - let updatedUsers = (0.. User in - let user = sortedUsers[index] - user.name = userNames[index] - return user - } - - return self.dbClient.update(updatedUsers) - } - .continueOnSuccessWith { _ in - expectation.fulfill() - } - .waitUntilCompleted() - } - - // check - execute { expectation in - let task: Task<[User]> = self.dbClient.findAll() - task - .continueOnSuccessWith { users in - let fetchedUserNames = users - .sorted() - .map { $0.name } - XCTAssertEqual(userNames, fetchedUserNames) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} diff --git a/Example/DBClientTests/Interface/UpsertTests.swift b/Example/DBClientTests/Interface/UpsertTests.swift deleted file mode 100644 index 89d59e5..0000000 --- a/Example/DBClientTests/Interface/UpsertTests.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// UpsertTests.swift -// DBClient-Example -// -// Created by Roman Kyrylenko on 2/15/17. -// Copyright © 2017 Yalantis. All rights reserved. -// - -import XCTest -import BoltsSwift -@testable import Example - -final class UpsertTests: DBClientTest { - - func testUpsert() { - let savedUsers = createRandomUsers(10) - let newUsers: [User] = (0...5).map { _ in User.createRandom() } - var combinedUsers = savedUsers - combinedUsers.append(contentsOf: newUsers) - - execute { expectation in - self.dbClient - .upsert(combinedUsers) - .continueOnSuccessWith { upsertions in - XCTAssertEqual(savedUsers.sorted(), upsertions.updated.sorted()) - XCTAssertEqual(newUsers.sorted(), upsertions.inserted.sorted()) - expectation.fulfill() - } - .waitUntilCompleted() - } - } - -} diff --git a/Example/DBClientTests/User+Comparable.swift b/Example/DBClientTests/User+Comparable.swift index e6dddb9..9e87ec7 100644 --- a/Example/DBClientTests/User+Comparable.swift +++ b/Example/DBClientTests/User+Comparable.swift @@ -11,19 +11,19 @@ // allows us to use `.sorted()` on the array of `User objects extension User: Comparable { - public static func <(lhs: User, rhs: User) -> Bool { + public static func < (lhs: User, rhs: User) -> Bool { return lhs.id < rhs.id } - public static func <=(lhs: User, rhs: User) -> Bool { + public static func <= (lhs: User, rhs: User) -> Bool { return lhs.id <= rhs.id } - public static func >=(lhs: User, rhs: User) -> Bool { + public static func >= (lhs: User, rhs: User) -> Bool { return lhs.id >= rhs.id } - public static func >(lhs: User, rhs: User) -> Bool { + 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 index 01c4567..374c77e 100644 --- a/Example/DBClientTests/User+Equtable.swift +++ b/Example/DBClientTests/User+Equtable.swift @@ -11,7 +11,7 @@ // allows us to use `XCAssertEqual` on `User` objects extension User: Equatable { - public static func ==(lhs: User, rhs: User) -> Bool { + 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 79c484a..742e25d 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -10,15 +10,24 @@ 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 */; }; - B8477E9C1E4DC0EA00608B78 /* CreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E951E4DC0EA00608B78 /* CreateTests.swift */; }; - B8477E9D1E4DC0EA00608B78 /* DeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E961E4DC0EA00608B78 /* DeleteTests.swift */; }; - B8477E9E1E4DC0EA00608B78 /* ExecuteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E971E4DC0EA00608B78 /* ExecuteTests.swift */; }; - B8477E9F1E4DC0EA00608B78 /* FetchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E981E4DC0EA00608B78 /* FetchTests.swift */; }; - B8477EA01E4DC0EA00608B78 /* UpdateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8477E991E4DC0EA00608B78 /* UpdateTests.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 */; }; - B88711421E5211AF00189D40 /* ObservableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B88711411E5211AF00189D40 /* ObservableTests.swift */; }; - B8E447AB1E54916E0078D81A /* UpsertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E447AA1E54916E0078D81A /* UpsertTests.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 */; }; @@ -54,15 +63,24 @@ 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 = ""; }; - B8477E951E4DC0EA00608B78 /* CreateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CreateTests.swift; sourceTree = ""; }; - B8477E961E4DC0EA00608B78 /* DeleteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeleteTests.swift; sourceTree = ""; }; - B8477E971E4DC0EA00608B78 /* ExecuteTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExecuteTests.swift; sourceTree = ""; }; - B8477E981E4DC0EA00608B78 /* FetchTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchTests.swift; sourceTree = ""; }; - B8477E991E4DC0EA00608B78 /* UpdateTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateTests.swift; 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 = ""; }; - B88711411E5211AF00189D40 /* ObservableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObservableTests.swift; sourceTree = ""; }; - B8E447AA1E54916E0078D81A /* UpsertTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpsertTests.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 = ""; }; @@ -137,17 +155,42 @@ B8477E941E4DC0EA00608B78 /* Interface */ = { isa = PBXGroup; children = ( - B8477E951E4DC0EA00608B78 /* CreateTests.swift */, - B8477E961E4DC0EA00608B78 /* DeleteTests.swift */, - B8477E971E4DC0EA00608B78 /* ExecuteTests.swift */, - B8477E981E4DC0EA00608B78 /* FetchTests.swift */, - B88711411E5211AF00189D40 /* ObservableTests.swift */, - B8477E991E4DC0EA00608B78 /* UpdateTests.swift */, - B8E447AA1E54916E0078D81A /* UpsertTests.swift */, + 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 = ( @@ -187,9 +230,9 @@ C533726E1E261A30004ECBCF /* Models */ = { isa = PBXGroup; children = ( - C53372781E261A30004ECBCF /* User.swift */, C533726F1E261A30004ECBCF /* CoreData */, C53372751E261A30004ECBCF /* Realm */, + C53372781E261A30004ECBCF /* User.swift */, ); path = Models; sourceTree = ""; @@ -226,7 +269,6 @@ B8275AFA1E4B6D2500232EE4 /* Frameworks */, B8275AFB1E4B6D2500232EE4 /* Resources */, 3C04B97117E537708993B649 /* [CP] Embed Pods Frameworks */, - 80407DAF4353353A748FD4DC /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -247,7 +289,7 @@ C53372531E26155D004ECBCF /* Frameworks */, C53372541E26155D004ECBCF /* Resources */, 5DD0265501157C9DFB18D487 /* [CP] Embed Pods Frameworks */, - 3E7D40D093AC662DD66ED84E /* [CP] Copy Pods Resources */, + 8839DF70215C08D90021A85A /* ShellScript */, ); buildRules = ( ); @@ -265,16 +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; }; }; @@ -284,6 +328,7 @@ developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( + English, en, Base, ); @@ -325,28 +370,22 @@ 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] 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-DBClientTests/Pods-DBClientTests-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3E7D40D093AC662DD66ED84E /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - ); - 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 */ = { @@ -355,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 */ = { @@ -370,29 +418,34 @@ 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"; + 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; }; - 80407DAF4353353A748FD4DC /* [CP] Copy Pods Resources */ = { + 8839DF70215C08D90021A85A /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( ); - name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-DBClientTests/Pods-DBClientTests-resources.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "\n"; }; BF151D4AF0AD5B6D29F45416 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -400,13 +453,16 @@ 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_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"; + 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 */ @@ -416,16 +472,25 @@ 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 */, - B8477E9C1E4DC0EA00608B78 /* CreateTests.swift in Sources */, - B8477EA01E4DC0EA00608B78 /* UpdateTests.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 */, - B88711421E5211AF00189D40 /* ObservableTests.swift in Sources */, + B8D2D446217F36B40069CC57 /* DBClientRealmTest.swift in Sources */, + B8D2D43E217F35260069CC57 /* RealmUpdateTests.swift in Sources */, + B8D2D430217F35200069CC57 /* CoreDataUpsertTests.swift in Sources */, B8477EA11E4DC0EA00608B78 /* User+Comparable.swift in Sources */, - B8477E9D1E4DC0EA00608B78 /* DeleteTests.swift in Sources */, - B8477E9F1E4DC0EA00608B78 /* FetchTests.swift in Sources */, - B8E447AB1E54916E0078D81A /* UpsertTests.swift in Sources */, - B8477E9E1E4DC0EA00608B78 /* ExecuteTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -486,7 +551,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Debug; @@ -500,7 +565,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_BUNDLE_IDENTIFIER = com.yalantis.DBClientTests; PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 3.0; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; }; name = Release; @@ -514,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; @@ -564,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; @@ -607,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; }; @@ -620,7 +701,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 = Release; }; 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/DBClientInjector.swift b/Example/Example/DBClientInjector.swift index 2fa98bb..28a2558 100644 --- a/Example/Example/DBClientInjector.swift +++ b/Example/Example/DBClientInjector.swift @@ -11,30 +11,17 @@ import DBClient import RealmSwift private struct DBClientInjector { - + static let coreDataClient: DBClient = CoreDataDBClient(forModel: "Users") - - static let realmClient: DBClient = { - let realm: Realm - do { - realm = try Realm() - } catch { - fatalError(error.localizedDescription) - } - return RealmDBClient(realm: realm) - }() - + 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 7fc937b..aaf4903 100644 --- a/Example/Example/DetailViewController.swift +++ b/Example/Example/DetailViewController.swift @@ -23,8 +23,8 @@ class DetailViewController: UIViewController, DBClientInjectable { @IBAction private func saveButtonAction() { detailItem.name = userNameTextField.text ?? "" - dbClient.update(detailItem).continueOnSuccessWith(.mainThread) { _ in - self.navigationController?.popViewController(animated: true) + dbClient.update(detailItem) { [weak self] _ in + self?.navigationController?.popViewController(animated: true) } } diff --git a/Example/Example/MasterViewController.swift b/Example/Example/MasterViewController.swift index 5aab29b..de99bcf 100644 --- a/Example/Example/MasterViewController.swift +++ b/Example/Example/MasterViewController.swift @@ -9,7 +9,7 @@ import UIKit import DBClient -class MasterViewController: UITableViewController, DBClientInjectable { +final class MasterViewController: UITableViewController, DBClientInjectable { fileprivate var objects = [User]() @@ -18,11 +18,11 @@ class MasterViewController: UITableViewController, DBClientInjectable { override func viewDidLoad() { super.viewDidLoad() - let observable = dbClient.observable(for: FetchRequest(sortDescriptor: NSSortDescriptor(key: "name", ascending: true))) + let observable = dbClient.observable(for: FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])) + userChangesObservable = observable observable.observe { [weak self] changeSet in self?.observeChanges(changeSet) } - userChangesObservable = observable navigationItem.leftBarButtonItem = editButtonItem } @@ -44,11 +44,10 @@ class MasterViewController: UITableViewController, DBClientInjectable { // MARK: - Actions @IBAction private func addObject(_ sender: Any) { - dbClient.insert(User.createRandom()) + dbClient.insert(User.createRandom()) { _ in } } private func observeChanges(_ changeSet: ObservableChange) { - switch changeSet { case .initial(let initial): objects.append(contentsOf: initial) @@ -73,7 +72,6 @@ class MasterViewController: UITableViewController, DBClientInjectable { print("Got an error: \(error)") } } - } // MARK: - Table View @@ -96,13 +94,12 @@ extension MasterViewController { return true } - override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { + 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 15ede89..1c40091 100644 --- a/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift +++ b/Example/Example/Models/CoreData/ManagedUser+CoreDataClass.swift @@ -13,4 +13,3 @@ import CoreData public class ManagedUser: NSManagedObject { } - diff --git a/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift b/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift index 49fd4d8..e449d79 100644 --- a/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift +++ b/Example/Example/Models/CoreData/ManagedUser+CoreDataProperties.swift @@ -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/Realm/ObjectUser.swift b/Example/Example/Models/Realm/ObjectUser.swift index 1efbe5d..ad3b715 100644 --- a/Example/Example/Models/Realm/ObjectUser.swift +++ b/Example/Example/Models/Realm/ObjectUser.swift @@ -15,7 +15,7 @@ class ObjectUser: Object { 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/User.swift b/Example/Example/Models/User.swift index d10e438..c84d59e 100644 --- a/Example/Example/Models/User.swift +++ b/Example/Example/Models/User.swift @@ -19,6 +19,9 @@ class User { self.name = name } + func mutate() { + name = String(name.reversed()) + } } extension User: Stored { @@ -30,7 +33,6 @@ extension User: Stored { public var valueOfPrimaryKey: CVarArg? { return id } - } extension User { @@ -41,5 +43,4 @@ extension User { return user } - } diff --git a/Example/Podfile b/Example/Podfile index 6fc1b17..1707261 100644 --- a/Example/Podfile +++ b/Example/Podfile @@ -1,16 +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 \ No newline at end of file + pod 'DBClient/CoreData', :path => '../' +end diff --git a/Example/Podfile.lock b/Example/Podfile.lock index 0ae39de..65ae243 100644 --- a/Example/Podfile.lock +++ b/Example/Podfile.lock @@ -1,32 +1,40 @@ PODS: - - Bolts-Swift (1.3.0) - - DBClient/Core (0.4): - - Bolts-Swift (~> 1.3.0) - - DBClient/CoreData (0.4): + - DBClient/Core (1.4.2): + - YALResult (= 1.4) + - DBClient/CoreData (1.4.2): - DBClient/Core - - DBClient/Realm (0.4): + - 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: a9404be1db68eff3a9d04d1ea61043e00e39081d - Realm: efe855f4d977c8ce5a82d3116d9f1ff155a6550c - RealmSwift: 17d6ee30b6f9df86364408c2197492e33bfea567 + DBClient: 6833b6f3abb9bbd90dc3289621179f3fad102c0c + Realm: 9b834e1be6062f544805252c812348872dc5d4ed + RealmSwift: 8a41886f8ab6efef9eb8df97de2f2bb911561a79 + YALResult: 26915691cdd19269936336d6f28e1a015c64175e -PODFILE CHECKSUM: e1b66d1226f9ec10a94bb2534dbf6b4ca65549de +PODFILE CHECKSUM: 01e2cf56f70348c45b9e7248a505591dd86d210b -COCOAPODS: 1.2.1.beta.1 +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 278b4a3..4bc75a3 100644 --- a/README.md +++ b/README.md @@ -1,31 +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', '~> 0.3' +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) +} ``` -Wrapper for CoreData: +The last one converts your model to realm's object: +``` +func toRealmObject() -> Object { + let user = ObjectUser() + user.id = id + user.name = name -```ruby -pod 'DBClient/CoreData', '~> 0.3' + return user +} ``` -Wrapper for Realm: +### CoreData + +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 +} +``` + +The next method determines whether given object equal to current: +``` +func isPrimaryValueEqualTo(value: Any) -> Bool { + if let value = value as? String { + return value == id + } + + return false +} +``` + +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 + -```ruby -pod 'DBClient/Realm', '~> 0.3' -``` \ No newline at end of file +| 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 |