diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..3bb76e5 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Dollar.swift"] + path = Dollar.swift + url = https://github.com/ankurp/Dollar.swift.git diff --git a/Cartfile.resolved b/Cartfile.resolved index b8dd35b..91fc81e 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,5 @@ github "krzyzanowskim/CryptoSwift" "0.1.1" +github "ankurp/Dollar.swift" "4.0.1" github "Quick/Nimble" "v2.0.0-rc.3" github "Quick/Quick" "v0.6.0" github "tidwall/SwiftWebSocket" "v2.3.0" diff --git a/SwiftDDP.podspec b/SwiftDDP.podspec index 7719787..8f232c2 100644 --- a/SwiftDDP.podspec +++ b/SwiftDDP.podspec @@ -19,6 +19,7 @@ Pod::Spec.new do |s| s.dependency 'CryptoSwift' s.dependency 'SwiftWebSocket' + s.dependency 'Dollar' s.dependency 'XCGLogger' end diff --git a/SwiftDDP.xcodeproj/project.pbxproj b/SwiftDDP.xcodeproj/project.pbxproj index 1746641..df3c6b4 100644 --- a/SwiftDDP.xcodeproj/project.pbxproj +++ b/SwiftDDP.xcodeproj/project.pbxproj @@ -25,7 +25,11 @@ D0C71B561BC172F40089B6CE /* Meteor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C71B551BC172F40089B6CE /* Meteor.swift */; settings = {ASSET_TAGS = (); }; }; D0C71B581BC173030089B6CE /* SwiftMeteor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C71B571BC173030089B6CE /* SwiftMeteor.swift */; settings = {ASSET_TAGS = (); }; }; D0C71B5B1BC174280089B6CE /* Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C71B5A1BC174280089B6CE /* Data.swift */; settings = {ASSET_TAGS = (); }; }; +<<<<<<< HEAD D0F6C99D1BFFA04600A6CB70 /* EJSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F6C99C1BFFA04600A6CB70 /* EJSON.swift */; settings = {ASSET_TAGS = (); }; }; +======= + D0F6C9221BFE97C800A6CB70 /* Dollar.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0F6C91F1BFE97BC00A6CB70 /* Dollar.framework */; }; +>>>>>>> 75ac27cd6b5f36231c0ad7b65baed7b0cf65f997 /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -36,6 +40,20 @@ remoteGlobalIDString = D02A71DB1BBEFBCA00940C17; remoteInfo = SwiftDDP; }; + D0F6C91E1BFE97BC00A6CB70 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0F6C9191BFE97BC00A6CB70 /* Dollar.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 92E0D03C19467C67002ACC3D; + remoteInfo = Dollar; + }; + D0F6C9201BFE97BC00A6CB70 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D0F6C9191BFE97BC00A6CB70 /* Dollar.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 92E6686F19F09C6400BB4FB8; + remoteInfo = DollarTests; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -80,7 +98,11 @@ D0C71B551BC172F40089B6CE /* Meteor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Meteor.swift; sourceTree = ""; }; D0C71B571BC173030089B6CE /* SwiftMeteor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftMeteor.swift; sourceTree = ""; }; D0C71B5A1BC174280089B6CE /* Data.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Data.swift; sourceTree = ""; }; +<<<<<<< HEAD D0F6C99C1BFFA04600A6CB70 /* EJSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EJSON.swift; sourceTree = ""; }; +======= + D0F6C9191BFE97BC00A6CB70 /* Dollar.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Dollar.xcodeproj; path = Dollar.swift/Dollar/Dollar.xcodeproj; sourceTree = ""; }; +>>>>>>> 75ac27cd6b5f36231c0ad7b65baed7b0cf65f997 /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,6 +110,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D0F6C9221BFE97C800A6CB70 /* Dollar.framework in Frameworks */, D02A72281BBF01E900940C17 /* CryptoSwift.framework in Frameworks */, D02A722A1BBF01ED00940C17 /* SwiftWebSocket.framework in Frameworks */, D02A722C1BBF01EF00940C17 /* XCGLogger.framework in Frameworks */, @@ -149,6 +172,7 @@ D02A72341BBF02C200940C17 /* Frameworks */ = { isa = PBXGroup; children = ( + D0F6C9191BFE97BC00A6CB70 /* Dollar.xcodeproj */, D02A72321BBF02B900940C17 /* XCGLogger.framework.dSYM */, D02A72301BBF02B400940C17 /* SwiftWebSocket.framework.dSYM */, D02A722E1BBF02AA00940C17 /* CryptoSwift.framework.dSYM */, @@ -173,6 +197,15 @@ path = SwiftDDPTests; sourceTree = ""; }; + D0F6C91A1BFE97BC00A6CB70 /* Products */ = { + isa = PBXGroup; + children = ( + D0F6C91F1BFE97BC00A6CB70 /* Dollar.framework */, + D0F6C9211BFE97BC00A6CB70 /* DollarTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -252,6 +285,12 @@ mainGroup = D02A71D21BBEFBCA00940C17; productRefGroup = D02A71DD1BBEFBCA00940C17 /* Products */; projectDirPath = ""; + projectReferences = ( + { + ProductGroup = D0F6C91A1BFE97BC00A6CB70 /* Products */; + ProjectRef = D0F6C9191BFE97BC00A6CB70 /* Dollar.xcodeproj */; + }, + ); projectRoot = ""; targets = ( D02A71DB1BBEFBCA00940C17 /* SwiftDDP */, @@ -260,6 +299,23 @@ }; /* End PBXProject section */ +/* Begin PBXReferenceProxy section */ + D0F6C91F1BFE97BC00A6CB70 /* Dollar.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Dollar.framework; + remoteRef = D0F6C91E1BFE97BC00A6CB70 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + D0F6C9211BFE97BC00A6CB70 /* DollarTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = wrapper.cfbundle; + path = DollarTests.xctest; + remoteRef = D0F6C9201BFE97BC00A6CB70 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + /* Begin PBXResourcesBuildPhase section */ D02A71DA1BBEFBCA00940C17 /* Resources */ = { isa = PBXResourcesBuildPhase; diff --git a/SwiftDDP/Meteor.swift b/SwiftDDP/Meteor.swift index aaba706..005b938 100644 --- a/SwiftDDP/Meteor.swift +++ b/SwiftDDP/Meteor.swift @@ -32,16 +32,12 @@ case InternalServerError = "500" } */ -protocol MeteorCollectionType { +public protocol MeteorCollectionType { func documentWasAdded(collection:String, id:String, fields:NSDictionary?) func documentWasChanged(collection:String, id:String, fields:NSDictionary?, cleared:[String]?) func documentWasRemoved(collection:String, id:String) } -protocol MeteorDocument { - var id:String { get } -} - /** Meteor is a class to simplify communicating with and consuming MeteorJS server services */ @@ -270,31 +266,42 @@ public class Meteor { } } -/** -MeteorCollection is a class created to provide a base class and api for integrating SwiftDDP with persistence stores. MeteorCollection -should generally be subclassed, with the methods documentWasAdded, documentWasChanged and documentWasRemoved facilitating communicating -with the datastore. -*/ +public class MeteorDocument: NSObject { + + var id:String + + required public init(id: String, fields: NSDictionary?) { + self.id = id + super.init() + + if let properties = fields { + for (key,value) in properties { + self.setValue(value, forKey: key as! String) + } + } + } + + public func update(fields: NSDictionary?, cleared: [String]?) { + if let properties = fields { + for (key,value) in properties { + self.setValue(value, forKey: key as! String) + } + } + + if let deletions = cleared { + for property in deletions { + self.setNilValueForKey(property) + } + } + } + +} -public class MeteorCollection: NSObject, MeteorCollectionType { +public class AbstractCollection: NSObject, MeteorCollectionType { public var name:String public let client = Meteor.client - private var documents = [String:MeteorDocument]() - - // Alternative API to subclassing - // Can also set these closures to modify behavior on added, changed, removed - internal var onAdded:((collection:String, id:String, fields:NSDictionary?) -> ())? - internal var onChanged:((collection:String, id:String, fields:NSDictionary?, cleared:[String]?) -> ())? - internal var onRemoved:((collection:String, id:String) -> ())? - - /** - Initializes a MeteorCollection object - - - parameter name: The string name of the collection (must match the name of the collection on the server) - */ - public init(name:String) { self.name = name super.init() @@ -304,6 +311,75 @@ public class MeteorCollection: NSObject, MeteorCollectionType { deinit { Meteor.collections[name] = nil } + + /** + Invoked when a document has been sent from the server. + + - parameter collection: the string name of the collection to which the document belongs + - parameter id: the string unique id that identifies the document on the server + - parameter fields: an optional NSDictionary with the documents properties + */ + + public func documentWasAdded(collection:String, id:String, fields:NSDictionary?) {} + + /** + Invoked when a document has been changed on the server. + + - parameter collection: the string name of the collection to which the document belongs + - parameter id: the string unique id that identifies the document on the server + - parameter fields: an optional NSDictionary with the documents properties + - parameter cleared: Optional array of strings (field names to delete) + */ + + public func documentWasChanged(collection:String, id:String, fields:NSDictionary?, cleared:[String]?) {} + + /** + Invoked when a document has been removed on the server. + + - parameter collection: the string name of the collection to which the document belongs + - parameter id: the string unique id that identifies the document on the server + */ + + public func documentWasRemoved(collection:String, id:String) {} + +} + + + +/** +MeteorCollection provides basic persistence as well as an api for integrating SwiftDDP with persistence stores. MeteorCollection +should generally be subclassed, with the methods documentWasAdded, documentWasChanged and documentWasRemoved facilitating communicating +with the datastore. +*/ + +// MeteorCollectionType protocol declaration is necessary +public class MeteorCollection: AbstractCollection { + + + var documents = [String:T]() + + var sorted:[T] { + return Array(documents.values).sort({ $0.id > $1.id }) + } + + /** + Returns the number of documents in the collection + */ + + var count:Int { + return documents.count + } + + /** + Initializes a MeteorCollection object + + - parameter name: The string name of the collection (must match the name of the collection on the server) + */ + + private func sorted(property:String) -> [T] { + let values = Array(documents.values) + return values.sort({ $0.id > $1.id }) + } /** Invoked when a document has been sent from the server. @@ -313,8 +389,10 @@ public class MeteorCollection: NSObject, MeteorCollectionType { - parameter fields: an optional NSDictionary with the documents properties */ - public func documentWasAdded(collection:String, id:String, fields:NSDictionary?) { - if let added = onAdded { added(collection: collection, id: id, fields:fields) } + public override func documentWasAdded(collection:String, id:String, fields:NSDictionary?) { + let document = T(id: id, fields: fields) + documents[id] = document + } /** @@ -326,8 +404,12 @@ public class MeteorCollection: NSObject, MeteorCollectionType { - parameter cleared: Optional array of strings (field names to delete) */ - public func documentWasChanged(collection:String, id:String, fields:NSDictionary?, cleared:[String]?) { - if let changed = onChanged { changed(collection:collection, id:id, fields:fields, cleared:cleared) } + public override func documentWasChanged(collection:String, id:String, fields:NSDictionary?, cleared:[String]?) { + if let document = documents[id] { + document.update(fields, cleared: cleared) + documents[id] = document + } + } /** @@ -337,8 +419,10 @@ public class MeteorCollection: NSObject, MeteorCollectionType { - parameter id: the string unique id that identifies the document on the server */ - public func documentWasRemoved(collection:String, id:String) { - if let removed = onRemoved { removed(collection:collection, id:id) } + public override func documentWasRemoved(collection:String, id:String) { + if let _ = documents[id] { + documents[id] = nil + } } } diff --git a/SwiftDDPTests/Data.swift b/SwiftDDPTests/Data.swift index cd81fc1..4c3b119 100644 --- a/SwiftDDPTests/Data.swift +++ b/SwiftDDPTests/Data.swift @@ -1,6 +1,6 @@ import Foundation -import SwiftDDP +@testable import SwiftDDP // // @@ -8,6 +8,13 @@ import SwiftDDP // // +class Document: MeteorDocument { + + var state:String? + var city:String? + +} + // *** methods that are tested against a server are tested against the url below *** let url = "ws://swiftddp.meteor.com/websocket" diff --git a/SwiftDDPTests/SwiftMeteor.swift b/SwiftDDPTests/SwiftMeteor.swift index 8dca9a2..1b090e5 100644 --- a/SwiftDDPTests/SwiftMeteor.swift +++ b/SwiftDDPTests/SwiftMeteor.swift @@ -8,7 +8,7 @@ class MeteorTest: QuickSpec { override func spec() { let client = Meteor.client - let collection = MeteorCollection(name: "test-collection") + let collection = MeteorCollection(name: "test-collection") describe("Collections") { /* @@ -24,40 +24,37 @@ class MeteorTest: QuickSpec { describe("Document methods send notifications") { it("sends a message when a document is added") { - var _id:String! - - collection.onAdded = {collection, id, fields in - if (id == "2gAMzqvE8K8kBWK8F") { _id = id } - } try! client.ddpMessageHandler(added[0]) - expect(_id).toEventuallyNot(beNil()) - expect(_id).toEventually(equal("2gAMzqvE8K8kBWK8F")) + + print("Collection -> \(collection.documents)") + + expect(collection.documents["2gAMzqvE8K8kBWK8F"]).toEventuallyNot(beNil()) + expect(collection.documents["2gAMzqvE8K8kBWK8F"]?.city).toEventually(equal("Boston")) } it("sends a message when a document is removed") { - var _id:String! - collection.onRemoved = {collection, id in - if (id == "2gAMzqvE8K8kBWK8F") { _id = id } - } + try! client.ddpMessageHandler(added[1]) + expect(collection.documents["ByuwhKPGuLru8h4TT"]).toEventuallyNot(beNil()) + expect(collection.documents["ByuwhKPGuLru8h4TT"]!.city).toEventually(equal("Truro")) - try! client.ddpMessageHandler(removed[0]) - expect(_id).toEventuallyNot(beNil()) - expect(_id).toEventually(equal("2gAMzqvE8K8kBWK8F")) + try! client.ddpMessageHandler(removed[1]) + expect(collection.documents["ByuwhKPGuLru8h4TT"]).toEventually(beNil()) } + it("sends a message when a document is updated") { - var _id:String! - collection.onChanged = {collection, id, fields, cleared in - if (id == "2gAMzqvE8K8kBWK8F") { _id = id } - } + try! client.ddpMessageHandler(added[2]) + expect(collection.documents["AGX6vyxCJtjqdxbFH"]).toEventuallyNot(beNil()) + expect(collection.documents["AGX6vyxCJtjqdxbFH"]!.city).toEventually(equal("Austin")) - try! client.ddpMessageHandler(changed[0]) - expect(_id).toEventuallyNot(beNil()) - expect(_id).toEventually(equal("2gAMzqvE8K8kBWK8F")) + try! client.ddpMessageHandler(changed[2]) + expect(collection.documents["AGX6vyxCJtjqdxbFH"]!.city).toEventually(equal("Houston")) + } + } } } \ No newline at end of file