├── LICENSE.md ├── README.md ├── SwiftRedis.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcuserdata │ └── ron.xcuserdatad │ └── xcschemes │ ├── SwiftRedis.xcscheme │ └── xcschememanagement.plist ├── SwiftRedis ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Info.plist ├── RedisBuffer.swift ├── RedisCommand.swift ├── RedisConnection.swift ├── RedisInterface.swift ├── RedisResponse.swift ├── RedisResponseParser.swift └── ViewController.swift └── SwiftRedisTests ├── ConnectionParams-example.swift ├── Info.plist └── SwiftRedisTests.swift /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ron Perry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift 3.0](https://img.shields.io/badge/Swift-3.0-green.svg?style=flat)](https://swift.org) 2 | 3 | # SwiftRedis 4 | 5 | Asynchronous Redis client in Swift 3. 6 | 7 | Currently supports the following: 8 | * Asynchronous operation 9 | * Has a "generic" mechanism that allows sending any Redis command that returns a result 10 | * Utility functions to send GET, SET, AUTH commands 11 | * Support for PubSub commands: PUBLISH, SUBSCRIBE 12 | * Mechanism to extend with more commands 13 | * Provides a RedisResponse class which encapsulates possible responses from Redis: String, Int, Data (BulkString), Error, Array 14 | 15 | Blatanly missing: 16 | * Error checking still needs work. 17 | * Source code definitely needs more documentation 18 | * SSL connectivity not tested. 19 | * A single RedisInterface object can only subscribe to a single channel (workaround: use two RedisInterface objects) 20 | 21 | 22 | ## Usage 23 | 24 | Add the SwiftRedis source files to your project. Create a RedisInterface object, call connect() and then issue Redis commands. 25 | 26 | 27 | ## Simple example 28 | 29 | ```swift 30 | let redis = RedisInterface(host: , port: , auth: ) 31 | 32 | // Queue a request to initiate a connection. 33 | // Once a connection is established, an AUTH command will be issued with the auth parameters specified above. 34 | redis.connect() 35 | 36 | // Queue a request to set a value for a key in the Redis database. This command will only 37 | // execute after the connection is established and authenticated. 38 | redis.setValueForKey("some:key", stringValue: "a value", completionHandler: { success, cmd in 39 | // this completion handler will be executed after the SET command returns 40 | if success { 41 | print("value stored successfully") 42 | } else { 43 | print("value was not stored") 44 | } 45 | }) 46 | 47 | // Queue a request to get the value of a key in the Redis database. This command will only 48 | // execute after the previous command is complete. 49 | redis.getValueForKey("some:key", completionHandler: { success, key, data, cmd in 50 | if success { 51 | print("the stored data for \(key) is \(data!.stringVal)") 52 | } else { 53 | print("could not get value for \(key)") 54 | } 55 | }) 56 | 57 | // Queue a QUIT command (the connection will close when the QUIT command completes) 58 | redis.quit({success, cmd in 59 | print("QUIT command completed") 60 | }) 61 | ``` 62 | 63 | 64 | 65 | 66 | 67 | ## For more info 68 | 69 | To understand high-level usage: check the [Unit Tests](https://github.com/ronp001/SwiftRedis/blob/master/SwiftRedisTests/SwiftRedisTests.swift) for usage of the RedisInterface() class. 70 | 71 | To understand internals and learn how to extend this: examine the Unit Tests for the lower-level classes (RedisConnection, RedisCommand, RedisParser, etc.) 72 | 73 | Note that in order to run the unit tests, you'll need to specifiy host/port/auth details in the ConnectionParams class. 74 | 75 | 76 | -------------------------------------------------------------------------------- /SwiftRedis.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | E491E8151C0AA2800073E014 /* LICENSE.md in Resources */ = {isa = PBXBuildFile; fileRef = E491E8131C0AA2800073E014 /* LICENSE.md */; }; 11 | E491E8161C0AA2800073E014 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = E491E8141C0AA2800073E014 /* README.md */; }; 12 | E49804C21BF07248003D5539 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804C11BF07248003D5539 /* AppDelegate.swift */; }; 13 | E49804C41BF07248003D5539 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804C31BF07248003D5539 /* ViewController.swift */; }; 14 | E49804C71BF07248003D5539 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E49804C51BF07248003D5539 /* Main.storyboard */; }; 15 | E49804C91BF07248003D5539 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E49804C81BF07248003D5539 /* Assets.xcassets */; }; 16 | E49804CC1BF07248003D5539 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E49804CA1BF07248003D5539 /* LaunchScreen.storyboard */; }; 17 | E49804D71BF07248003D5539 /* SwiftRedisTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804D61BF07248003D5539 /* SwiftRedisTests.swift */; }; 18 | E49804E81BF0727D003D5539 /* RedisBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E11BF0727D003D5539 /* RedisBuffer.swift */; }; 19 | E49804E91BF0727D003D5539 /* RedisCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E21BF0727D003D5539 /* RedisCommand.swift */; }; 20 | E49804EA1BF0727D003D5539 /* RedisConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E31BF0727D003D5539 /* RedisConnection.swift */; }; 21 | E49804EB1BF0727D003D5539 /* RedisInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E41BF0727D003D5539 /* RedisInterface.swift */; }; 22 | E49804EC1BF0727D003D5539 /* RedisResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E51BF0727D003D5539 /* RedisResponse.swift */; }; 23 | E49804ED1BF0727D003D5539 /* RedisResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804E61BF0727D003D5539 /* RedisResponseParser.swift */; }; 24 | E49804F01BF072EB003D5539 /* ConnectionParams-example.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804EF1BF072EB003D5539 /* ConnectionParams-example.swift */; }; 25 | E49804FB1BF19774003D5539 /* ConnectionParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = E49804FA1BF19774003D5539 /* ConnectionParams.swift */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | E49804D31BF07248003D5539 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = E49804B61BF07248003D5539 /* Project object */; 32 | proxyType = 1; 33 | remoteGlobalIDString = E49804BD1BF07248003D5539; 34 | remoteInfo = SwiftRedis; 35 | }; 36 | /* End PBXContainerItemProxy section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | E491E8131C0AA2800073E014 /* LICENSE.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = LICENSE.md; sourceTree = ""; }; 40 | E491E8141C0AA2800073E014 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 41 | E49804BE1BF07248003D5539 /* SwiftRedis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftRedis.app; sourceTree = BUILT_PRODUCTS_DIR; }; 42 | E49804C11BF07248003D5539 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 43 | E49804C31BF07248003D5539 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 44 | E49804C61BF07248003D5539 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 45 | E49804C81BF07248003D5539 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 46 | E49804CB1BF07248003D5539 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 47 | E49804CD1BF07248003D5539 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 48 | E49804D21BF07248003D5539 /* SwiftRedisTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftRedisTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 49 | E49804D61BF07248003D5539 /* SwiftRedisTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftRedisTests.swift; sourceTree = ""; }; 50 | E49804D81BF07248003D5539 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | E49804E11BF0727D003D5539 /* RedisBuffer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisBuffer.swift; sourceTree = ""; }; 52 | E49804E21BF0727D003D5539 /* RedisCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisCommand.swift; sourceTree = ""; }; 53 | E49804E31BF0727D003D5539 /* RedisConnection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisConnection.swift; sourceTree = ""; }; 54 | E49804E41BF0727D003D5539 /* RedisInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisInterface.swift; sourceTree = ""; }; 55 | E49804E51BF0727D003D5539 /* RedisResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisResponse.swift; sourceTree = ""; }; 56 | E49804E61BF0727D003D5539 /* RedisResponseParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedisResponseParser.swift; sourceTree = ""; }; 57 | E49804EF1BF072EB003D5539 /* ConnectionParams-example.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ConnectionParams-example.swift"; sourceTree = ""; }; 58 | E49804FA1BF19774003D5539 /* ConnectionParams.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ConnectionParams.swift; path = ../../ConnectionParams.swift; sourceTree = ""; }; 59 | /* End PBXFileReference section */ 60 | 61 | /* Begin PBXFrameworksBuildPhase section */ 62 | E49804BB1BF07248003D5539 /* Frameworks */ = { 63 | isa = PBXFrameworksBuildPhase; 64 | buildActionMask = 2147483647; 65 | files = ( 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | E49804CF1BF07248003D5539 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | /* End PBXFrameworksBuildPhase section */ 77 | 78 | /* Begin PBXGroup section */ 79 | E49804B51BF07248003D5539 = { 80 | isa = PBXGroup; 81 | children = ( 82 | E491E8131C0AA2800073E014 /* LICENSE.md */, 83 | E491E8141C0AA2800073E014 /* README.md */, 84 | E49804C01BF07248003D5539 /* SwiftRedis */, 85 | E49804D51BF07248003D5539 /* SwiftRedisTests */, 86 | E49804BF1BF07248003D5539 /* Products */, 87 | ); 88 | sourceTree = ""; 89 | }; 90 | E49804BF1BF07248003D5539 /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | E49804BE1BF07248003D5539 /* SwiftRedis.app */, 94 | E49804D21BF07248003D5539 /* SwiftRedisTests.xctest */, 95 | ); 96 | name = Products; 97 | sourceTree = ""; 98 | }; 99 | E49804C01BF07248003D5539 /* SwiftRedis */ = { 100 | isa = PBXGroup; 101 | children = ( 102 | E49804E11BF0727D003D5539 /* RedisBuffer.swift */, 103 | E49804E21BF0727D003D5539 /* RedisCommand.swift */, 104 | E49804E31BF0727D003D5539 /* RedisConnection.swift */, 105 | E49804E41BF0727D003D5539 /* RedisInterface.swift */, 106 | E49804E51BF0727D003D5539 /* RedisResponse.swift */, 107 | E49804E61BF0727D003D5539 /* RedisResponseParser.swift */, 108 | E49804C11BF07248003D5539 /* AppDelegate.swift */, 109 | E49804C31BF07248003D5539 /* ViewController.swift */, 110 | E49804C51BF07248003D5539 /* Main.storyboard */, 111 | E49804C81BF07248003D5539 /* Assets.xcassets */, 112 | E49804CA1BF07248003D5539 /* LaunchScreen.storyboard */, 113 | E49804CD1BF07248003D5539 /* Info.plist */, 114 | ); 115 | path = SwiftRedis; 116 | sourceTree = ""; 117 | }; 118 | E49804D51BF07248003D5539 /* SwiftRedisTests */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | E49804FA1BF19774003D5539 /* ConnectionParams.swift */, 122 | E49804D61BF07248003D5539 /* SwiftRedisTests.swift */, 123 | E49804EF1BF072EB003D5539 /* ConnectionParams-example.swift */, 124 | E49804D81BF07248003D5539 /* Info.plist */, 125 | ); 126 | path = SwiftRedisTests; 127 | sourceTree = ""; 128 | }; 129 | /* End PBXGroup section */ 130 | 131 | /* Begin PBXNativeTarget section */ 132 | E49804BD1BF07248003D5539 /* SwiftRedis */ = { 133 | isa = PBXNativeTarget; 134 | buildConfigurationList = E49804DB1BF07248003D5539 /* Build configuration list for PBXNativeTarget "SwiftRedis" */; 135 | buildPhases = ( 136 | E49804BA1BF07248003D5539 /* Sources */, 137 | E49804BB1BF07248003D5539 /* Frameworks */, 138 | E49804BC1BF07248003D5539 /* Resources */, 139 | ); 140 | buildRules = ( 141 | ); 142 | dependencies = ( 143 | ); 144 | name = SwiftRedis; 145 | productName = SwiftRedis; 146 | productReference = E49804BE1BF07248003D5539 /* SwiftRedis.app */; 147 | productType = "com.apple.product-type.application"; 148 | }; 149 | E49804D11BF07248003D5539 /* SwiftRedisTests */ = { 150 | isa = PBXNativeTarget; 151 | buildConfigurationList = E49804DE1BF07248003D5539 /* Build configuration list for PBXNativeTarget "SwiftRedisTests" */; 152 | buildPhases = ( 153 | E49804CE1BF07248003D5539 /* Sources */, 154 | E49804CF1BF07248003D5539 /* Frameworks */, 155 | E49804D01BF07248003D5539 /* Resources */, 156 | ); 157 | buildRules = ( 158 | ); 159 | dependencies = ( 160 | E49804D41BF07248003D5539 /* PBXTargetDependency */, 161 | ); 162 | name = SwiftRedisTests; 163 | productName = SwiftRedisTests; 164 | productReference = E49804D21BF07248003D5539 /* SwiftRedisTests.xctest */; 165 | productType = "com.apple.product-type.bundle.unit-test"; 166 | }; 167 | /* End PBXNativeTarget section */ 168 | 169 | /* Begin PBXProject section */ 170 | E49804B61BF07248003D5539 /* Project object */ = { 171 | isa = PBXProject; 172 | attributes = { 173 | LastSwiftUpdateCheck = 0710; 174 | LastUpgradeCheck = 0800; 175 | ORGANIZATIONNAME = "Ron Perry"; 176 | TargetAttributes = { 177 | E49804BD1BF07248003D5539 = { 178 | CreatedOnToolsVersion = 7.1; 179 | DevelopmentTeam = 4TRCE5W7BE; 180 | LastSwiftMigration = 0800; 181 | }; 182 | E49804D11BF07248003D5539 = { 183 | CreatedOnToolsVersion = 7.1; 184 | DevelopmentTeam = 4TRCE5W7BE; 185 | LastSwiftMigration = 0800; 186 | TestTargetID = E49804BD1BF07248003D5539; 187 | }; 188 | }; 189 | }; 190 | buildConfigurationList = E49804B91BF07248003D5539 /* Build configuration list for PBXProject "SwiftRedis" */; 191 | compatibilityVersion = "Xcode 3.2"; 192 | developmentRegion = English; 193 | hasScannedForEncodings = 0; 194 | knownRegions = ( 195 | en, 196 | Base, 197 | ); 198 | mainGroup = E49804B51BF07248003D5539; 199 | productRefGroup = E49804BF1BF07248003D5539 /* Products */; 200 | projectDirPath = ""; 201 | projectRoot = ""; 202 | targets = ( 203 | E49804BD1BF07248003D5539 /* SwiftRedis */, 204 | E49804D11BF07248003D5539 /* SwiftRedisTests */, 205 | ); 206 | }; 207 | /* End PBXProject section */ 208 | 209 | /* Begin PBXResourcesBuildPhase section */ 210 | E49804BC1BF07248003D5539 /* Resources */ = { 211 | isa = PBXResourcesBuildPhase; 212 | buildActionMask = 2147483647; 213 | files = ( 214 | E491E8151C0AA2800073E014 /* LICENSE.md in Resources */, 215 | E491E8161C0AA2800073E014 /* README.md in Resources */, 216 | E49804CC1BF07248003D5539 /* LaunchScreen.storyboard in Resources */, 217 | E49804C91BF07248003D5539 /* Assets.xcassets in Resources */, 218 | E49804C71BF07248003D5539 /* Main.storyboard in Resources */, 219 | ); 220 | runOnlyForDeploymentPostprocessing = 0; 221 | }; 222 | E49804D01BF07248003D5539 /* Resources */ = { 223 | isa = PBXResourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | ); 227 | runOnlyForDeploymentPostprocessing = 0; 228 | }; 229 | /* End PBXResourcesBuildPhase section */ 230 | 231 | /* Begin PBXSourcesBuildPhase section */ 232 | E49804BA1BF07248003D5539 /* Sources */ = { 233 | isa = PBXSourcesBuildPhase; 234 | buildActionMask = 2147483647; 235 | files = ( 236 | E49804C41BF07248003D5539 /* ViewController.swift in Sources */, 237 | E49804C21BF07248003D5539 /* AppDelegate.swift in Sources */, 238 | E49804E81BF0727D003D5539 /* RedisBuffer.swift in Sources */, 239 | E49804EC1BF0727D003D5539 /* RedisResponse.swift in Sources */, 240 | E49804EA1BF0727D003D5539 /* RedisConnection.swift in Sources */, 241 | E49804E91BF0727D003D5539 /* RedisCommand.swift in Sources */, 242 | E49804EB1BF0727D003D5539 /* RedisInterface.swift in Sources */, 243 | E49804ED1BF0727D003D5539 /* RedisResponseParser.swift in Sources */, 244 | ); 245 | runOnlyForDeploymentPostprocessing = 0; 246 | }; 247 | E49804CE1BF07248003D5539 /* Sources */ = { 248 | isa = PBXSourcesBuildPhase; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | E49804D71BF07248003D5539 /* SwiftRedisTests.swift in Sources */, 252 | E49804FB1BF19774003D5539 /* ConnectionParams.swift in Sources */, 253 | E49804F01BF072EB003D5539 /* ConnectionParams-example.swift in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXSourcesBuildPhase section */ 258 | 259 | /* Begin PBXTargetDependency section */ 260 | E49804D41BF07248003D5539 /* PBXTargetDependency */ = { 261 | isa = PBXTargetDependency; 262 | target = E49804BD1BF07248003D5539 /* SwiftRedis */; 263 | targetProxy = E49804D31BF07248003D5539 /* PBXContainerItemProxy */; 264 | }; 265 | /* End PBXTargetDependency section */ 266 | 267 | /* Begin PBXVariantGroup section */ 268 | E49804C51BF07248003D5539 /* Main.storyboard */ = { 269 | isa = PBXVariantGroup; 270 | children = ( 271 | E49804C61BF07248003D5539 /* Base */, 272 | ); 273 | name = Main.storyboard; 274 | sourceTree = ""; 275 | }; 276 | E49804CA1BF07248003D5539 /* LaunchScreen.storyboard */ = { 277 | isa = PBXVariantGroup; 278 | children = ( 279 | E49804CB1BF07248003D5539 /* Base */, 280 | ); 281 | name = LaunchScreen.storyboard; 282 | sourceTree = ""; 283 | }; 284 | /* End PBXVariantGroup section */ 285 | 286 | /* Begin XCBuildConfiguration section */ 287 | E49804D91BF07248003D5539 /* Debug */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 292 | CLANG_CXX_LIBRARY = "libc++"; 293 | CLANG_ENABLE_MODULES = YES; 294 | CLANG_ENABLE_OBJC_ARC = YES; 295 | CLANG_WARN_BOOL_CONVERSION = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 303 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 304 | CLANG_WARN_UNREACHABLE_CODE = YES; 305 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 306 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 307 | COPY_PHASE_STRIP = NO; 308 | DEBUG_INFORMATION_FORMAT = dwarf; 309 | ENABLE_STRICT_OBJC_MSGSEND = YES; 310 | ENABLE_TESTABILITY = YES; 311 | GCC_C_LANGUAGE_STANDARD = gnu99; 312 | GCC_DYNAMIC_NO_PIC = NO; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_OPTIMIZATION_LEVEL = 0; 315 | GCC_PREPROCESSOR_DEFINITIONS = ( 316 | "DEBUG=1", 317 | "$(inherited)", 318 | ); 319 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 320 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 321 | GCC_WARN_UNDECLARED_SELECTOR = YES; 322 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 323 | GCC_WARN_UNUSED_FUNCTION = YES; 324 | GCC_WARN_UNUSED_VARIABLE = YES; 325 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 326 | MTL_ENABLE_DEBUG_INFO = YES; 327 | ONLY_ACTIVE_ARCH = YES; 328 | SDKROOT = iphoneos; 329 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 330 | TARGETED_DEVICE_FAMILY = "1,2"; 331 | }; 332 | name = Debug; 333 | }; 334 | E49804DA1BF07248003D5539 /* Release */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ALWAYS_SEARCH_USER_PATHS = NO; 338 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 339 | CLANG_CXX_LIBRARY = "libc++"; 340 | CLANG_ENABLE_MODULES = YES; 341 | CLANG_ENABLE_OBJC_ARC = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INFINITE_RECURSION = YES; 348 | CLANG_WARN_INT_CONVERSION = YES; 349 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 350 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 351 | CLANG_WARN_UNREACHABLE_CODE = YES; 352 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 353 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 354 | COPY_PHASE_STRIP = NO; 355 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 356 | ENABLE_NS_ASSERTIONS = NO; 357 | ENABLE_STRICT_OBJC_MSGSEND = YES; 358 | GCC_C_LANGUAGE_STANDARD = gnu99; 359 | GCC_NO_COMMON_BLOCKS = YES; 360 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 361 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 362 | GCC_WARN_UNDECLARED_SELECTOR = YES; 363 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 364 | GCC_WARN_UNUSED_FUNCTION = YES; 365 | GCC_WARN_UNUSED_VARIABLE = YES; 366 | IPHONEOS_DEPLOYMENT_TARGET = 9.1; 367 | MTL_ENABLE_DEBUG_INFO = NO; 368 | SDKROOT = iphoneos; 369 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 370 | TARGETED_DEVICE_FAMILY = "1,2"; 371 | VALIDATE_PRODUCT = YES; 372 | }; 373 | name = Release; 374 | }; 375 | E49804DC1BF07248003D5539 /* Debug */ = { 376 | isa = XCBuildConfiguration; 377 | buildSettings = { 378 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 379 | INFOPLIST_FILE = SwiftRedis/Info.plist; 380 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 381 | PRODUCT_BUNDLE_IDENTIFIER = ronp001.SwiftRedis; 382 | PRODUCT_NAME = "$(TARGET_NAME)"; 383 | SWIFT_VERSION = 3.0; 384 | }; 385 | name = Debug; 386 | }; 387 | E49804DD1BF07248003D5539 /* Release */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 391 | INFOPLIST_FILE = SwiftRedis/Info.plist; 392 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 393 | PRODUCT_BUNDLE_IDENTIFIER = ronp001.SwiftRedis; 394 | PRODUCT_NAME = "$(TARGET_NAME)"; 395 | SWIFT_VERSION = 3.0; 396 | }; 397 | name = Release; 398 | }; 399 | E49804DF1BF07248003D5539 /* Debug */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | BUNDLE_LOADER = "$(TEST_HOST)"; 403 | INFOPLIST_FILE = SwiftRedisTests/Info.plist; 404 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 405 | PRODUCT_BUNDLE_IDENTIFIER = ronp001.SwiftRedisTests; 406 | PRODUCT_NAME = "$(TARGET_NAME)"; 407 | SWIFT_VERSION = 3.0; 408 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftRedis.app/SwiftRedis"; 409 | }; 410 | name = Debug; 411 | }; 412 | E49804E01BF07248003D5539 /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | BUNDLE_LOADER = "$(TEST_HOST)"; 416 | INFOPLIST_FILE = SwiftRedisTests/Info.plist; 417 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 418 | PRODUCT_BUNDLE_IDENTIFIER = ronp001.SwiftRedisTests; 419 | PRODUCT_NAME = "$(TARGET_NAME)"; 420 | SWIFT_VERSION = 3.0; 421 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftRedis.app/SwiftRedis"; 422 | }; 423 | name = Release; 424 | }; 425 | /* End XCBuildConfiguration section */ 426 | 427 | /* Begin XCConfigurationList section */ 428 | E49804B91BF07248003D5539 /* Build configuration list for PBXProject "SwiftRedis" */ = { 429 | isa = XCConfigurationList; 430 | buildConfigurations = ( 431 | E49804D91BF07248003D5539 /* Debug */, 432 | E49804DA1BF07248003D5539 /* Release */, 433 | ); 434 | defaultConfigurationIsVisible = 0; 435 | defaultConfigurationName = Release; 436 | }; 437 | E49804DB1BF07248003D5539 /* Build configuration list for PBXNativeTarget "SwiftRedis" */ = { 438 | isa = XCConfigurationList; 439 | buildConfigurations = ( 440 | E49804DC1BF07248003D5539 /* Debug */, 441 | E49804DD1BF07248003D5539 /* Release */, 442 | ); 443 | defaultConfigurationIsVisible = 0; 444 | defaultConfigurationName = Release; 445 | }; 446 | E49804DE1BF07248003D5539 /* Build configuration list for PBXNativeTarget "SwiftRedisTests" */ = { 447 | isa = XCConfigurationList; 448 | buildConfigurations = ( 449 | E49804DF1BF07248003D5539 /* Debug */, 450 | E49804E01BF07248003D5539 /* Release */, 451 | ); 452 | defaultConfigurationIsVisible = 0; 453 | defaultConfigurationName = Release; 454 | }; 455 | /* End XCConfigurationList section */ 456 | }; 457 | rootObject = E49804B61BF07248003D5539 /* Project object */; 458 | } 459 | -------------------------------------------------------------------------------- /SwiftRedis.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SwiftRedis.xcodeproj/xcuserdata/ron.xcuserdatad/xcschemes/SwiftRedis.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 51 | 52 | 53 | 54 | 64 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 85 | 91 | 92 | 93 | 94 | 96 | 97 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /SwiftRedis.xcodeproj/xcuserdata/ron.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SwiftRedis.xcscheme 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | E49804BD1BF07248003D5539 16 | 17 | primary 18 | 19 | 20 | E49804D11BF07248003D5539 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SwiftRedis/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // SwiftRedis 4 | // 5 | // Created by Ron Perry on 11/9/15. 6 | // Copyright © 2015 Ron Perry. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(_ application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(_ application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(_ application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(_ application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(_ application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /SwiftRedis/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /SwiftRedis/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /SwiftRedis/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /SwiftRedis/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /SwiftRedis/RedisBuffer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisBuffer.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/7/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RedisBuffer 12 | { 13 | var dataAccumulatedFromStream: Data? 14 | 15 | func clear() 16 | { 17 | dataAccumulatedFromStream = nil 18 | } 19 | 20 | func restoreRemovedData(_ dataToRestore: Data) { 21 | if dataAccumulatedFromStream == nil { 22 | dataAccumulatedFromStream = dataToRestore 23 | return 24 | } else { 25 | var combinedData = NSData(data: dataToRestore) as Data 26 | combinedData.append(dataAccumulatedFromStream!) 27 | dataAccumulatedFromStream = combinedData 28 | } 29 | } 30 | 31 | func removeBytesFromBuffer(_ numBytesToRemove: Int) 32 | { 33 | let data = dataAccumulatedFromStream!.subdata(in: numBytesToRemove ..< dataAccumulatedFromStream!.count) 34 | 35 | if data.count == 0 { 36 | dataAccumulatedFromStream = nil 37 | } else { 38 | dataAccumulatedFromStream = data 39 | } 40 | } 41 | 42 | /* Swift 2 syntax: 43 | func removeBytesFromBuffer(_ numBytesToRemove: Int) 44 | { 45 | let len = dataAccumulatedFromStream!.count - numBytesToRemove 46 | 47 | let range = NSMakeRange(numBytesToRemove, len) 48 | let data = dataAccumulatedFromStream!.subdata(in: range) 49 | 50 | if data.count == 0 { 51 | dataAccumulatedFromStream = nil 52 | } else { 53 | dataAccumulatedFromStream = data 54 | } 55 | } 56 | */ 57 | 58 | func getNextDataOfSize(_ size: Int?) -> Data? 59 | { 60 | if size == nil { return getNextDataUntilCRLF() } 61 | 62 | if dataAccumulatedFromStream == nil { return nil } 63 | if dataAccumulatedFromStream!.count < size! { 64 | return nil 65 | } 66 | 67 | let data = dataAccumulatedFromStream!.subdata(in: 0 ..< size! ) 68 | 69 | removeBytesFromBuffer(size!) 70 | 71 | return data 72 | } 73 | 74 | /* Swift 2 syntax: 75 | func getNextDataOfSize(_ size: Int?) -> Data? 76 | { 77 | if size == nil { return getNextDataUntilCRLF() } 78 | 79 | if dataAccumulatedFromStream == nil { return nil } 80 | if dataAccumulatedFromStream!.count < size! { 81 | return nil 82 | } 83 | 84 | let range = NSMakeRange(0, size!) 85 | let data = dataAccumulatedFromStream!.subdata(in: range) 86 | 87 | removeBytesFromBuffer(size!) 88 | 89 | return data 90 | } 91 | */ 92 | 93 | func getNextStringOfSize(_ size: Int) -> String? 94 | { 95 | let data = getNextDataOfSize(size) 96 | if data == nil { return nil } 97 | 98 | return String(NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!) 99 | } 100 | 101 | func getNextStringUntilCRLF() -> String? 102 | { 103 | let data = getNextDataUntilCRLF() 104 | if data == nil { return nil } 105 | 106 | return String(NSString(data: data!, encoding: String.Encoding.utf8.rawValue)!) 107 | } 108 | 109 | func getNextDataUntilCRLF() -> Data? 110 | { 111 | if dataAccumulatedFromStream == nil { return nil } 112 | 113 | let len = dataAccumulatedFromStream!.count 114 | 115 | var done = false 116 | var bytesProcessed = 0 117 | 118 | let charCR = UInt8(0x0d) 119 | let charLF = UInt8(0x0a) 120 | 121 | var foundCR = false 122 | 123 | let inputStream = InputStream(data: dataAccumulatedFromStream!) 124 | inputStream.open() 125 | 126 | while !done && bytesProcessed < len 127 | { 128 | var char: UInt8 = 0 129 | let count = inputStream.read(&char, maxLength: 1) 130 | 131 | assert(count > 0) 132 | 133 | bytesProcessed += 1 134 | 135 | if foundCR == true && char == charLF { 136 | done = true 137 | } else if ( char == charCR ) { 138 | foundCR = true 139 | } 140 | } 141 | 142 | if !done { 143 | return nil 144 | } 145 | 146 | let result = getNextDataOfSize(bytesProcessed-2) 147 | removeBytesFromBuffer(2) // remove the CRLF 148 | 149 | return result 150 | 151 | } 152 | 153 | func storeReceivedBytes(_ data: Data) 154 | { 155 | if dataAccumulatedFromStream == nil { 156 | dataAccumulatedFromStream = data 157 | } else { 158 | let existingData = dataAccumulatedFromStream! 159 | var mutableData = NSData.init(data: existingData) as Data 160 | mutableData.append(data) 161 | dataAccumulatedFromStream = mutableData 162 | } 163 | } 164 | 165 | func storeReceivedString(_ string: String) 166 | { 167 | let data = string.data(using: String.Encoding.utf8) 168 | 169 | storeReceivedBytes(data!) 170 | } 171 | 172 | } 173 | -------------------------------------------------------------------------------- /SwiftRedis/RedisCommand.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisCommand.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/7/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol RedisCommandDelegate { 12 | func commandExecuted(_ cmd: RedisCommand) 13 | func commandFailed(_ cmd: RedisCommand) 14 | } 15 | 16 | class RedisCommand : CustomStringConvertible 17 | { 18 | var delegate: RedisCommandDelegate? = nil 19 | 20 | var description: String { 21 | return "RedisCommand(\(commandType))" 22 | } 23 | 24 | enum OperationType { case get, set, auth, publish, subscribe, quit, generic } 25 | typealias ValueCompletionHandler = (_ success: Bool, _ key: String, _ result: RedisResponse?, _ cmd: RedisCommand) -> Void 26 | typealias VoidCompletionHandler = (_ success: Bool, _ cmd: RedisCommand) -> Void 27 | 28 | var sent = false // connection object sets this to true when the command is sent 29 | 30 | let commandType: OperationType 31 | var param1: String? 32 | var param2: Data? 33 | var additionalParams: [String?]? 34 | 35 | let valueCompletionHandler: ValueCompletionHandler? 36 | let voidCompletionHandler: VoidCompletionHandler? 37 | let finishWhenResponseReceived: Bool 38 | 39 | var key: String? { get { return param1 } set(value) { param1 = value } } 40 | var valueToSet: Data? { get { return param2 } set(value) {param2 = value} } 41 | 42 | init(type: OperationType, param1: String? = nil, param2: Data? = nil, valueCompletionHandler: ValueCompletionHandler? = nil, voidCompletionHandler: VoidCompletionHandler? = nil, additionalParams: [String?]? = nil ) 43 | { 44 | self.commandType = type 45 | self.param1 = param1 46 | self.param2 = param2 47 | self.valueCompletionHandler = valueCompletionHandler 48 | self.voidCompletionHandler = voidCompletionHandler 49 | self.additionalParams = additionalParams 50 | 51 | finishWhenResponseReceived = (type != .subscribe) 52 | } 53 | 54 | // call the completion handler with a failure status 55 | func completionFailed() 56 | { 57 | switch self.commandType { 58 | case .get, .subscribe, .publish, .generic: 59 | valueCompletionHandler?(false, key!, nil, self) 60 | case .set, .auth, .quit: 61 | voidCompletionHandler?(false, self) 62 | } 63 | 64 | delegate?.commandFailed(self) 65 | } 66 | 67 | var response: RedisResponse? = nil 68 | 69 | func responseReceived(_ response: RedisResponse) 70 | { 71 | self.response = response 72 | 73 | print("received response: \(self.response)") 74 | 75 | var success = true 76 | if response.responseType == .error { 77 | success = false 78 | } 79 | 80 | switch self.commandType { 81 | case .get, .publish, .subscribe, .generic: 82 | valueCompletionHandler?(success, key!, response, self) 83 | case .set, .auth, .quit: 84 | voidCompletionHandler?(success, self) 85 | } 86 | 87 | delegate?.commandExecuted(self) 88 | } 89 | 90 | static func Quit(_ handler: VoidCompletionHandler?) -> RedisCommand 91 | { 92 | return RedisCommand(type: .quit, voidCompletionHandler: handler) 93 | } 94 | 95 | static func Auth(_ password: String, handler: VoidCompletionHandler?) -> RedisCommand 96 | { 97 | return RedisCommand(type: .auth, param1: password, voidCompletionHandler: handler) 98 | } 99 | 100 | static func Set(_ key: String, valueToSet: Data, handler: VoidCompletionHandler?) -> RedisCommand 101 | { 102 | return RedisCommand(type: .set, param1: key, param2: valueToSet, voidCompletionHandler: handler) 103 | } 104 | 105 | static func Set(_ key: String, valueToSet: String, handler: VoidCompletionHandler?) -> RedisCommand 106 | { 107 | return Set(key, valueToSet: valueToSet.data(using: String.Encoding.utf8)!, handler: handler) 108 | } 109 | 110 | static func Get(_ key: String, handler: ValueCompletionHandler?) -> RedisCommand 111 | { 112 | return RedisCommand(type: .get, param1: key, valueCompletionHandler: handler) 113 | } 114 | 115 | static func Publish(_ channel: String, value: String, handler: ValueCompletionHandler?) -> RedisCommand 116 | { 117 | return RedisCommand(type: .publish, param1: channel, param2: value.data(using: String.Encoding.utf8)!, valueCompletionHandler: handler) 118 | } 119 | 120 | static func Subscribe(_ channel: String, handler: ValueCompletionHandler?) -> RedisCommand 121 | { 122 | return RedisCommand(type: .subscribe, param1: channel, valueCompletionHandler: handler) 123 | } 124 | 125 | static func Generic(_ cmd: String, _ arg1: String? = nil, _ arg2: String? = nil, _ arg3: String? = nil, _ arg4: String? = nil, handler: ValueCompletionHandler? ) -> RedisCommand 126 | { 127 | return RedisCommand(type: .generic, param1: cmd, param2: nil, valueCompletionHandler: handler, voidCompletionHandler: nil, additionalParams: [arg1, arg2, arg3, arg4]) 128 | } 129 | 130 | func buildCommandString(_ words: [NSObject]) -> Data 131 | { 132 | var result = NSData(data: "*\(words.count)\r\n".data(using: String.Encoding.utf8)!) as Data 133 | 134 | for word in words { 135 | if let wordStr = word as? NSString { 136 | let lenStr = NSData(data: "$\(wordStr.length)\r\n".data(using: String.Encoding.utf8)!) as Data 137 | result.append(lenStr) 138 | 139 | let strStr = NSData(data: "\(wordStr)\r\n".data(using: String.Encoding.utf8)!) as Data 140 | result.append(strStr) 141 | } else if let wordData = word as? Data { 142 | let lenStr = NSData(data: "$\(wordData.count)\r\n".data(using: String.Encoding.utf8)!) as Data 143 | result.append(lenStr) 144 | 145 | result.append(wordData) 146 | result.append("\r\n".data(using: String.Encoding.utf8)!) 147 | } else { 148 | assert(false) 149 | } 150 | } 151 | 152 | return result 153 | } 154 | 155 | func getCommandString() -> Data? { 156 | 157 | switch commandType { 158 | case .get: 159 | return buildCommandString(["GET" as NSObject, self.param1! as NSObject]) 160 | case .set: 161 | return buildCommandString(["SET" as NSObject, self.param1! as NSObject, self.param2! as NSObject]) 162 | case .publish: 163 | return buildCommandString(["PUBLISH" as NSObject, self.param1! as NSObject, self.param2! as NSObject]) 164 | case .subscribe: 165 | return buildCommandString(["SUBSCRIBE" as NSObject, self.param1! as NSObject]) 166 | case .auth: 167 | return buildCommandString(["AUTH" as NSObject, self.param1! as NSObject]) 168 | case .quit: 169 | return buildCommandString(["QUIT" as NSObject]) 170 | case .generic: 171 | var cmdArray: [String] = [] 172 | cmdArray += [self.param1!] 173 | if let additionalParams = self.additionalParams { 174 | for param in additionalParams { 175 | if let param = param { 176 | cmdArray += [param] 177 | } 178 | } 179 | } 180 | return buildCommandString(cmdArray as [NSObject]) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /SwiftRedis/RedisConnection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionToRedis.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/1/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | 10 | 11 | import UIKit 12 | 13 | /* 14 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 15 | switch (lhs, rhs) { 16 | case let (l?, r?): 17 | return l < r 18 | case (nil, _?): 19 | return true 20 | default: 21 | return false 22 | } 23 | } 24 | */ 25 | 26 | 27 | 28 | protocol RedisConnectionDelegate { 29 | func connected() 30 | func connectionError(_ error: String) 31 | } 32 | 33 | 34 | class RedisConnection : NSObject, StreamDelegate, RedisResponseParserDelegate 35 | { 36 | // MARK: init 37 | 38 | let serverAddress: CFString 39 | let serverPort: UInt32 40 | let enableSSL = false 41 | 42 | init(serverAddress: String, serverPort: UInt32) 43 | { 44 | self.serverAddress = serverAddress as CFString 45 | self.serverPort = serverPort 46 | } 47 | 48 | // account on redislabs.com 49 | 50 | 51 | // MARK: Delegate 52 | var delegate: RedisConnectionDelegate? 53 | 54 | func setDelegate(_ delegate: RedisConnectionDelegate) 55 | { 56 | self.delegate = delegate 57 | } 58 | 59 | // MARK: Streams 60 | fileprivate var inputStream: InputStream? 61 | fileprivate var outputStream: OutputStream? 62 | 63 | func statusRequiresOpening(_ status: CFStreamStatus) -> Bool { 64 | switch(status) { 65 | case .closed, .error, .notOpen: return true 66 | case .atEnd, .open, .opening, .reading, .writing: return false 67 | } 68 | 69 | } 70 | 71 | func inputStreamRequiresOpening(_ stream: InputStream?) -> Bool 72 | { 73 | if stream == nil { return true } 74 | let isStatus = CFReadStreamGetStatus(stream) 75 | return statusRequiresOpening(isStatus) 76 | } 77 | 78 | func outputStreamRequiresOpening(_ stream: OutputStream?) -> Bool 79 | { 80 | if stream == nil { return true } 81 | let osStatus = CFWriteStreamGetStatus(stream) 82 | return statusRequiresOpening(osStatus) 83 | } 84 | 85 | func disconnect() { 86 | connectionState = .closed 87 | 88 | if inputStream != nil { 89 | inputStream!.close() 90 | print("closed input stream. status is now: \(statusOfStreamAsString(inputStream))") 91 | //inputStream = nil 92 | } 93 | if outputStream != nil { 94 | outputStream!.close() 95 | print("closed output stream. status is now: \(statusOfStreamAsString(outputStream))") 96 | //outputStream = nil 97 | } 98 | 99 | if pendingCommand != nil { 100 | pendingCommand!.completionFailed() 101 | pendingCommand = nil 102 | } 103 | 104 | responseParser.abortParsing() 105 | } 106 | 107 | deinit { 108 | disconnect() 109 | } 110 | 111 | 112 | func connect() { 113 | print("input stream: \(statusOfStreamAsString(inputStream)) output stream: \(statusOfStreamAsString(outputStream))") 114 | 115 | if !inputStreamRequiresOpening(self.inputStream) && !outputStreamRequiresOpening(self.outputStream) { 116 | print("Streams are already open. No need to reconnect") 117 | return 118 | } else { 119 | disconnect() 120 | } 121 | 122 | print("connecting...") 123 | 124 | var readStream: Unmanaged? 125 | var writeStream: Unmanaged? 126 | 127 | CFStreamCreatePairWithSocketToHost(nil, self.serverAddress, self.serverPort, &readStream, &writeStream) 128 | 129 | // Documentation suggests readStream and writeStream can be assumed to 130 | // be non-nil. If you believe otherwise, you can test if either is nil 131 | // and implement whatever error-handling you wish. 132 | 133 | self.inputStream = readStream!.takeRetainedValue() 134 | self.outputStream = writeStream!.takeRetainedValue() 135 | 136 | self.inputStream!.delegate = self 137 | self.outputStream!.delegate = self 138 | 139 | self.inputStream!.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode) 140 | self.outputStream!.schedule(in: RunLoop.current, forMode: RunLoopMode.defaultRunLoopMode) 141 | /*** 142 | if enableSSL { 143 | self.inputStream?.setProperty(StreamSocketSecurityLevel.negotiatedSSL, forKey: Stream.PropertyKey.socketSecurityLevelKey) 144 | self.outputStream?.setProperty(StreamSocketSecurityLevel.negotiatedSSL, forKey: Stream.PropertyKey.socketSecurityLevelKey) 145 | } 146 | ***/ 147 | 148 | self.inputStream!.open() 149 | self.outputStream!.open() 150 | } 151 | 152 | // keep track of the connection to the web service 153 | enum ConnectionState { case closed, ready, error } 154 | var connectionState: ConnectionState = .closed 155 | 156 | func stream(_ stream: Stream, handle eventCode: Stream.Event) { 157 | 158 | if stream == inputStream { handleInputStreamEvent(eventCode) } 159 | else if stream == outputStream { handleOutputStreamEvent(eventCode) } 160 | else { 161 | assert(false) 162 | } 163 | } 164 | /**** 165 | func readData(_ maxBytes: Int = 1024) -> Data? 166 | { 167 | return nil 168 | } 169 | ****/ 170 | func readData(_ maxBytes: Int = 1024) -> Data? 171 | { 172 | //let data = Data(capacity: maxBytes) 173 | let data = NSMutableData(length: maxBytes)! 174 | let op = OpaquePointer(data.mutableBytes) 175 | let bytes = UnsafeMutablePointer(op) 176 | 177 | print("readData: reading up to \(maxBytes) from stream") 178 | let length = inputStream?.read(bytes, maxLength: maxBytes) 179 | print("readData: read \(length) bytes") 180 | if length == nil || length! < 0 { 181 | return nil 182 | } 183 | 184 | data.length = length! 185 | 186 | return data as Data 187 | } 188 | 189 | /* Swift 2 syntax: 190 | func readData(_ maxBytes: Int = 1024) -> Data? 191 | { 192 | let data = NSMutableData(length: maxBytes)! 193 | 194 | print("readData: reading up to \(maxBytes) from stream") 195 | let length = inputStream?.read(UnsafeMutablePointer(data.mutableBytes), maxLength: maxBytes) 196 | print("readData: read \(length) bytes") 197 | if length == nil || length < 0 { 198 | return nil 199 | } 200 | 201 | data.length = length! 202 | 203 | return data as Data 204 | } 205 | */ 206 | 207 | func error(_ message: String) 208 | { 209 | connectionState = .error 210 | delegate?.connectionError(message) 211 | } 212 | 213 | 214 | let responseParser = RedisResponseParser() 215 | 216 | func handleInputStreamEvent(_ eventCode: Stream.Event) 217 | { 218 | responseParser.setDelegate(self) 219 | 220 | switch(eventCode) 221 | { 222 | case Stream.Event.openCompleted: 223 | // nothing to do here 224 | print("input stream: .OpenCompleted") 225 | case Stream.Event.hasBytesAvailable: 226 | print("input stream: .HasBytesAvaialable") 227 | switch connectionState { 228 | case .closed: 229 | warnIf(true, "InputStream - HasBytesAvailable when state is .Closed") 230 | case .ready: 231 | while inputStream!.hasBytesAvailable { 232 | let data = readData() 233 | if data != nil { 234 | responseParser.storeReceivedData(data!) 235 | } else { 236 | warn("could not read data even though inputStream!.hasBytesAvaialble is true") 237 | } 238 | } 239 | case .error: 240 | break 241 | } 242 | 243 | case Stream.Event.endEncountered: 244 | print("input stream: .EndEncountered") 245 | case Stream.Event.errorOccurred: 246 | print("input stream: .ErrorEncountered") 247 | case Stream.Event.hasSpaceAvailable: 248 | print("input stream: .HasSpaceAvailable") 249 | case Stream.Event(): 250 | print("input stream: .None") 251 | default: 252 | print("input stream: unknown event \(eventCode)") 253 | } 254 | } 255 | 256 | func warn(_ description: String) 257 | { 258 | NSLog("**** WARNING: \(description)") 259 | } 260 | 261 | func warnIf(_ condition: Bool, _ description: String) { 262 | if !condition { return } 263 | 264 | warn(description) 265 | } 266 | 267 | // MARK: Response Parser Delegate 268 | func errorParsingResponse(_ error: String?) { 269 | var message = "Could not parse response received" 270 | if error != nil { 271 | message += "(" + error! + ")" 272 | } 273 | delegate?.connectionError(message) 274 | } 275 | 276 | func parseOperationAborted() { 277 | print("RedisConnection: parse operation aborted") 278 | } 279 | 280 | func receivedResponse(_ response: RedisResponse) { 281 | if let cmd = pendingCommand { 282 | if cmd.finishWhenResponseReceived { 283 | pendingCommand = nil 284 | } 285 | cmd.responseReceived(response) 286 | } else { 287 | warn("Response received when no command pending \(response)") 288 | } 289 | 290 | } 291 | 292 | 293 | func handleOutputStreamEvent(_ eventCode: Stream.Event) 294 | { 295 | switch(eventCode) 296 | { 297 | case Stream.Event.openCompleted: 298 | warnIf(connectionState != .closed, "OutputStream .OpenCompleted when connectionState is \(connectionState)") 299 | 300 | // mark the current state as "Unauthenticated", so that when the 301 | // output stream is ready for writing, we'll send the authentication command 302 | connectionState = .ready 303 | delegate?.connected() 304 | 305 | case Stream.Event.hasSpaceAvailable: 306 | 307 | // the action to take depends on the state 308 | switch connectionState { 309 | case .closed: 310 | warn("OutputStream - HasSpaceAvailable when state is .Closed") 311 | 312 | case .ready: 313 | sendPendingDataIfPossible() 314 | 315 | case .error: 316 | assert(false) 317 | } 318 | case Stream.Event.openCompleted: 319 | // nothing to do here 320 | print("output stream: .OpenCompleted") 321 | case Stream.Event.hasBytesAvailable: 322 | print("output stream: .HasBytesAvaialable") 323 | case Stream.Event.endEncountered: 324 | print("output stream: .EndEncountered") 325 | case Stream.Event.errorOccurred: 326 | print("output stream: .ErrorEncountered") 327 | case Stream.Event.hasSpaceAvailable: 328 | print("output stream: .HasSpaceAvailable") 329 | sendPendingDataIfPossible() 330 | case Stream.Event(): 331 | print("output stream: .None") 332 | default: 333 | warn("output stream: unknown event \(eventCode)") 334 | } 335 | } 336 | 337 | var pendingCommand: RedisCommand? = nil 338 | 339 | 340 | // this function should be called in two cases: 341 | // 1 - the stream has become ready for writing 342 | // 2 - new command is available for sending 343 | func sendPendingDataIfPossible() 344 | { 345 | if pendingCommand?.sent == true { return } 346 | 347 | if let command = pendingCommand { 348 | 349 | if outputStream?.hasSpaceAvailable == true { 350 | if let data = command.getCommandString() { 351 | command.sent = true 352 | print("sending command string (showing up to 100 bytes): \(String(data: data.subdata(in: 0 ..< min(100,data.count)) as Data, encoding: String.Encoding.utf8))") 353 | //print("sending command string: \(String(data: data as Data, encoding: String.Encoding.utf8))") 354 | 355 | let chunk_size = 10000 356 | let num_chunks = (data.count / chunk_size)+1 357 | print("splitting into \(num_chunks) chunks of up to \(chunk_size) bytes") 358 | for i in 0.. String { 393 | switch(status) { 394 | case .closed: return "Closed" 395 | case .atEnd: return "AtEnd" 396 | case .error: return "Error" 397 | case .notOpen: return "NotOpen" 398 | case .open: return "Open" 399 | case .opening: return "Opening" 400 | case .reading: return "Reading" 401 | case .writing: return "Writing" 402 | } 403 | } 404 | 405 | func statusAsString(_ status: CFStreamStatus) -> String { 406 | switch(status) { 407 | case .closed: return "Closed" 408 | case .atEnd: return "AtEnd" 409 | case .error: return "Error" 410 | case .notOpen: return "NotOpen" 411 | case .open: return "Open" 412 | case .opening: return "Opening" 413 | case .reading: return "Reading" 414 | case .writing: return "Writing" 415 | } 416 | } 417 | 418 | func statusOfStreamAsString(_ stream: Stream?) -> String 419 | { 420 | if stream == nil { return "" } 421 | return statusAsString(stream!.streamStatus) 422 | } 423 | 424 | -------------------------------------------------------------------------------- /SwiftRedis/RedisInterface.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisInterface.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/7/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RedisInterface : RedisCommandDelegate, RedisConnectionDelegate 12 | { 13 | // MARK: properties 14 | let c: RedisConnection 15 | let auth: String 16 | 17 | init(host: String, port: UInt32, auth: String) 18 | { 19 | self.c = RedisConnection(serverAddress: host, serverPort: port) 20 | self.auth = auth 21 | } 22 | 23 | deinit 24 | { 25 | disconnect() 26 | } 27 | 28 | var commandQueue = [RedisCommand]() 29 | var currentCommand: RedisCommand? = nil 30 | var isConnected = false 31 | 32 | func connect() 33 | { 34 | c.delegate = self 35 | c.connect() 36 | } 37 | 38 | func disconnect() 39 | { 40 | commandQueue.removeAll() 41 | c.disconnect() 42 | } 43 | 44 | func addCommandToQueue(_ command: RedisCommand) 45 | { 46 | commandQueue.append(command) 47 | sendNextCommandIfPossible() 48 | } 49 | 50 | func skipPendingCommandsAndQuit(_ completionHandler: RedisCommand.VoidCompletionHandler? ) 51 | { 52 | if commandQueue.count > 0 { 53 | print("RedisInterface -- skipPendingCommandsAndQuit: removing \(commandQueue.count) pending commands from redis queue") 54 | commandQueue.removeAll() 55 | } else { 56 | print("RedisInterface -- skipPendingCommandsAndQuit: no pending commands to remove") 57 | } 58 | self.quit(completionHandler) 59 | } 60 | 61 | func sendNextCommandIfPossible() 62 | { 63 | if !isConnected { return } 64 | if commandQueue.count == 0 { return } 65 | 66 | if currentCommand != nil { return } 67 | 68 | currentCommand = commandQueue.remove(at: 0) 69 | currentCommand?.delegate = self 70 | 71 | print("sending next command: \(currentCommand)") 72 | c.setPendingCommand(currentCommand!) 73 | } 74 | 75 | func authenticationFailed() 76 | { 77 | print("Redis authentication failed") 78 | abort() 79 | } 80 | 81 | // MARK: RedisConnectionDelegate functions 82 | 83 | func connectionError(_ error: String) { 84 | print("RedisInterface: connection error \(error)") 85 | isConnected = false 86 | } 87 | 88 | func connected() 89 | { 90 | isConnected = true 91 | commandQueue.insert(RedisCommand.Auth(self.auth, handler: {success, cmd in 92 | if !success { 93 | self.authenticationFailed() 94 | } 95 | }), at: 0) 96 | 97 | sendNextCommandIfPossible() 98 | } 99 | 100 | // MARK: RedisCommandDelegate functions 101 | 102 | func commandExecuted(_ cmd: RedisCommand) { 103 | self.currentCommand = nil 104 | sendNextCommandIfPossible() 105 | } 106 | 107 | func commandFailed(_ cmd: RedisCommand) { 108 | self.currentCommand = nil 109 | sendNextCommandIfPossible() 110 | } 111 | 112 | // MARK: operational interface 113 | 114 | func setDataForKey(_ key: String, data: Data, completionHandler: RedisCommand.VoidCompletionHandler? ) 115 | { 116 | addCommandToQueue(RedisCommand.Set(key, valueToSet: data, handler: completionHandler)) 117 | } 118 | 119 | func setValueForKey(_ key: String, stringValue: String, completionHandler: RedisCommand.VoidCompletionHandler? ) 120 | { 121 | addCommandToQueue(RedisCommand.Set(key, valueToSet: stringValue, handler: completionHandler)) 122 | } 123 | 124 | func getDataForKey(_ key: String, completionHandler: RedisCommand.ValueCompletionHandler? ) 125 | { 126 | addCommandToQueue(RedisCommand.Get(key, handler: completionHandler)) 127 | } 128 | 129 | func getValueForKey(_ key: String, completionHandler: RedisCommand.ValueCompletionHandler? ) 130 | { 131 | getDataForKey(key, completionHandler: completionHandler) 132 | } 133 | 134 | func subscribe(_ channel: String, completionHandler: RedisCommand.ValueCompletionHandler? ) 135 | { 136 | addCommandToQueue(RedisCommand.Subscribe(channel, handler: completionHandler)) 137 | } 138 | 139 | func publish(_ channel: String, value: String, completionHandler: RedisCommand.ValueCompletionHandler? ) 140 | { 141 | addCommandToQueue(RedisCommand.Publish(channel, value: value, handler: completionHandler)) 142 | } 143 | 144 | func generic(_ cmd: String, _ arg1: String? = nil, _ arg2: String? = nil, _ arg3: String? = nil, _ arg4: String? = nil, completionHandler: RedisCommand.ValueCompletionHandler?) 145 | { 146 | addCommandToQueue(RedisCommand.Generic(cmd, arg1, arg2, arg3, arg4, handler: completionHandler)) 147 | } 148 | 149 | func quit(_ completionHandler: RedisCommand.VoidCompletionHandler? ) 150 | { 151 | addCommandToQueue(RedisCommand.Quit(completionHandler)) 152 | } 153 | 154 | } 155 | 156 | -------------------------------------------------------------------------------- /SwiftRedis/RedisResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisResponse.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/7/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class RedisResponse : CustomStringConvertible { 12 | 13 | enum ResponseType { case int, string, data, error, array, unknown } 14 | 15 | class ParseError : NSError { 16 | init(msg: String) { 17 | super.init(domain: "RedisResponse", code: 0, userInfo: ["message": msg]) 18 | } 19 | 20 | required init?(coder aDecoder: NSCoder) { 21 | fatalError("init(coder:) has not been implemented") 22 | } 23 | } 24 | 25 | let responseType: ResponseType 26 | 27 | var intVal: Int? 28 | 29 | fileprivate var stringValInternal: String? 30 | 31 | var stringVal: String? { 32 | get { 33 | switch responseType { 34 | case .string: 35 | return self.stringValInternal 36 | case .data: 37 | return String(describing: NSString(data: self.dataVal!, encoding: String.Encoding.utf8.rawValue)) 38 | case .int: 39 | return String(describing: intVal) 40 | case .error: 41 | return nil 42 | case .unknown: 43 | return nil 44 | case .array: 45 | var ar: [String?] = [] 46 | for elem in arrayVal! { 47 | ar += [elem.stringVal] 48 | } 49 | return String(describing: ar) 50 | } 51 | } 52 | set(value) { 53 | stringValInternal = value 54 | } 55 | } 56 | 57 | var dataVal: Data? 58 | var errorVal: String? 59 | var arrayVal: [RedisResponse]? 60 | 61 | var parseErrorMsg: String? = nil 62 | 63 | init(intVal: Int? = nil, dataVal: Data? = nil, stringVal: String? = nil, errorVal: String? = nil, arrayVal: [RedisResponse]? = nil, responseType: ResponseType? = nil) 64 | { 65 | self.parseErrorMsg = nil 66 | self.intVal = intVal 67 | self.stringValInternal = stringVal 68 | self.errorVal = errorVal 69 | self.arrayVal = arrayVal 70 | self.dataVal = dataVal 71 | 72 | if intVal != nil { self.responseType = .int } 73 | else if stringVal != nil { self.responseType = .string } 74 | else if errorVal != nil { self.responseType = .error } 75 | else if arrayVal != nil { self.responseType = .array } 76 | else if dataVal != nil { self.responseType = .data } 77 | else if responseType != nil { self.responseType = responseType! } 78 | else { self.responseType = .unknown } 79 | 80 | if self.responseType == .array && self.arrayVal == nil { 81 | self.arrayVal = [RedisResponse]() 82 | } 83 | } 84 | 85 | func parseError(_ msg: String) 86 | { 87 | NSLog("Parse error - \(msg)") 88 | parseErrorMsg = msg 89 | } 90 | 91 | // should only be called if the current object responseType is .Array 92 | func addArrayElement(_ response: RedisResponse) 93 | { 94 | self.arrayVal!.append(response) 95 | } 96 | 97 | var description: String { 98 | var result = "RedisResponse(\(self.responseType):" 99 | 100 | switch self.responseType { 101 | case .error: 102 | result += self.errorVal! 103 | case .string: 104 | result += self.stringVal! 105 | case .int: 106 | result += String(self.intVal!) 107 | case .data: 108 | result += "[Data - \(self.dataVal!.count) bytes]" 109 | case .array: 110 | result += "[Array - \(self.arrayVal!.count) elements]" 111 | case .unknown: 112 | result += "?" 113 | break 114 | } 115 | 116 | result += ")" 117 | 118 | return result 119 | } 120 | 121 | // reads data from the buffer according to own responseType 122 | func readValueFromBuffer(_ buffer: RedisBuffer, numBytes: Int?) -> Bool? 123 | { 124 | let data = buffer.getNextDataOfSize(numBytes) 125 | 126 | if data == nil { return nil } 127 | 128 | if numBytes != nil { // ensure there is also a CRLF after the buffer 129 | let crlfData = buffer.getNextDataUntilCRLF() 130 | if crlfData == nil { // didn't find data 131 | buffer.restoreRemovedData(data!) 132 | return nil 133 | } 134 | 135 | // if the data was CRLF, it would have been removed by getNxtDataUntilCRLF, so buffer should be empty 136 | if crlfData!.count != 0 { 137 | buffer.restoreRemovedData(crlfData!) 138 | return false 139 | } 140 | } 141 | 142 | switch responseType { 143 | case .data: 144 | self.dataVal = data 145 | return true 146 | 147 | case .string: 148 | self.stringVal = String(data: data!, encoding: String.Encoding.utf8) 149 | if ( self.stringVal == nil ) { 150 | parseError("Could not parse string") 151 | return false 152 | } 153 | return true 154 | case .error: 155 | self.errorVal = String(data: data!, encoding: String.Encoding.utf8) 156 | if ( self.errorVal == nil ) { 157 | parseError("Could not parse string") 158 | return false 159 | } 160 | return true 161 | case .int: 162 | let str = String(data: data!, encoding: String.Encoding.utf8) 163 | if ( str == nil ) { 164 | parseError("Could not parse string") 165 | return false 166 | } 167 | self.intVal = Int(str!) 168 | if ( self.intVal == nil ) { 169 | parseError("Received '\(str)' when expecting Integer") 170 | return false 171 | } 172 | return true 173 | 174 | case .array: 175 | parseError("Array parsing not supported yet") 176 | return false 177 | 178 | case .unknown: 179 | parseError("Trying to parse when ResponseType == .Unknown") 180 | return false 181 | } 182 | } 183 | } 184 | 185 | func != (left: RedisResponse, right: RedisResponse) -> Bool { 186 | return !(left == right) 187 | } 188 | 189 | func == (left: RedisResponse, right: RedisResponse) -> Bool { 190 | if left.responseType != right.responseType { return false } 191 | 192 | switch left.responseType { 193 | case .int: 194 | return left.intVal == right.intVal 195 | case .string: 196 | return left.stringVal == right.stringVal 197 | case .error: 198 | return left.errorVal == right.errorVal 199 | case .unknown: 200 | return true 201 | case .data: 202 | return (left.dataVal! == right.dataVal!) 203 | case .array: 204 | if left.arrayVal!.count != right.arrayVal!.count { return false } 205 | for i in 0 ... left.arrayVal!.count-1 { 206 | if !(left.arrayVal![i] == right.arrayVal![i]) { return false } 207 | } 208 | return true 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /SwiftRedis/RedisResponseParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RedisResponseParser.swift 3 | // ActivityKeeper 4 | // 5 | // Created by Ron Perry on 11/7/15. 6 | // Copyright © 2015 ronp001. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | protocol RedisResponseParserDelegate { 13 | func errorParsingResponse(_ error: String?) 14 | func receivedResponse(_ response: RedisResponse) 15 | func parseOperationAborted() 16 | } 17 | 18 | 19 | class RedisResponseParser: RedisResponseParserDelegate 20 | { 21 | // MARK: The Response 22 | fileprivate var _haveResponse = false 23 | var haveResponse: Bool { 24 | get { return _haveResponse } 25 | } 26 | 27 | var lastResponse: RedisResponse? { 28 | get { return response } 29 | } 30 | 31 | 32 | // MARK: Initialization 33 | let redisBuffer: RedisBuffer 34 | 35 | init(redisBuffer: RedisBuffer? = nil) 36 | { 37 | self.redisBuffer = redisBuffer ?? RedisBuffer() 38 | } 39 | 40 | 41 | // MARK: Calling the Delegate 42 | var delegate: RedisResponseParserDelegate? 43 | 44 | func setDelegate(_ delegate: RedisResponseParserDelegate) 45 | { 46 | self.delegate = delegate 47 | } 48 | 49 | func error(_ message: String?) { 50 | self.delegate?.errorParsingResponse(message) 51 | } 52 | 53 | func aborted() { 54 | self.delegate?.parseOperationAborted() 55 | } 56 | 57 | 58 | // MARK: Array processing 59 | var subParser: RedisResponseParser? = nil 60 | 61 | func deploySubParser() 62 | { 63 | subParser = RedisResponseParser(redisBuffer: self.redisBuffer) 64 | subParser?.setDelegate(self) 65 | } 66 | 67 | func detachSubParser() 68 | { 69 | // remove the subParser 70 | subParser = nil 71 | } 72 | 73 | // MARK: acting as the delegate for the sub-parser (when processing arrays) 74 | 75 | func receivedResponse(_ response: RedisResponse) { 76 | arrayElementsLeft! -= 1 77 | 78 | self.response!.addArrayElement(response) 79 | 80 | if arrayElementsLeft == 0 { 81 | detachSubParser() 82 | finishProcessing(.success, errorMessage: nil) 83 | } 84 | } 85 | 86 | func errorParsingResponse(_ error: String?) { 87 | finishProcessing(.failure, errorMessage: error) 88 | } 89 | 90 | func parseOperationAborted() { 91 | finishProcessing(.aborted, errorMessage: "Aborted") 92 | } 93 | 94 | 95 | // MARK: Processing data accumulated from stream 96 | enum ParserState { case waitingForTypeIndicator, waitingForSizeIndicator, waitingForData, waitingForArrayElementCount, processingArrayElements, idle } 97 | 98 | var parserState: ParserState = .idle 99 | var expectingNumOfBytes: Int? // if nil: read until CRLF 100 | var response: RedisResponse? 101 | 102 | // MARK: for array processing 103 | var arrayElementsLeft: Int? 104 | 105 | 106 | var processingComplete = false 107 | 108 | // function returns true if managed to complete processing, false otherwise 109 | @discardableResult func processAccumulatedData() -> Bool 110 | { 111 | processingComplete = false 112 | 113 | while !processingComplete 114 | { 115 | switch parserState { 116 | case .waitingForTypeIndicator, .idle: 117 | response = nil 118 | self._haveResponse = false 119 | 120 | let typeChar = redisBuffer.getNextStringOfSize(1) 121 | if typeChar == nil { return false } 122 | 123 | print("read type char: '\(typeChar)'") 124 | switch typeChar! as String { 125 | case "$": // Bulk String 126 | response = RedisResponse(responseType: .data) 127 | expectingNumOfBytes = 0 // will be set by the size indicator 128 | parserState = .waitingForSizeIndicator 129 | 130 | case ":": // Integer 131 | response = RedisResponse(responseType: .int) 132 | expectingNumOfBytes = nil // read until CRLF 133 | parserState = .waitingForData 134 | 135 | case "+": // Simple String 136 | response = RedisResponse(responseType: .string) 137 | expectingNumOfBytes = nil // read until CRLF 138 | parserState = .waitingForData 139 | 140 | case "-": // Error 141 | response = RedisResponse(responseType: .error) 142 | expectingNumOfBytes = nil // read until CRLF 143 | parserState = .waitingForData 144 | 145 | case "*": // Array 146 | response = RedisResponse(responseType: .array) 147 | arrayElementsLeft = nil 148 | expectingNumOfBytes = nil // read until CRLF 149 | parserState = .waitingForArrayElementCount 150 | 151 | default: 152 | error("unexpected character received while expecting type char: '\(typeChar)'") 153 | } 154 | 155 | case .waitingForSizeIndicator: 156 | let sizeStr = redisBuffer.getNextStringUntilCRLF() 157 | 158 | if sizeStr == nil { return false } 159 | 160 | let size = Int(sizeStr!) 161 | 162 | if size == nil { 163 | error("Expected size indicator. Received \(sizeStr)") 164 | parserState = .waitingForTypeIndicator 165 | return false 166 | } 167 | 168 | expectingNumOfBytes = size 169 | parserState = .waitingForData 170 | 171 | 172 | case .waitingForArrayElementCount: 173 | let sizeStr = redisBuffer.getNextStringUntilCRLF() 174 | if sizeStr == nil { return false } 175 | let size = Int(sizeStr!) 176 | 177 | if size == nil { 178 | error("Expected size indicator. Received \(sizeStr)") 179 | parserState = .waitingForTypeIndicator 180 | return false 181 | } 182 | arrayElementsLeft = size! 183 | print("Expecting \(size) elements in array") 184 | parserState = .processingArrayElements 185 | deploySubParser() 186 | 187 | case .processingArrayElements: 188 | // for arrays, we need to instanciate a new parser and pipe data to it 189 | // the sub-parser uses the same buffer as the parent parser. 190 | if subParser == nil { 191 | deploySubParser() 192 | } 193 | 194 | // the following call will activate the delegate functions (receivedResponse or errorParsing Response) when complete 195 | let subParserSuccess = subParser!.processAccumulatedData() 196 | 197 | if !subParserSuccess { return false } 198 | 199 | case .waitingForData: 200 | let success = response!.readValueFromBuffer(redisBuffer, numBytes: expectingNumOfBytes) 201 | 202 | if success == nil { return false } 203 | 204 | finishProcessing(success! ? .success : .failure, errorMessage: nil) 205 | } 206 | } 207 | return true 208 | } 209 | 210 | enum ParseOperationCompletionStatus { case success, failure, aborted } 211 | 212 | func finishProcessing(_ status: ParseOperationCompletionStatus, errorMessage: String?) 213 | { 214 | processingComplete = true 215 | parserState = .idle 216 | 217 | // inform the delegate that the read operation was completed. 218 | // note: when the delegate is called, it might initiate a recursive call to this function! 219 | switch status { 220 | case .success: 221 | self._haveResponse = true 222 | self.delegate?.receivedResponse(response!) 223 | case .failure: 224 | self.delegate?.errorParsingResponse(errorMessage) 225 | case .aborted: 226 | self.delegate?.parseOperationAborted() 227 | } 228 | } 229 | 230 | // MARK: Aborting 231 | func abortParsing() 232 | { 233 | if subParser != nil { 234 | subParser?.abortParsing() 235 | subParser = nil 236 | } 237 | redisBuffer.clear() 238 | response = nil 239 | _haveResponse = false 240 | parserState = .idle 241 | aborted() 242 | } 243 | 244 | 245 | 246 | // MARK: Reading from stream 247 | func storeReceivedData(_ data: Data) 248 | { 249 | redisBuffer.storeReceivedBytes(data) 250 | processAccumulatedData() 251 | } 252 | 253 | func storeReceivedString(_ str: String) 254 | { 255 | return storeReceivedData(str.data(using: String.Encoding.utf8)!) 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /SwiftRedis/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // SwiftRedis 4 | // 5 | // Created by Ron Perry on 11/9/15. 6 | // Copyright © 2015 Ron Perry. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | // Do any additional setup after loading the view, typically from a nib. 16 | } 17 | 18 | override func didReceiveMemoryWarning() { 19 | super.didReceiveMemoryWarning() 20 | // Dispose of any resources that can be recreated. 21 | } 22 | 23 | 24 | } 25 | 26 | -------------------------------------------------------------------------------- /SwiftRedisTests/ConnectionParams-example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConnectionParams.swift 3 | // RedisConnection 4 | // 5 | // Created by Ron Perry on 11/8/15. 6 | // Copyright © 2015 Ron Perry. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // NOTE: This file only serves as a template, and is not referenced by the unit tests. 12 | // 13 | // To run the unit tests: 14 | // 15 | // 1. Rename the class ConnectionParamsExample to ConnectionParams 16 | // 2. Save a copy of this file as ../../ConnectionParams.swift 17 | // 3. Put the real connection parameters (those that you don't want to share publicly) in ConnectionParams.swift 18 | // 4. Run the unit tests 19 | // 20 | class ConnectionParamsExample { 21 | static let serverAddress = "localhost" 22 | static let serverPort = UInt32(6379) 23 | static let auth = "connection-secret" // warning: including the authentication secret as an unecrypted string in an iOS app is extremely unsecure: hackers can easily grab this string from the app. Use this method only for unit testing, or if the app is installed only on trusted devices. 24 | } 25 | -------------------------------------------------------------------------------- /SwiftRedisTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /SwiftRedisTests/SwiftRedisTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftRedisTests.swift 3 | // SwiftRedisTests 4 | // 5 | // Created by Ron Perry on 11/9/15. 6 | // Copyright © 2015 Ron Perry. All rights reserved. 7 | // 8 | import XCTest 9 | @testable import SwiftRedis 10 | 11 | 12 | class RedisInterfaceTests: XCTestCase 13 | { 14 | func testReadmeExample() 15 | { 16 | 17 | let redis = RedisInterface(host: ConnectionParams.serverAddress, port: ConnectionParams.serverPort, auth: ConnectionParams.auth) 18 | // let redis = RedisInterface(host: , port: , auth: ) 19 | 20 | // Queue a request to initiate a connection. 21 | // Once a connection is established, an AUTH command will be issued with the auth parameters specified above. 22 | redis.connect() 23 | 24 | // Queue a request to set a value for a key in the Redis database. This command will only 25 | // execute after the connection is established and authenticated. 26 | redis.setValueForKey("some:key", stringValue: "a value", completionHandler: { success, cmd in 27 | // this completion handler will be executed after the SET command returns 28 | if success { 29 | print("value stored successfully") 30 | } else { 31 | print("value was not stored") 32 | } 33 | }) 34 | 35 | // Queue a request to get the value of a key in the Redis database. This command will only 36 | // execute after the previous command is complete. 37 | redis.getValueForKey("some:key", completionHandler: { success, key, data, cmd in 38 | if success { 39 | print("the stored data for \(key) is \(data!.stringVal)") 40 | } else { 41 | print("could not get value for \(key)") 42 | } 43 | }) 44 | 45 | // Queue a QUIT command (the connection will close when the QUIT command returns) 46 | var quitComplete: Bool = false 47 | let doneExpectation = expectation(description: "done") 48 | redis.quit({success, cmd in 49 | quitComplete = true 50 | doneExpectation.fulfill() 51 | }) 52 | 53 | waitForExpectations(timeout: 10, handler: { error in 54 | XCTAssert(quitComplete) 55 | }) 56 | } 57 | 58 | 59 | func testSetAndGet() 60 | { 61 | let r = RedisInterface(host: ConnectionParams.serverAddress, port: ConnectionParams.serverPort, auth: ConnectionParams.auth) 62 | 63 | r.connect() 64 | 65 | let arr = [UInt8](repeating: 6, count: 230*1024) 66 | let data1 = Data(bytes: arr) 67 | let data3 = "hi".data(using: String.Encoding.utf8)! 68 | let data2 = "hello, world".data(using: String.Encoding.utf8)! 69 | 70 | 71 | // store data1 72 | let storedExpectation1 = expectation(description: "testkey1 stored") 73 | r.setDataForKey("testkey-getset", data: data1, completionHandler: { success, cmd in 74 | XCTAssertTrue(success, "expecting success storing data for testkey1") 75 | storedExpectation1.fulfill() 76 | }) 77 | waitForExpectations(timeout: 20, handler: { error in 78 | XCTAssertNil(error, "expecting operation to succeed") 79 | }) 80 | 81 | // retrieve data1 82 | let storedExpectation2 = expectation(description: "testkey1 retrieved") 83 | r.getDataForKey("testkey-getset", completionHandler: { success, key, data, cmd in 84 | storedExpectation2.fulfill() 85 | XCTAssertTrue(success) 86 | XCTAssert(key == "testkey-getset") 87 | XCTAssert(data! == RedisResponse(dataVal: data1)) 88 | }) 89 | waitForExpectations(timeout: 20, handler: { error in 90 | XCTAssertNil(error, "expecting operation to succeed") 91 | }) 92 | 93 | 94 | // store data2 95 | let storedExpectation3 = expectation(description: "testkey2 stored") 96 | r.setDataForKey("testkey-getset", data: data2, completionHandler: { success, cmd in 97 | XCTAssertTrue(success, "could not store testkey2") 98 | storedExpectation3.fulfill() 99 | }) 100 | waitForExpectations(timeout: 5, handler: { error in 101 | XCTAssertNil(error, "Error") 102 | }) 103 | 104 | // retrieve data2 105 | let storedExpectation4 = expectation(description: "testkey2 stored") 106 | r.getDataForKey("testkey-getset", completionHandler: { success, key, data, cmd in 107 | storedExpectation4.fulfill() 108 | XCTAssertTrue(success) 109 | XCTAssert(key == "testkey-getset") 110 | XCTAssert(data! == RedisResponse(dataVal: data2)) 111 | }) 112 | waitForExpectations(timeout: 5, handler: { error in 113 | XCTAssertNil(error, "Error") 114 | }) 115 | 116 | 117 | // store data3 118 | let storedExpectation_d3a = expectation(description: "testkey3 stored") 119 | r.setDataForKey("testkey-getset", data: data3, completionHandler: { success, cmd in 120 | XCTAssertTrue(success, "could not store testkey3") 121 | storedExpectation_d3a.fulfill() 122 | }) 123 | waitForExpectations(timeout: 5, handler: { error in 124 | XCTAssertNil(error, "Error") 125 | }) 126 | 127 | // retrieve data3 128 | let storedExpectation_d3b = expectation(description: "testkey3 stored") 129 | r.getDataForKey("testkey-getset", completionHandler: { success, key, data, cmd in 130 | storedExpectation_d3b.fulfill() 131 | XCTAssertTrue(success) 132 | XCTAssert(key == "testkey-getset") 133 | XCTAssert(data! == RedisResponse(dataVal: data3)) 134 | }) 135 | waitForExpectations(timeout: 5, handler: { error in 136 | XCTAssertNil(error, "Error") 137 | }) 138 | 139 | 140 | // quit 141 | let storedExpectation5 = expectation(description: "quit complete") 142 | r.quit({ success in 143 | storedExpectation5.fulfill() 144 | }) 145 | waitForExpectations(timeout: 5, handler: { error in 146 | XCTAssertNil(error, "Error") 147 | }) 148 | 149 | 150 | 151 | } 152 | 153 | 154 | func testSkipPendingCommandsAndQuit() 155 | { 156 | let r = RedisInterface(host: ConnectionParams.serverAddress, port: ConnectionParams.serverPort, auth: ConnectionParams.auth) 157 | 158 | r.connect() // this stores an AUTH command in the queue. since we are running in a single thread, 159 | // the command will not be sent before this routine reaches "waitForExpectation" 160 | 161 | let storedExpectation = expectation(description: "a handler was called") 162 | 163 | 164 | // queue a command that we do not expect will execute 165 | r.setValueForKey("testkey1", stringValue: "a value", completionHandler: { success, cmd in 166 | XCTAssertFalse(true, "not expecting this handler to be called, because the call to testSkipPendingCommandsAndQuit() should have removed the command from the queue") 167 | storedExpectation.fulfill() 168 | }) 169 | 170 | var quitHandlerCalled = false 171 | 172 | r.skipPendingCommandsAndQuit({ success, cmd in 173 | XCTAssertTrue(success == true, "expecting quit handler to succeed") 174 | quitHandlerCalled = true 175 | storedExpectation.fulfill() 176 | }) 177 | 178 | waitForExpectations(timeout: 2, handler: { error in 179 | XCTAssertNil(error, "expecting operation to succeed") 180 | XCTAssertTrue(quitHandlerCalled, "expecting quit handler to have been called") 181 | }) 182 | 183 | } 184 | 185 | func testPubSub() 186 | { 187 | let riPublish = RedisInterface(host: ConnectionParams.serverAddress, port: ConnectionParams.serverPort, auth: ConnectionParams.auth) 188 | riPublish.connect() 189 | 190 | let riSubscribe = RedisInterface(host: ConnectionParams.serverAddress, port: ConnectionParams.serverPort, auth: ConnectionParams.auth) 191 | riSubscribe.connect() 192 | 193 | let expectingSubscribeToReturn1 = expectation(description: "subscribe operation returned once") 194 | var expectingSubscribeToReturn2: XCTestExpectation? = nil 195 | var expectingSubscribeToReturn3: XCTestExpectation? = nil 196 | var subscribeReturnCount = 0 197 | 198 | // subscribe to channel "testchannel" 199 | // important assumption: no one else is subscribed to this channel!!! 200 | riSubscribe.subscribe("testchannel", completionHandler: { success, channel, data, cmd in 201 | 202 | // this completion handler should be called several times. 203 | // the first time: to acknowledge that the subscribe operation was registered 204 | // the next two times: in response to publish operations 205 | 206 | switch subscribeReturnCount { 207 | case 0: 208 | XCTAssertTrue(success) 209 | XCTAssert(channel == "testchannel") 210 | XCTAssert(data! == RedisResponse(arrayVal: [ 211 | RedisResponse(dataVal: "subscribe".data(using: String.Encoding.utf8)), 212 | RedisResponse(dataVal: "testchannel".data(using: String.Encoding.utf8)), 213 | RedisResponse(intVal: 1) 214 | ])) 215 | XCTAssertNotNil(expectingSubscribeToReturn1) 216 | expectingSubscribeToReturn1.fulfill() 217 | 218 | subscribeReturnCount += 1 219 | 220 | case 1: 221 | XCTAssertTrue(success) 222 | XCTAssert(channel == "testchannel") 223 | XCTAssert(data! == RedisResponse(arrayVal: [ 224 | RedisResponse(dataVal: "message".data(using: String.Encoding.utf8)), 225 | RedisResponse(dataVal: "testchannel".data(using: String.Encoding.utf8)), 226 | RedisResponse(dataVal: "publish op 1".data(using: String.Encoding.utf8)), 227 | ])) 228 | 229 | XCTAssertNotNil(expectingSubscribeToReturn2) 230 | expectingSubscribeToReturn2!.fulfill() 231 | subscribeReturnCount += 1 232 | 233 | case 2: 234 | XCTAssertTrue(success) 235 | XCTAssert(channel == "testchannel") 236 | XCTAssert(data! == RedisResponse(arrayVal: [ 237 | RedisResponse(dataVal: "message".data(using: String.Encoding.utf8)), 238 | RedisResponse(dataVal: "testchannel".data(using: String.Encoding.utf8)), 239 | RedisResponse(dataVal: "publish op 2".data(using: String.Encoding.utf8)), 240 | ])) 241 | 242 | XCTAssertNotNil(expectingSubscribeToReturn3) 243 | expectingSubscribeToReturn3!.fulfill() 244 | subscribeReturnCount+=1 245 | 246 | default: 247 | XCTAssert(false) 248 | } 249 | }) 250 | 251 | // wait for the subscribe operation to complete 252 | waitForExpectations(timeout: 1, handler: { error in 253 | XCTAssertNil(error, "expecting operation to succeed") 254 | }) 255 | 256 | XCTAssertEqual(subscribeReturnCount, 1) 257 | // ----- 258 | 259 | // publish something to the test channel 260 | expectingSubscribeToReturn2 = expectation(description: "subscribe operation returned twice") 261 | let expectingPublishToComplete1 = expectation(description: "publish operation 1 completed") 262 | riPublish.publish("testchannel", value: "publish op 1", completionHandler: { success, key, data, cmd in 263 | expectingPublishToComplete1.fulfill() 264 | }) 265 | 266 | // wait for both the publish to complete, and the subscribe to return the 2nd time 267 | waitForExpectations(timeout: 1, handler: { error in 268 | XCTAssertNil(error, "expecting publish 1 to complete, and subscribe to return 2nd time") 269 | }) 270 | 271 | XCTAssertEqual(subscribeReturnCount, 2) 272 | 273 | // ----- 274 | 275 | // publish something else to the test channel 276 | expectingSubscribeToReturn3 = expectation(description: "subscribe operation returned third time") 277 | let expectingPublishToComplete2 = expectation(description: "publish operation 2 completed") 278 | riPublish.publish("testchannel", value: "publish op 2", completionHandler: { success, key, data, cmd in 279 | expectingPublishToComplete2.fulfill() 280 | }) 281 | 282 | // wait for both the publish to complete, and the subscribe to return the 2nd time 283 | waitForExpectations(timeout: 2, handler: { error in 284 | XCTAssertNil(error, "expecting publish 2 to complete, and subscribe to return 3nd time") 285 | }) 286 | 287 | XCTAssertEqual(subscribeReturnCount, 3) 288 | 289 | 290 | } 291 | } 292 | 293 | 294 | 295 | 296 | class RedisConnectionTests: XCTestCase { 297 | 298 | let authCmd = RedisCommand.Auth(ConnectionParams.auth, handler: nil) 299 | 300 | func testAuthentication() 301 | { 302 | let r = RedisConnection(serverAddress: ConnectionParams.serverAddress, serverPort: ConnectionParams.serverPort) 303 | r.connect() 304 | 305 | // test that it works once 306 | let storedExpectation1 = expectation(description: "set command handler activated") 307 | let cmd = RedisCommand.Auth(ConnectionParams.auth, handler: { success, cmd in 308 | XCTAssertTrue(success, "auth command expected to succeed") 309 | XCTAssert(cmd.response! == RedisResponse(stringVal: "OK"), "expecting response from Redis to be OK") 310 | storedExpectation1.fulfill() 311 | }) 312 | 313 | r.setPendingCommand(cmd) 314 | 315 | waitForExpectations(timeout: 2, handler: { error in 316 | XCTAssertNil(error, "Error") 317 | }) 318 | 319 | } 320 | 321 | func testDisconnect() 322 | { 323 | let r = RedisConnection(serverAddress: ConnectionParams.serverAddress, serverPort: ConnectionParams.serverPort) 324 | r.connect() 325 | 326 | // test that it works once 327 | let storedExpectation1 = expectation(description: "set command handler activated") 328 | let cmd = RedisCommand.Auth(ConnectionParams.auth, handler: { success, cmd in 329 | XCTAssertTrue(success, "auth command expected to succeed") 330 | XCTAssert(cmd.response! == RedisResponse(stringVal: "OK"), "expecting response from Redis to be OK") 331 | storedExpectation1.fulfill() 332 | }) 333 | 334 | r.setPendingCommand(cmd) 335 | 336 | waitForExpectations(timeout: 2, handler: { error in 337 | XCTAssertNil(error, "Error") 338 | }) 339 | 340 | 341 | r.disconnect() 342 | r.connect() 343 | 344 | // test that it works again 345 | let storedExpectation2 = expectation(description: "set command handler activated second time") 346 | let cmd2 = RedisCommand.Auth(ConnectionParams.auth, handler: { success, cmd in 347 | XCTAssertTrue(success, "auth command expected to succeed again") 348 | XCTAssert(cmd.response! == RedisResponse(stringVal: "OK"), "expecting second response from Redis to be OK") 349 | storedExpectation2.fulfill() 350 | }) 351 | 352 | r.setPendingCommand(cmd2) 353 | 354 | waitForExpectations(timeout: 2, handler: { error in 355 | XCTAssertNil(error, "Error") 356 | }) 357 | 358 | } 359 | 360 | func testSavingDataWithoutAuthentication() 361 | { 362 | let r = RedisConnection(serverAddress: ConnectionParams.serverAddress, serverPort: ConnectionParams.serverPort) 363 | r.connect() 364 | 365 | // test that it works once 366 | let storedExpectation1 = expectation(description: "set command handler activated") 367 | let cmd = RedisCommand.Set("A", valueToSet: "1", handler: { success, cmd in 368 | XCTAssertFalse(success, "set command expected to fail") 369 | XCTAssert(cmd.response! == RedisResponse(errorVal: "NOAUTH Authentication required"), "expecting response from Redis to be NOAUTH Authentication Required") 370 | storedExpectation1.fulfill() 371 | }) 372 | 373 | r.setPendingCommand(cmd) 374 | 375 | waitForExpectations(timeout: 2, handler: { error in 376 | XCTAssertNil(error, "Error") 377 | }) 378 | 379 | 380 | // now ensure that works the second time too 381 | let storedExpectation2 = expectation(description: "set command handler activated again") 382 | let cmd2 = RedisCommand.Set("A", valueToSet: "1", handler: { success, cmd in 383 | XCTAssertFalse(success, "set command expected to fail") 384 | XCTAssert(cmd.response! == RedisResponse(errorVal: "NOAUTH Authentication required"), "expecting response from Redis to be NOAUTH Authentication Required") 385 | storedExpectation2.fulfill() 386 | }) 387 | 388 | r.setPendingCommand(cmd2) 389 | 390 | waitForExpectations(timeout: 2, handler: { error in 391 | XCTAssertNil(error, "Error") 392 | }) 393 | 394 | } 395 | } 396 | 397 | class RedisParserTests: XCTestCase { 398 | func testRedisResponse() 399 | { 400 | var r = RedisResponse(stringVal: "abc") 401 | XCTAssert(r.responseType == .string) 402 | 403 | r = RedisResponse(intVal: 3) 404 | XCTAssert(r.responseType == .int) 405 | 406 | r = RedisResponse(errorVal: "abc") 407 | XCTAssert(r.responseType == .error) 408 | 409 | r = RedisResponse(dataVal: Data()) 410 | XCTAssert(r.responseType == .data) 411 | 412 | r = RedisResponse(dataVal: NSMutableData() as Data) 413 | XCTAssert(r.responseType == .data) 414 | 415 | r = RedisResponse(arrayVal: [RedisResponse(intVal: 1), RedisResponse(stringVal: "hi")]) 416 | XCTAssert(r.responseType == .array) 417 | 418 | 419 | XCTAssert(RedisResponse(intVal: 1) == RedisResponse(intVal: 1)) 420 | XCTAssert(RedisResponse(intVal: 1) != RedisResponse(intVal: 2)) 421 | XCTAssert(RedisResponse(stringVal: "a") != RedisResponse(intVal: 2)) 422 | XCTAssert(RedisResponse(arrayVal: [RedisResponse(stringVal: "a"), RedisResponse(errorVal: "err")]) == RedisResponse(arrayVal: [RedisResponse(stringVal: "a"), RedisResponse(errorVal: "err")])) 423 | XCTAssert(RedisResponse(arrayVal: [RedisResponse(stringVal: "a"), RedisResponse(errorVal: "err")]) != RedisResponse(arrayVal: [RedisResponse(stringVal: "a"), RedisResponse(errorVal: "err1")])) 424 | } 425 | 426 | func testRedisParser() 427 | { 428 | let parser = RedisResponseParser() 429 | 430 | let resp1 = "+OK\r\n" 431 | parser.storeReceivedData(resp1.data(using: String.Encoding.utf8)!) 432 | 433 | XCTAssertEqual(parser.haveResponse, true) 434 | XCTAssert(parser.lastResponse! == RedisResponse(stringVal: "OK")) 435 | 436 | 437 | parser.storeReceivedString("+") 438 | XCTAssertEqual(parser.haveResponse, false) 439 | parser.storeReceivedString("OK") 440 | XCTAssertEqual(parser.haveResponse, false) 441 | parser.storeReceivedString("\r\n") 442 | XCTAssertEqual(parser.haveResponse, true) 443 | XCTAssert(parser.lastResponse! == RedisResponse(stringVal: "OK")) 444 | 445 | parser.storeReceivedString("+") 446 | XCTAssertEqual(parser.haveResponse, false) 447 | parser.storeReceivedString("OK") 448 | XCTAssertEqual(parser.haveResponse, false) 449 | parser.storeReceivedString("\r") 450 | XCTAssertEqual(parser.haveResponse, false) 451 | parser.storeReceivedString("\r\n") 452 | XCTAssertEqual(parser.haveResponse, true) 453 | XCTAssert(parser.lastResponse! == RedisResponse(stringVal: "OK\r")) 454 | 455 | parser.storeReceivedString(":476\r\n") 456 | XCTAssert(parser.lastResponse! == RedisResponse(intVal: 476)) 457 | 458 | parser.storeReceivedString("$12\r\nabcde\r\nfghij\r\n") 459 | let respData = "abcde\r\nfghij".data(using: String.Encoding.utf8) 460 | XCTAssert(parser.lastResponse! == RedisResponse(dataVal: respData)) 461 | 462 | let data2 = "hello\r\n, world".data(using: String.Encoding.utf8) 463 | parser.storeReceivedString("$\(data2!.count)\r\n") 464 | parser.storeReceivedData(data2!) 465 | XCTAssertEqual(parser.haveResponse, false) 466 | parser.storeReceivedString("\r\n") 467 | XCTAssertEqual(parser.haveResponse, true) 468 | XCTAssert(parser.lastResponse! == RedisResponse(dataVal: data2)) 469 | 470 | parser.storeReceivedString(":123\r\n") 471 | XCTAssert(parser.lastResponse! == RedisResponse(intVal: 123)) 472 | 473 | 474 | parser.storeReceivedString("$12\r\nabcde\r\nfghij") 475 | XCTAssertEqual(parser.haveResponse, false) 476 | parser.storeReceivedString("\r\n") 477 | XCTAssertEqual(parser.haveResponse, true) 478 | XCTAssert(parser.lastResponse! == RedisResponse(dataVal: respData)) 479 | 480 | } 481 | 482 | func testArrayParsing() 483 | { 484 | let parser = RedisResponseParser() 485 | 486 | parser.storeReceivedString("*3\r\n+subscribe\r\n+ev1\r\n:1\r\n") 487 | XCTAssertEqual(parser.haveResponse, true) 488 | if parser.haveResponse { 489 | XCTAssert(parser.lastResponse! == RedisResponse(arrayVal: [ 490 | RedisResponse(stringVal: "subscribe"), 491 | RedisResponse(stringVal: "ev1"), 492 | RedisResponse(intVal: 1) 493 | ])) 494 | } 495 | 496 | } 497 | 498 | 499 | func testAbortWhileParsing() 500 | { 501 | // this class allows us to test whether the parser correctly reports the abort to its delegate 502 | class ParserDelegateForTestingAbort : RedisResponseParserDelegate { 503 | var errorReported = false 504 | var responseReported = false 505 | var abortReported = false 506 | 507 | func errorParsingResponse(_ error: String?) { 508 | errorReported = true 509 | } 510 | func parseOperationAborted() { 511 | abortReported = true 512 | } 513 | func receivedResponse(_ response: RedisResponse) { 514 | responseReported = true 515 | } 516 | func reset() { 517 | errorReported = false 518 | abortReported = false 519 | responseReported = false 520 | } 521 | } 522 | let d = ParserDelegateForTestingAbort() 523 | 524 | let parser = RedisResponseParser() 525 | parser.setDelegate(d) 526 | 527 | 528 | // in the middle of processing an array element 529 | parser.storeReceivedString("*4\r\n+sub") 530 | XCTAssertEqual(parser.haveResponse, false) 531 | parser.abortParsing() 532 | 533 | XCTAssertFalse(d.errorReported, "Parser should not report an error to delegate") 534 | XCTAssertTrue(d.abortReported, "Parser should report an abort to delegate") 535 | XCTAssertFalse(d.responseReported, "Parser should not report a response to delegate") 536 | 537 | d.reset() 538 | 539 | /// ensure can now process a full array 540 | parser.storeReceivedString("*3\r\n+subscribe\r\n+ev1\r\n:1\r\n") 541 | XCTAssertEqual(parser.haveResponse, true) 542 | if parser.haveResponse { 543 | XCTAssert(parser.lastResponse! == RedisResponse(arrayVal: [ 544 | RedisResponse(stringVal: "subscribe"), 545 | RedisResponse(stringVal: "ev1"), 546 | RedisResponse(intVal: 1) 547 | ])) 548 | } 549 | 550 | XCTAssertFalse(d.errorReported, "Parser should not report an error to delegate") 551 | XCTAssertFalse(d.abortReported, "Parser should not report an abort to delegate") 552 | XCTAssertTrue(d.responseReported, "Parser should report a response to delegate") 553 | 554 | 555 | // in the middle of processing an array 556 | d.reset() 557 | parser.storeReceivedString("*4\r\n+sub\r\n") 558 | XCTAssertEqual(parser.haveResponse, false) 559 | parser.abortParsing() 560 | 561 | XCTAssertFalse(d.errorReported, "Parser should not report an error to delegate") 562 | XCTAssertTrue(d.abortReported, "Parser should report an abort to delegate") 563 | XCTAssertFalse(d.responseReported, "Parser should not report a response to delegate") 564 | 565 | 566 | // in the middle of processing a string 567 | d.reset() 568 | parser.storeReceivedString("+sub") 569 | XCTAssertEqual(parser.haveResponse, false) 570 | parser.abortParsing() 571 | 572 | XCTAssertFalse(d.errorReported, "Parser should not report an error to delegate") 573 | XCTAssertTrue(d.abortReported, "Parser should report an abort to delegate") 574 | XCTAssertFalse(d.responseReported, "Parser should not report a response to delegate") 575 | 576 | } 577 | 578 | func testErrorHandling() 579 | { 580 | 581 | // TODO: test error handling 582 | 583 | } 584 | 585 | 586 | func testRedisBuffer() 587 | { 588 | let buf = RedisBuffer() 589 | 590 | // simple test 591 | buf.storeReceivedString("123") 592 | XCTAssertEqual(buf.getNextStringOfSize(1), "1") 593 | XCTAssertEqual(buf.getNextStringOfSize(1), "2") 594 | XCTAssertEqual(buf.getNextStringOfSize(1), "3") 595 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 596 | 597 | // now get longer string back 598 | buf.storeReceivedString("123") 599 | XCTAssertEqual(buf.getNextStringOfSize(3), "123") 600 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 601 | 602 | // store in parts 603 | buf.storeReceivedString("1") 604 | buf.storeReceivedString("2") 605 | buf.storeReceivedString("34") 606 | XCTAssertEqual(buf.getNextStringOfSize(4), "1234") 607 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 608 | 609 | // ensure nil if not enough data 610 | buf.storeReceivedString("123") 611 | XCTAssertEqual(buf.getNextStringOfSize(4), nil) 612 | XCTAssertEqual(buf.getNextStringOfSize(3), "123") 613 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 614 | 615 | 616 | // basic "CRLF" behavior 617 | buf.storeReceivedString("abcde\r\n") 618 | XCTAssertEqual(buf.getNextStringUntilCRLF(), "abcde") 619 | XCTAssertEqual(buf.getNextStringUntilCRLF(), nil) 620 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 621 | 622 | // partial string behavior 623 | buf.storeReceivedString("abcde\r") 624 | XCTAssertEqual(buf.getNextStringUntilCRLF(), nil) 625 | XCTAssertEqual(buf.getNextStringOfSize(1), "a") 626 | buf.storeReceivedString("\n") 627 | XCTAssertEqual(buf.getNextStringUntilCRLF(), "bcde") 628 | XCTAssertEqual(buf.getNextStringOfSize(1), nil) 629 | XCTAssertEqual(buf.getNextStringUntilCRLF(), nil) 630 | 631 | } 632 | 633 | func testRedisCommands() 634 | { 635 | let getCmd = RedisCommand.Get("aKey", handler: nil) 636 | XCTAssertEqual(getCmd.getCommandString(), "*2\r\n$3\r\nGET\r\n$4\r\naKey\r\n".data(using: String.Encoding.utf8)) 637 | 638 | let authCmd = RedisCommand.Auth("12345", handler: nil) 639 | XCTAssertEqual(authCmd.getCommandString(), "*2\r\n$4\r\nAUTH\r\n$5\r\n12345\r\n".data(using: String.Encoding.utf8)) 640 | 641 | 642 | let setCmd = RedisCommand.Set("aKey", valueToSet: "abc".data(using: String.Encoding.utf8)!, handler: nil) 643 | XCTAssertEqual(setCmd.getCommandString(), "*3\r\n$3\r\nSET\r\n$4\r\naKey\r\n$3\r\nabc\r\n".data(using: String.Encoding.utf8)) 644 | 645 | 646 | let genericCmd = RedisCommand.Generic("SET", "mykey", "1", "EX", "3", handler: nil) 647 | 648 | XCTAssertEqual(String(data: genericCmd.getCommandString()!, encoding: String.Encoding.utf8), "*5\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$1\r\n1\r\n$2\r\nEX\r\n$1\r\n3\r\n") 649 | 650 | 651 | 652 | } 653 | 654 | } 655 | --------------------------------------------------------------------------------