├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── Example ├── NetworkingServiceKit.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── NetworkingServiceKit-Example.xcscheme ├── NetworkingServiceKit.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── NetworkingServiceKit │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ └── Main.storyboard │ ├── Images.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-57x57@1x.png │ │ │ ├── Icon-App-57x57@2x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-72x72@1x.png │ │ │ ├── Icon-App-72x72@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ ├── Icon-Small-50x50@1x.png │ │ │ └── Icon-Small-50x50@2x.png │ ├── Info.plist │ ├── LocationService.swift │ ├── NetworkingServiceKit_Example-Bridging-Header.h │ ├── TweetCell.swift │ ├── TwitterAPIConfiguration.swift │ ├── TwitterAPIToken.swift │ ├── TwitterAuthenticationService.swift │ ├── TwitterSearchService.swift │ └── ViewController.swift ├── Podfile ├── Podfile.lock └── Tests │ ├── APITokenTests.swift │ ├── AlamoNetworkManagerTests.swift │ ├── Info.plist │ ├── NetworkingServiceLocatorTests.swift │ ├── TwitterAuthenticationServiceTests.swift │ ├── TwitterSearchServiceTests.swift │ └── twitterSearch.json ├── LICENSE ├── NetworkingServiceKit.podspec ├── NetworkingServiceKit ├── Assets │ ├── .gitkeep │ └── logo.png ├── Classes │ ├── .gitkeep │ ├── Extensions │ │ ├── ArrayExtensions.swift │ │ ├── BundleExtensions.swift │ │ ├── DictionaryExtensions.swift │ │ ├── HTTPURLResponseExtensions.swift │ │ ├── NSErrorExtensions.swift │ │ ├── StringExtensions.swift │ │ ├── URLRequestExtensions.swift │ │ └── UserDefaultsExtensions.swift │ ├── Networking │ │ ├── APIConfigurations.swift │ │ ├── APIToken.swift │ │ ├── AlamoAuthenticationAdapter.swift │ │ ├── AlamoNetworkManager.swift │ │ ├── NetworkManager.swift │ │ └── NetworkURLProtocol.swift │ ├── Service.swift │ ├── ServiceLocator.swift │ └── Testing │ │ ├── ServiceLocator+Stub.swift │ │ ├── ServiceStub.swift │ │ └── StubNetworkManager.swift └── ReactiveSwift │ └── NetworkManager+ReactiveSwift.swift ├── README.md ├── _Pods.xcodeproj └── update-spec.sh /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Description:** 8 | 11 | 12 | **Important files to review:** 13 | 16 | 17 | **Screenshots:** 18 | 21 | 22 | **Link to Bug Fixes:** 23 | 26 | 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata/ 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | Carthage 26 | # We recommend against adding the Pods directory to your .gitignore. However 27 | # you should judge for yourself, the pros and cons are mentioned at: 28 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 29 | # 30 | # Note: if you ignore the Pods directory, make sure to uncomment 31 | # `pod install` in .travis.yml 32 | # 33 | # Pods/ 34 | /Example/Pods 35 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit.xcodeproj/xcshareddata/xcschemes/NetworkingServiceKit-Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 45 | 46 | 48 | 54 | 55 | 56 | 57 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 79 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 98 | 100 | 106 | 107 | 108 | 109 | 111 | 112 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 02/23/2017. 6 | // Copyright (c) 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NetworkingServiceKit 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | // Attach our services 19 | ServiceLocator.set(services: [TwitterAuthenticationService.self, LocationService.self, TwitterSearchService.self], 20 | api: TwitterAPIConfigurationType.self, 21 | auth: TwitterApp.self, 22 | token: TwitterAPIToken.self) 23 | //if we are not authenticated, launch out auth service 24 | if APITokenManager.currentToken == nil { 25 | let authService = ServiceLocator.service(forType: TwitterAuthenticationService.self) 26 | authService?.authenticateTwitterClient(completion: { _ in 27 | print("Authenticated with Twitter") 28 | }) 29 | } 30 | return true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/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 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "57x57", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-57x57@1x.png", 49 | "scale" : "1x" 50 | }, 51 | { 52 | "size" : "57x57", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-57x57@2x.png", 55 | "scale" : "2x" 56 | }, 57 | { 58 | "size" : "60x60", 59 | "idiom" : "iphone", 60 | "filename" : "Icon-App-60x60@2x.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "60x60", 65 | "idiom" : "iphone", 66 | "filename" : "Icon-App-60x60@3x.png", 67 | "scale" : "3x" 68 | }, 69 | { 70 | "size" : "20x20", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-20x20@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "20x20", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-20x20@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "29x29", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-29x29@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "29x29", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-29x29@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "40x40", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-40x40@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "40x40", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-40x40@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "50x50", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-Small-50x50@1x.png", 109 | "scale" : "1x" 110 | }, 111 | { 112 | "size" : "50x50", 113 | "idiom" : "ipad", 114 | "filename" : "Icon-Small-50x50@2x.png", 115 | "scale" : "2x" 116 | }, 117 | { 118 | "size" : "72x72", 119 | "idiom" : "ipad", 120 | "filename" : "Icon-App-72x72@1x.png", 121 | "scale" : "1x" 122 | }, 123 | { 124 | "size" : "72x72", 125 | "idiom" : "ipad", 126 | "filename" : "Icon-App-72x72@2x.png", 127 | "scale" : "2x" 128 | }, 129 | { 130 | "size" : "76x76", 131 | "idiom" : "ipad", 132 | "filename" : "Icon-App-76x76@1x.png", 133 | "scale" : "1x" 134 | }, 135 | { 136 | "size" : "76x76", 137 | "idiom" : "ipad", 138 | "filename" : "Icon-App-76x76@2x.png", 139 | "scale" : "2x" 140 | }, 141 | { 142 | "size" : "83.5x83.5", 143 | "idiom" : "ipad", 144 | "filename" : "Icon-App-83.5x83.5@2x.png", 145 | "scale" : "2x" 146 | } 147 | ], 148 | "info" : { 149 | "version" : 1, 150 | "author" : "xcode" 151 | } 152 | } -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@1x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/Example/NetworkingServiceKit/Images.xcassets/AppIcon.appiconset/Icon-Small-50x50@2x.png -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | MKSP 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 0.1 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | 1 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UIStatusBarStyle 36 | UIStatusBarStyleLightContent 37 | UISupportedInterfaceOrientations 38 | 39 | UIInterfaceOrientationPortrait 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/LocationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LocationService.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/25/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreLocation 11 | import MapKit 12 | import NetworkingServiceKit 13 | 14 | open class LocationAnnotation: NSObject, MKAnnotation { 15 | 16 | public var title: String? 17 | public var coordinate: CLLocationCoordinate2D 18 | 19 | public init(title: String, coordinate: CLLocationCoordinate2D) { 20 | self.title = title 21 | self.coordinate = coordinate 22 | } 23 | } 24 | 25 | open class LocationService: AbstractBaseService { 26 | 27 | /// Maps a set of IndexPaths and TwitterSearchResult into LocationAnnotation by matching the ones with location data against Apple CLGeocoder 28 | /// 29 | /// - Parameters: 30 | /// - visibleIndexes: a list of table indexes 31 | /// - results: current list of search results 32 | /// - completion: A list of location annotation for the search results that had valid locations 33 | public func geocodeTweets(on visibleIndexes: [IndexPath]?, 34 | with results: [TwitterSearchResult], 35 | completion: @escaping (_ annotations: [LocationAnnotation]) -> Void) { 36 | 37 | if let visibleIndexes = visibleIndexes, visibleIndexes.count > 0 { 38 | let flatVisibleIndexRows = visibleIndexes.compactMap { $0.row } 39 | let visibleSearchResults = results.enumerated().filter { index, result -> Bool in 40 | return flatVisibleIndexRows.contains(index) && !result.user.location.isEmpty 41 | } 42 | let visibleLocationResults = visibleSearchResults.flatMap {$0.element.user.location} 43 | 44 | var annotations = [LocationAnnotation]() 45 | let group = DispatchGroup() 46 | for locationName in visibleLocationResults { 47 | group.enter() 48 | CLGeocoder().geocodeAddressString(String(locationName), completionHandler: { placemark, _ in 49 | if let placemark = placemark?.first, 50 | let location = placemark.location { 51 | annotations.append(LocationAnnotation(title: String(locationName), coordinate: location.coordinate)) 52 | } 53 | group.leave() 54 | }) 55 | } 56 | 57 | group.notify(queue: .main) { 58 | //All location requests done 59 | completion(annotations) 60 | } 61 | } 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/NetworkingServiceKit_Example-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Use this file to import your target's public headers that you would like to expose to Swift. 3 | // 4 | 5 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/TweetCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TweetCell.swift 3 | // MakespaceServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/25/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import NetworkingServiceKit 11 | 12 | class TweetCell: UITableViewCell { 13 | 14 | @IBOutlet private weak var userLabel: UILabel! 15 | @IBOutlet private weak var tweetLabel: UILabel! 16 | @IBOutlet private weak var tweetImageView: UIImageView! 17 | 18 | func load(with result: TwitterSearchResult) { 19 | tweetLabel.text = result.tweet 20 | userLabel.text = result.user.handle 21 | tweetImageView.setImageWith(url: URL(string: result.user.imagePath)!, placeholderImage: nil) 22 | } 23 | } 24 | 25 | extension UIImageView { 26 | 27 | /// Loads an image URL into this ImageView 28 | /// 29 | /// - Parameters: 30 | /// - url: the image url to set into this ImageView 31 | /// - placeholder: a placeholder image, if the request fails or the image is invalid 32 | public func setImageWith(url: URL, placeholderImage placeholder: UIImage?) { 33 | //Let's make it all manual for the sake of not adding another dependency 34 | 35 | let session = URLSession(configuration: .default) 36 | 37 | let downloadPicTask = session.dataTask(with: url) { (data, response, error) in 38 | if error == nil, let imageData = data { 39 | DispatchQueue.main.sync { 40 | self.image = UIImage(data: imageData) 41 | } 42 | } 43 | 44 | DispatchQueue.main.sync { 45 | if self.image == nil { 46 | self.image = placeholder 47 | } 48 | } 49 | } 50 | 51 | downloadPicTask.resume() 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/TwitterAPIConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterAPIConfiguration.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 5/4/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NetworkingServiceKit 11 | 12 | public enum TwitterApp: Int, APIConfigurationAuth { 13 | case twitterClient = 0 14 | 15 | public init(bundleId: String?) { 16 | /// For this example lets default into our twitter client, otherwise here where you check your bundleID for your app 17 | self.init(rawValue: 0)! 18 | } 19 | 20 | /// Twitter Developer Key and Secret, create your own here: https://apps.twitter.com 21 | public var key: String { 22 | switch self { 23 | case .twitterClient: 24 | return "QUk41VlXdW1fgvxPU2YP5wiGB" 25 | } 26 | } 27 | public var secret: String { 28 | switch self { 29 | case .twitterClient: 30 | return "RiikF9L25dMhSbFGFD1QRPk036IgQ3BUqpWkq7gN8fIfuYwpoI" 31 | } 32 | } 33 | } 34 | 35 | /// Server information for our twtter servers, in this case we dont have a staging URL for twitter API 36 | public enum TwitterAPIConfigurationType: String, APIConfigurationType { 37 | case staging = "STAGING" 38 | case production = "PRODUCTION" 39 | case custom = "CUSTOM" 40 | 41 | /// Custom init for a key (makes it non failable as opposed to (rawValue:) 42 | public init(stringKey: String) { 43 | switch stringKey { 44 | case "STAGING": 45 | self = .staging 46 | case "PRODUCTION": 47 | self = .production 48 | case "CUSTOM": 49 | self = .custom 50 | default: 51 | self = .staging 52 | } 53 | } 54 | 55 | /// URL for given server 56 | public var URL: String { 57 | switch self { 58 | case .staging: 59 | return "https://api.twitter.com" 60 | case .production: 61 | return "https://api.twitter.com" 62 | case .custom: 63 | return "https://api.twitter.com" 64 | } 65 | } 66 | 67 | /// Web URL for given server 68 | public var webURL: String { 69 | switch self { 70 | case .staging: 71 | return "https://twitter.com" 72 | case .production: 73 | return "https://twitter.com" 74 | case .custom: 75 | return "https://twitter.com" 76 | } 77 | } 78 | 79 | /// Display name for given server 80 | public var displayName: String { 81 | return self.rawValue.capitalized 82 | } 83 | 84 | /// Explicitly tells our protocol which is our default configuration 85 | public static var defaultConfiguration: APIConfigurationType { 86 | return TwitterAPIConfigurationType.production 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/TwitterAPIToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterAPIToken.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 5/5/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import NetworkingServiceKit 11 | 12 | //Custom Keys for Twitter Auth Response 13 | public enum TwitterAPITokenKey: String { 14 | case tokenTypeKey = "Token_CurrentTokenTypeKey" 15 | case accessTokenKey = "Token_CurrentAccessTokenKey" 16 | var responseKey: String { 17 | switch self { 18 | case .tokenTypeKey: return "token_type" 19 | case .accessTokenKey: return "access_token" 20 | } 21 | } 22 | } 23 | 24 | /// Twitter API Token Object used for authenticating requests 25 | public class TwitterAPIToken: NSObject, APIToken { 26 | var tokenType: String 27 | var accessToken: String 28 | 29 | public init(tokenType: String, 30 | accessToken: String) { 31 | 32 | self.tokenType = tokenType 33 | self.accessToken = accessToken 34 | } 35 | 36 | public var authorization: String { 37 | return self.accessToken 38 | } 39 | 40 | public static func make(from tokenResponse: Any) -> APIToken? { 41 | if let tokenResponse = tokenResponse as? [String: Any], 42 | let tokenType = tokenResponse[TwitterAPITokenKey.tokenTypeKey.responseKey] as? String, 43 | let accessToken = tokenResponse[TwitterAPITokenKey.accessTokenKey.responseKey] as? String { 44 | let token = TwitterAPIToken(tokenType: tokenType, accessToken: accessToken) 45 | self.set(object: tokenType, for: .tokenTypeKey) 46 | self.set(object: accessToken, for: .accessTokenKey) 47 | return token 48 | } 49 | return nil 50 | } 51 | 52 | public static func makePersistedToken() -> APIToken? { 53 | if let tokenType = self.object(for: .tokenTypeKey) as? String, 54 | let accessToken = self.object(for: .accessTokenKey) as? String { 55 | let token = TwitterAPIToken(tokenType: tokenType, accessToken: accessToken) 56 | return token 57 | } 58 | return nil 59 | } 60 | 61 | public static func clearToken() { 62 | self.set(object: nil, for: .tokenTypeKey) 63 | self.set(object: nil, for: .accessTokenKey) 64 | } 65 | 66 | public static func object(for key: TwitterAPITokenKey) -> Any? { 67 | return UserDefaults.serviceLocator?.object(forKey: key.rawValue) 68 | } 69 | 70 | public static func set(object obj:Any?, for key: TwitterAPITokenKey) { 71 | UserDefaults.serviceLocator?.set(obj, forKey: key.rawValue) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/TwitterAuthenticationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterAuthenticationService.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import NetworkingServiceKit 11 | 12 | /// Response for de/authenticating users 13 | @objc 14 | open class TwitterAuthenticationService: AbstractBaseService { 15 | 16 | /// Authenticates a user with an existing email and password, 17 | /// if successful this service automatically persist all token information 18 | /// 19 | /// - Parameters: 20 | /// - email: user's email 21 | /// - password: user's password 22 | /// - completion: a completion block indicating if the authentication was succesful 23 | public func authenticateTwitterClient(completion:@escaping (_ authenticated: Bool) -> Void) { 24 | if let encodedKey = currentConfiguration.APIKey.addingPercentEncoding(withAllowedCharacters:.urlHostAllowed), 25 | let encodedSecret = currentConfiguration.APISecret.addingPercentEncoding(withAllowedCharacters:.urlHostAllowed) { 26 | let combinedAuth = "\(encodedKey):\(encodedSecret)" 27 | let base64Auth = Data(combinedAuth.utf8).base64EncodedString() 28 | let body = "grant_type=client_credentials" 29 | request(path: "oauth2/token", 30 | method: .post, 31 | with: body.asParameters(), 32 | headers: ["Authorization": "Basic " + base64Auth], 33 | success: { response in 34 | let token = APITokenManager.store(tokenInfo: response) 35 | completion(token != nil) 36 | }, failure: { _ in 37 | completion(false) 38 | }) 39 | } else { 40 | completion(false) 41 | } 42 | } 43 | 44 | /// Logout the existing token 45 | /// if succesful all token data will get automatically clear, service locator will get reset also 46 | /// 47 | /// - Parameter completion: a completion block indicating if the logout was successful 48 | public func logoutTwitterClient(completion:@escaping (_ authenticated: Bool) -> Void) { 49 | 50 | request(path: "oauth2/invalidate_token", 51 | method: .post, 52 | with: [:], 53 | success: { _ in 54 | APITokenManager.clearAuthentication() 55 | ServiceLocator.reloadExistingServices() 56 | completion(true) 57 | }, failure: { _ in 58 | completion(false) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/TwitterSearchService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterSearchService.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/24/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import NetworkingServiceKit 11 | import ReactiveSwift 12 | 13 | public struct TwitterUser { 14 | public let handle: String 15 | public let imagePath: String 16 | public let location: String 17 | 18 | public init?(dictionary:[String:Any]) { 19 | guard let userHandle = dictionary["screen_name"] as? String, 20 | let userImagePath = dictionary["profile_image_url_https"] as? String, 21 | let location = dictionary["location"] as? String else { return nil } 22 | self.handle = userHandle 23 | self.imagePath = userImagePath 24 | self.location = location 25 | } 26 | } 27 | public struct TwitterSearchResult { 28 | public let tweet: String 29 | public let user: TwitterUser 30 | 31 | public init?(dictionary:[String:Any]) { 32 | guard let tweet = dictionary["text"] as? String, 33 | let userDictionary = dictionary["user"] as? [String:Any], 34 | let user = TwitterUser(dictionary:userDictionary) else { return nil } 35 | self.tweet = tweet 36 | self.user = user 37 | } 38 | } 39 | open class TwitterSearchService: AbstractBaseService { 40 | private var nextResultsPage: String? 41 | open override var serviceVersion: String { 42 | return "1.1" 43 | } 44 | 45 | /// Search Recent Tweets by hashtag 46 | /// 47 | /// - Parameters: 48 | /// - hashtag: the hashtag to use when searching 49 | /// - completion: a list of TwitterSearchResult based on the term/hashtag sent 50 | public func searchRecents(by hashtag: String, 51 | completion:@escaping (_ results: [TwitterSearchResult]) -> Void) { 52 | guard !hashtag.isEmpty else { 53 | completion([TwitterSearchResult]()) 54 | return 55 | } 56 | let parameters = ["q": hashtag] 57 | request(path: "search/tweets.json", 58 | method: .get, 59 | with: parameters, 60 | success: { response in 61 | var searchResults = [TwitterSearchResult]() 62 | if let results = response["statuses"] as? [[String:Any]] { 63 | for result in results { 64 | if let twitterResult = TwitterSearchResult(dictionary: result) { 65 | searchResults.append(twitterResult) 66 | } 67 | } 68 | } 69 | //save next page 70 | if let metadata = response["search_metadata"] as? [String:Any], 71 | let nextPage = metadata["next_results"] as? String { 72 | self.nextResultsPage = nextPage 73 | } 74 | completion(searchResults) 75 | }, failure: { _ in 76 | completion([TwitterSearchResult]()) 77 | }) 78 | } 79 | 80 | /// Continue the search for the last valid hashtag that was searched for (Reactive) 81 | /// 82 | /// - Parameter completion: a list of TwitterSearchResult based on the term/hashtag sent 83 | public func searchNextRecentsPageProducer() -> SignalProducer<[TwitterSearchResult],MSError> { 84 | guard let nextPage = self.nextResultsPage else { 85 | return SignalProducer.empty 86 | } 87 | 88 | return request(path: "search/tweets.json\(nextPage)").map { response -> [TwitterSearchResult] in 89 | var searchResults = [TwitterSearchResult]() 90 | 91 | guard let response = response else { return searchResults } 92 | 93 | if let results = response["statuses"] as? [[String:Any]] { 94 | for result in results { 95 | if let twitterResult = TwitterSearchResult(dictionary: result) { 96 | searchResults.append(twitterResult) 97 | } 98 | } 99 | } 100 | //save next page 101 | if let metadata = response["search_metadata"] as? [String:Any], 102 | let nextPage = metadata["next_results"] as? String { 103 | self.nextResultsPage = nextPage 104 | } 105 | return searchResults 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Example/NetworkingServiceKit/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 02/23/2017. 6 | // Copyright (c) 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import MapKit 11 | import NetworkingServiceKit 12 | import ReactiveSwift 13 | 14 | class ViewController: UIViewController { 15 | 16 | @IBOutlet fileprivate weak var mapView: MKMapView! 17 | @IBOutlet fileprivate weak var searchBar: UISearchBar! 18 | @IBOutlet fileprivate weak var tweetsTableView: UITableView! 19 | fileprivate var currentResults = [TwitterSearchResult]() 20 | 21 | override func viewDidLoad() { 22 | super.viewDidLoad() 23 | tweetsTableView.estimatedRowHeight = 85 24 | tweetsTableView.rowHeight = UITableView.automaticDimension 25 | mapView.region = MKCoordinateRegion(MKMapRect.world) 26 | mapView.centerCoordinate = CLLocationCoordinate2D(latitude: 40.709540, longitude: -74.008078) //center in NYC 27 | //Do initial search 28 | searchBar(searchBar, textDidChange: "Makespace") 29 | } 30 | 31 | func showTweetsLocationsOnMap() { 32 | let locationService = ServiceLocator.service(forType: LocationService.self) 33 | locationService?.geocodeTweets(on: tweetsTableView.indexPathsForVisibleRows, 34 | with: currentResults, 35 | completion: { annotations in 36 | annotations.forEach { self.mapView.addAnnotation($0) } 37 | }) 38 | } 39 | } 40 | 41 | extension ViewController: UISearchBarDelegate 42 | { 43 | 44 | func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { 45 | 46 | //this should be throtte 47 | let twitterSearchService = ServiceLocator.service(forType: TwitterSearchService.self) 48 | twitterSearchService?.searchRecents(by: searchText, completion: { [weak self] results in 49 | 50 | if let annotations = self?.mapView.annotations { 51 | self?.mapView.removeAnnotations(annotations) 52 | } 53 | self?.currentResults = results 54 | self?.tweetsTableView.reloadData() 55 | self?.showTweetsLocationsOnMap() 56 | }) 57 | } 58 | } 59 | 60 | extension ViewController: UITableViewDelegate, UITableViewDataSource 61 | { 62 | 63 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 64 | return currentResults.count 65 | } 66 | 67 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 68 | if let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TweetCell.self)) as? TweetCell { 69 | cell.load(with: currentResults[indexPath.row]) 70 | return cell 71 | } 72 | return UITableViewCell() 73 | } 74 | 75 | func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 76 | let approachingBottomIndex = currentResults.count - 2 77 | if indexPath.row == approachingBottomIndex { 78 | let twitterSearchService = ServiceLocator.service(forType: TwitterSearchService.self) 79 | twitterSearchService?.searchNextRecentsPageProducer().on(value: { [weak self] results in 80 | self?.currentResults.append(contentsOf: results) 81 | self?.tweetsTableView.reloadData() 82 | self?.showTweetsLocationsOnMap() 83 | }).start() 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://cdn.cocoapods.org/' 2 | platform :ios, '13.0' 3 | inhibit_all_warnings! 4 | use_frameworks! 5 | 6 | target 'NetworkingServiceKit_Example' do 7 | pod 'NetworkingServiceKit', :path => '../' 8 | pod 'NetworkingServiceKit/ReactiveSwift', :path => '../' 9 | 10 | target 'NetworkingServiceKit_Tests' do 11 | inherit! :search_paths 12 | 13 | pod 'Quick' 14 | pod 'Nimble' 15 | pod 'Mockingjay', :git => 'https://github.com/kylef/Mockingjay.git', :tag => '3.0.0-alpha.1' 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /Example/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (5.4.1) 3 | - AlamofireImage (4.1.0): 4 | - Alamofire (~> 5.1) 5 | - CryptoSwift (1.3.8) 6 | - Mockingjay (3.0.0-alpha.1): 7 | - Mockingjay/Core (= 3.0.0-alpha.1) 8 | - Mockingjay/XCTest (= 3.0.0-alpha.1) 9 | - Mockingjay/Core (3.0.0-alpha.1): 10 | - URITemplate (~> 3.0) 11 | - Mockingjay/XCTest (3.0.0-alpha.1): 12 | - Mockingjay/Core 13 | - NetworkingServiceKit (1.8.1): 14 | - NetworkingServiceKit/Core (= 1.8.1) 15 | - NetworkingServiceKit/Core (1.8.1): 16 | - Alamofire 17 | - SwiftyJSON 18 | - NetworkingServiceKit/ReactiveSwift (1.8.1): 19 | - Alamofire 20 | - AlamofireImage 21 | - CryptoSwift 22 | - ReactiveSwift 23 | - SwiftyJSON 24 | - Nimble (9.0.0) 25 | - Quick (3.1.1) 26 | - ReactiveSwift (6.5.0) 27 | - SwiftyJSON (5.0.0) 28 | - URITemplate (3.0.0) 29 | 30 | DEPENDENCIES: 31 | - Mockingjay (from `https://github.com/kylef/Mockingjay.git`, tag `3.0.0-alpha.1`) 32 | - NetworkingServiceKit (from `../`) 33 | - NetworkingServiceKit/ReactiveSwift (from `../`) 34 | - Nimble 35 | - Quick 36 | 37 | SPEC REPOS: 38 | trunk: 39 | - Alamofire 40 | - AlamofireImage 41 | - CryptoSwift 42 | - Nimble 43 | - Quick 44 | - ReactiveSwift 45 | - SwiftyJSON 46 | - URITemplate 47 | 48 | EXTERNAL SOURCES: 49 | Mockingjay: 50 | :git: https://github.com/kylef/Mockingjay.git 51 | :tag: 3.0.0-alpha.1 52 | NetworkingServiceKit: 53 | :path: "../" 54 | 55 | CHECKOUT OPTIONS: 56 | Mockingjay: 57 | :git: https://github.com/kylef/Mockingjay.git 58 | :tag: 3.0.0-alpha.1 59 | 60 | SPEC CHECKSUMS: 61 | Alamofire: 2291f7d21ca607c491dd17642e5d40fdcda0e65c 62 | AlamofireImage: c4a2ba349885fb3064feb74d2e547bd42ce9be10 63 | CryptoSwift: 01b0f0cba1d5c212e5a335ff6c054fb75a204f00 64 | Mockingjay: 97656c6f59879923976a0a52ef09da45756cca82 65 | NetworkingServiceKit: b695a33376c38c8d56151635bb103d9eae5a8e7e 66 | Nimble: 3b4ec3fd40f1dc178058e0981107721c615643d8 67 | Quick: ea9a702cdf4bfd561c47006f1e48c084dfca309e 68 | ReactiveSwift: 8d159904084e908856cde90b2e28823f3c7e485f 69 | SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 70 | URITemplate: 58e0d47f967006c5d59888af5356c4a8ed3b197d 71 | 72 | PODFILE CHECKSUM: d17e5d5031d259548b18efdd0d30c3e4ea330bb3 73 | 74 | COCOAPODS: 1.11.2 75 | -------------------------------------------------------------------------------- /Example/Tests/APITokenTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APITokenTests.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 4/4/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import NetworkingServiceKit 12 | 13 | class APITokenTests: QuickSpec { 14 | 15 | 16 | override func spec() { 17 | beforeEach { 18 | ServiceLocator.defaultNetworkClientType = StubNetworkManager.self 19 | UserDefaults.clearServiceLocatorUserDefaults() 20 | ServiceLocator.reset() 21 | } 22 | 23 | describe("when setting up access token information") { 24 | context("if we have an access token") { 25 | 26 | it("should return the appropiate information for the given email") { 27 | let dataResponse = ["refresh_token" : "DWALI", 28 | "token_type" : "access", 29 | "access_token" : "KWALI", 30 | "expires_in" : 100, 31 | "scope" : "mobile"] as [String : Any] 32 | APITokenManager.tokenType = TwitterAPIToken.self 33 | let apiToken = APITokenManager.store(tokenInfo: dataResponse) 34 | expect(apiToken).toNot(beNil()) 35 | 36 | let accessToken = TwitterAPIToken.object(for: TwitterAPITokenKey.accessTokenKey) as! String 37 | expect(accessToken).to(equal("KWALI")) 38 | 39 | let tokenType = TwitterAPIToken.object(for: TwitterAPITokenKey.tokenTypeKey) as! String 40 | expect(tokenType).to(equal("access")) 41 | } 42 | it("should be valid token on a loaded service") { 43 | 44 | let dataResponse = ["refresh_token" : "DWALI", 45 | "token_type" : "access", 46 | "access_token" : "KWALI", 47 | "expires_in" : 100, 48 | "scope" : "mobile"] as [String : Any] 49 | APITokenManager.tokenType = TwitterAPIToken.self 50 | let apiToken = APITokenManager.store(tokenInfo: dataResponse) 51 | expect(apiToken).toNot(beNil()) 52 | 53 | ServiceLocator.set(services: [TwitterSearchService.self], 54 | api: TwitterAPIConfigurationType.self, 55 | auth: TwitterApp.self, 56 | token: TwitterAPIToken.self) 57 | let searchService = ServiceLocator.service(forType: TwitterSearchService.self) 58 | expect(searchService).toNot(beNil()) 59 | expect(searchService!.isAuthenticated).to(beTrue()) 60 | } 61 | } 62 | 63 | context("if we have clear the token info") { 64 | it("should not be authenticated") { 65 | let dataResponse = ["refresh_token" : "DWALI", 66 | "token_type" : "access", 67 | "access_token" : "KWALI", 68 | "expires_in" : 100, 69 | "scope" : "mobile"] as [String : Any] 70 | APITokenManager.tokenType = TwitterAPIToken.self 71 | let apiToken = APITokenManager.store(tokenInfo: dataResponse) 72 | expect(apiToken).toNot(beNil()) 73 | APITokenManager.clearAuthentication() 74 | 75 | let accessToken = TwitterAPIToken.object(for: TwitterAPITokenKey.accessTokenKey) 76 | expect(accessToken).to(beNil()) 77 | 78 | let tokenType = TwitterAPIToken.object(for: TwitterAPITokenKey.tokenTypeKey) 79 | expect(tokenType).to(beNil()) 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Example/Tests/AlamoNetworkManagerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlamoNetworkManagerTests.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 4/5/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import Quick 11 | import Nimble 12 | import NetworkingServiceKit 13 | import Mockingjay 14 | 15 | public enum RandomConfigurationType: String, APIConfigurationType { 16 | case basic = "BASIC" 17 | 18 | public init(stringKey: String) { 19 | self = .basic 20 | } 21 | 22 | /// URL for given server 23 | public var URL: String { 24 | return "https://random.com" 25 | } 26 | 27 | /// Web URL for given server 28 | public var webURL: String { 29 | return "https://random.com" 30 | } 31 | 32 | /// Display name for given server 33 | public var displayName: String { 34 | return self.rawValue.capitalized 35 | } 36 | 37 | /// Explicitly tells our protocol which is our default configuration 38 | public static var defaultConfiguration: APIConfigurationType { 39 | return RandomConfigurationType.basic 40 | } 41 | } 42 | 43 | class RandomService : AbstractBaseService { 44 | override var serviceVersion: String { 45 | return "v3" 46 | } 47 | } 48 | 49 | class AlamoNetworkManagerStubDelegate: ServiceLocatorDelegate 50 | { 51 | func authenticationTokenDidExpire(forService service: Service) { 52 | 53 | } 54 | 55 | func shouldInterceptRequest(with request: URLRequest) -> Bool { 56 | if let requestPath = request.url?.absoluteString, requestPath.contains("requestToIntercept") { 57 | return true 58 | } 59 | return false 60 | } 61 | 62 | func processIntercept(for request: NSMutableURLRequest) -> URLRequest? { 63 | if let requestPath = request.url?.absoluteString { 64 | let newRequestPath = requestPath.replacingOccurrences(of: "requestToIntercept", with: "myInterceptedRequest") 65 | request.url = URL(string: newRequestPath) 66 | } 67 | 68 | return request as URLRequest 69 | } 70 | 71 | } 72 | 73 | class AlamoNetworkManagerTests: QuickSpec 74 | { 75 | let networkDelegate = AlamoNetworkManagerStubDelegate() 76 | override func spec() { 77 | let arrayResponse = [["param" : "value"],["param" : "value"]] as [[String : Any]] 78 | let dictionaryResponse = ["param" : "value"] as [String : Any] 79 | let dictionaryResponseUpdated = ["param" : "value2"] as [String : Any] 80 | let dictionaryNextResponse2 = ["next" : "https://random.com/v3/dictionary_next2", "results" : [["obj1" : "value"]]] as [String : Any] 81 | let dictionaryNextResponse3 = ["next" : "https://random.com/v3/dictionary_next3", "results" : [["obj2" : "value"]]] as [String : Any] 82 | let dictionaryNextResponse4 = ["results" : [["obj3" : "value"]]] as [String : Any] 83 | 84 | let responseIntercepted = ["intercept": true] as [String : Any] 85 | 86 | beforeEach { 87 | ServiceLocator.shouldInterceptRequests = false 88 | MockingjayProtocol.removeAllStubs() 89 | ServiceLocator.defaultNetworkClientType = AlamoNetworkManager.self 90 | ServiceLocator.set(services: [RandomService.self], 91 | api: RandomConfigurationType.self, 92 | auth: TwitterApp.self, 93 | token: TwitterAPIToken.self) 94 | } 95 | describe("when executing a request") { 96 | context("that returns an array") { 97 | 98 | it("should have a response dictionary with an array of results inside") { 99 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/array"), builder: json(arrayResponse)) 100 | 101 | waitUntil { done in 102 | let randomService = ServiceLocator.service(forType: RandomService.self) 103 | randomService?.request(path: "array", success: { response in 104 | let results = response["results"] as! [[String : Any]] 105 | expect(results).toNot(beNil()) 106 | done() 107 | }, failure: { error in 108 | }) 109 | } 110 | } 111 | } 112 | 113 | context("that returns a dictionary"){ 114 | it("should have a response dictionary with a dictionary response") { 115 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary"), builder: json(dictionaryResponse)) 116 | 117 | waitUntil { done in 118 | let randomService = ServiceLocator.service(forType: RandomService.self) 119 | randomService?.request(path: "dictionary", success: { response in 120 | expect(response).toNot(beNil()) 121 | done() 122 | }, failure: { error in 123 | }) 124 | } 125 | } 126 | } 127 | 128 | context("that returns a paginated dictionary") { 129 | it("should have a merged dictionary from all the requests") { 130 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next1"), builder: json(dictionaryNextResponse2)) 131 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next2"), builder: json(dictionaryNextResponse3)) 132 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next3"), builder: json(dictionaryNextResponse4)) 133 | 134 | waitUntil { done in 135 | let randomService = ServiceLocator.service(forType: RandomService.self) 136 | randomService?.request(path: "dictionary_next1", paginated:true, success: { response in 137 | expect(response["results"]).toNot(beNil()) 138 | let results = response["results"] as! [[String:Any]] 139 | expect(results.count).to(equal(3)) 140 | done() 141 | }, failure: { error in 142 | }) 143 | } 144 | } 145 | } 146 | 147 | context("that returns an empty response") { 148 | it("should have a empty dictionary") { 149 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/empty_dictionary"), builder: json([String : Any]())) 150 | 151 | waitUntil { done in 152 | let randomService = ServiceLocator.service(forType: RandomService.self) 153 | randomService?.request(path: "empty_dictionary", success: { response in 154 | expect(response).toNot(beNil()) 155 | expect(response.count).to(equal(0)) 156 | done() 157 | }, failure: { error in 158 | }) 159 | } 160 | } 161 | } 162 | 163 | context("that returns a 500") { 164 | it("should return an error of type .internalServerError") { 165 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/error500"), builder: http(500)) 166 | 167 | waitUntil { done in 168 | let randomService = ServiceLocator.service(forType: RandomService.self) 169 | randomService?.request(path: "error500", success: { response in 170 | }, failure: { error in 171 | expect(error.details.code).to(equal(500)) 172 | switch error.type { 173 | case .responseValidation(let reason): 174 | switch reason { 175 | case .internalServerError: done() 176 | default:break 177 | } 178 | default:break 179 | } 180 | }) 181 | } 182 | } 183 | } 184 | 185 | context("that returns a error on the response") { 186 | it("should return an error of type .badRequest") { 187 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/error500"), builder: http(410)) 188 | 189 | waitUntil { done in 190 | let randomService = ServiceLocator.service(forType: RandomService.self) 191 | randomService?.request(path: "error500", success: { response in 192 | }, failure: { error in 193 | expect(error.details.code).to(equal(410)) 194 | switch error.type { 195 | case .responseValidation(let reason): 196 | switch reason { 197 | case .badRequest: done() 198 | default:break 199 | } 200 | default:break 201 | } 202 | }) 203 | } 204 | } 205 | } 206 | 207 | context("that returns a 401") { 208 | it("should return an error of type .tokenExpired") { 209 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/error500"), builder: http(401)) 210 | 211 | waitUntil { done in 212 | let randomService = ServiceLocator.service(forType: RandomService.self) 213 | randomService?.request(path: "error500", success: { response in 214 | }, failure: { error in 215 | expect(error.details.code).to(equal(401)) 216 | switch error.type { 217 | case .responseValidation(let reason): 218 | switch reason { 219 | case .tokenExpired: done() 220 | default:break 221 | } 222 | default:break 223 | } 224 | }) 225 | } 226 | } 227 | } 228 | context("that returns a 404") { 229 | it("should return an error of type .notFound") { 230 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/error500"), builder: http(404)) 231 | 232 | waitUntil { done in 233 | let randomService = ServiceLocator.service(forType: RandomService.self) 234 | randomService?.request(path: "error500", success: { response in 235 | }, failure: { error in 236 | expect(error.details.code).to(equal(404)) 237 | switch error.type { 238 | case .responseValidation(let reason): 239 | switch reason { 240 | case .notFound: done() 241 | default:break 242 | } 243 | default:break 244 | } 245 | }) 246 | } 247 | } 248 | } 249 | 250 | context("that returns a 403") { 251 | it("should return an error of type .forbidden") { 252 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/error500"), builder: http(403)) 253 | 254 | waitUntil { done in 255 | let randomService = ServiceLocator.service(forType: RandomService.self) 256 | randomService?.request(path: "error500", success: { response in 257 | }, failure: { error in 258 | expect(error.details.code).to(equal(403)) 259 | switch error.type { 260 | case .responseValidation(let reason): 261 | switch reason { 262 | case .forbidden: done() 263 | default:break 264 | } 265 | default:break 266 | } 267 | }) 268 | } 269 | } 270 | } 271 | 272 | context("that is force cached") { 273 | it("should correctly store and return the cached request if the cache is valid") { 274 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary"), builder: json(dictionaryResponse)) 275 | 276 | waitUntil(timeout: .seconds(10), action: { done in 277 | let randomService = ServiceLocator.service(forType: RandomService.self) 278 | randomService?.request(path: "dictionary", cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 200), 279 | success: { response in 280 | expect(response).toNot(beNil()) 281 | let originalRequest = URLRequest(url: URL(string: "https://random.com/v3/dictionary")!) 282 | let cachedResponse = originalRequest.cachedJSONResponse() 283 | expect(cachedResponse).toNot(beNil()) 284 | let dic = cachedResponse as! [String:Any] 285 | expect(dic["param"] as? String).to(equal("value")) 286 | 287 | MockingjayProtocol.removeAllStubs() 288 | 289 | randomService?.request(path: "dictionary", cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 200), 290 | success: { response in 291 | expect(response).toNot(beNil()) 292 | expect(response["param"] as? String).to(equal("value")) 293 | done() 294 | }, failure: { error in 295 | }) 296 | }, failure: { error in 297 | }) 298 | }) 299 | } 300 | 301 | it("should fail if the cache has been invalidated") { 302 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionaryInvalidated"), builder: json(dictionaryResponse)) 303 | 304 | waitUntil(timeout:.seconds(10)) { done in 305 | let randomService = ServiceLocator.service(forType: RandomService.self) 306 | randomService?.request(path: "dictionaryInvalidated", cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 200), 307 | success: { response in 308 | expect(response).toNot(beNil()) 309 | let originalRequest = URLRequest(url: URL(string: "https://random.com/v3/dictionaryInvalidated")!) 310 | let cachedResponse = originalRequest.cachedJSONResponse() 311 | expect(cachedResponse).toNot(beNil()) 312 | 313 | MockingjayProtocol.removeAllStubs() 314 | randomService?.request(path: "dictionaryInvalidated", 315 | cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 200), 316 | success: { response in 317 | done() 318 | }, failure: { error in 319 | //the request to network fails since there is no stub and no cache 320 | done() 321 | }) 322 | }, failure: { error in 323 | done() 324 | }) 325 | } 326 | } 327 | 328 | it("should correctly return cached responses from paginated requests") { 329 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next1"), builder: json(dictionaryNextResponse2)) 330 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next2"), builder: json(dictionaryNextResponse3)) 331 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary_next3"), builder: json(dictionaryNextResponse4)) 332 | 333 | waitUntil { done in 334 | let randomService = ServiceLocator.service(forType: RandomService.self) 335 | randomService?.request(path: "dictionary_next1", 336 | paginated:true, 337 | cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 2), 338 | success: { response in 339 | expect(response["results"]).toNot(beNil()) 340 | let results = response["results"] as! [[String:Any]] 341 | expect(results.count).to(equal(3)) 342 | 343 | let originalRequest1 = URLRequest(url: URL(string: "https://random.com/v3/dictionary_next1")!) 344 | expect(originalRequest1.cachedJSONResponse()).toNot(beNil()) 345 | let originalRequest2 = URLRequest(url: URL(string: "https://random.com/v3/dictionary_next2")!) 346 | expect(originalRequest2.cachedJSONResponse()).toNot(beNil()) 347 | let originalRequest3 = URLRequest(url: URL(string: "https://random.com/v3/dictionary_next3")!) 348 | expect(originalRequest3.cachedJSONResponse()).toNot(beNil()) 349 | 350 | //Since we are cache now, the stubs should not be needed 351 | MockingjayProtocol.removeAllStubs() 352 | 353 | //paginated request should work through cache 354 | randomService?.request(path: "dictionary_next1", 355 | paginated:true, 356 | cachePolicy:CacheResponsePolicy(type: .forceCacheDataElseLoad, maxAge: 2), 357 | success: { response in 358 | expect(response["results"]).toNot(beNil()) 359 | let results = response["results"] as? [[String:Any]] 360 | expect(results?.count).to(equal(3)) 361 | done() 362 | }, failure: { error in 363 | }) 364 | }, failure: { error in 365 | }) 366 | } 367 | 368 | } 369 | 370 | it("should correctly revalidates cache data when using cache policy: .reloadRevalidatingForceCacheData") { 371 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary"), builder: json(dictionaryResponse)) 372 | 373 | waitUntil { done in 374 | let randomService = ServiceLocator.service(forType: RandomService.self) 375 | randomService?.request(path: "dictionary", cachePolicy:CacheResponsePolicy(type: .reloadRevalidatingForceCacheData, maxAge: 2), 376 | success: { response in 377 | expect(response).toNot(beNil()) 378 | let originalRequest = URLRequest(url: URL(string: "https://random.com/v3/dictionary")!) 379 | let cachedResponse = originalRequest.cachedJSONResponse() 380 | expect(cachedResponse).toNot(beNil()) 381 | let dic = cachedResponse as! [String:Any] 382 | expect(dic["param"] as? String).to(equal("value")) 383 | 384 | MockingjayProtocol.removeAllStubs() 385 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/dictionary"), builder: json(dictionaryResponseUpdated)) 386 | randomService?.request(path: "dictionary", 387 | cachePolicy:CacheResponsePolicy(type: .reloadRevalidatingForceCacheData, maxAge: 2), 388 | success: { response in 389 | expect(response).toNot(beNil()) 390 | let originalRequest = URLRequest(url: URL(string: "https://random.com/v3/dictionary")!) 391 | let cachedResponse = originalRequest.cachedJSONResponse() 392 | expect(cachedResponse).toNot(beNil()) 393 | let dic = cachedResponse as! [String:Any] 394 | expect(dic["param"] as? String).to(equal("value2")) 395 | done() 396 | }, failure: { error in 397 | }) 398 | }, failure: { error in 399 | }) 400 | } 401 | } 402 | } 403 | 404 | context("that is intercepted") { 405 | it("should correctly return the intercepted request") { 406 | ServiceLocator.shouldInterceptRequests = true 407 | ServiceLocator.setDelegate(delegate: self.networkDelegate) 408 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/requestToIntercept"), builder: json([:])) 409 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/v3/myInterceptedRequest"), builder: json(responseIntercepted)) 410 | 411 | let randomService = ServiceLocator.service(forType: RandomService.self) 412 | waitUntil { done in 413 | randomService?.request(path: "requestToIntercept", success: { response in 414 | expect(response["intercept"]).toNot(beNil()) 415 | done() 416 | }, failure: { error in 417 | }) 418 | } 419 | } 420 | } 421 | } 422 | } 423 | } 424 | -------------------------------------------------------------------------------- /Example/Tests/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 | -------------------------------------------------------------------------------- /Example/Tests/NetworkingServiceLocatorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceLocatorTests.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 4/4/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import NetworkingServiceKit 12 | 13 | class ServiceLocatorSpec: QuickSpec { 14 | 15 | override func spec() { 16 | 17 | beforeEach { 18 | ServiceLocator.defaultNetworkClientType = StubNetworkManager.self 19 | ServiceLocator.reset() 20 | } 21 | describe("Requesting for a service should be not nil if the service has been loaded") { 22 | context("when service has been loaded") { 23 | 24 | it("should return a properly setup service") { 25 | ServiceLocator.set(services: [TwitterAuthenticationService.self], 26 | api: TwitterAPIConfigurationType.self, 27 | auth: TwitterApp.self, 28 | token: TwitterAPIToken.self) 29 | let authService = ServiceLocator.service(forType: TwitterAuthenticationService.self) 30 | expect(authService).toNot(beNil()) 31 | } 32 | 33 | it("should return a properly setup service from a given class name as well") { 34 | ServiceLocator.set(services: [TwitterAuthenticationService.self], 35 | api: TwitterAPIConfigurationType.self, 36 | auth: TwitterApp.self, 37 | token: TwitterAPIToken.self) 38 | let authService = ServiceLocator.service(forClassName: "TwitterAuthenticationService") 39 | expect(authService).toNot(beNil()) 40 | } 41 | 42 | it("should return a properly setup service from a given class name as well even when including the module name") { 43 | ServiceLocator.set(services: [TwitterAuthenticationService.self], 44 | api: TwitterAPIConfigurationType.self, 45 | auth: TwitterApp.self, 46 | token: TwitterAPIToken.self) 47 | let authService = ServiceLocator.service(forClassName: "NetworkingServiceKit.TwitterAuthenticationService") 48 | expect(authService).toNot(beNil()) 49 | } 50 | } 51 | 52 | context("when service has not been loaded") { 53 | it("should NOT return a service for a service that has not been loaded") { 54 | ServiceLocator.set(services: [TwitterSearchService.self], 55 | api: TwitterAPIConfigurationType.self, 56 | auth: TwitterApp.self, 57 | token: TwitterAPIToken.self) 58 | let authService = ServiceLocator.service(forType: TwitterSearchService.self) 59 | expect(authService).toNot(beNil()) 60 | } 61 | 62 | it("should NOT return a service for a wrong service name") { 63 | ServiceLocator.set(services: [TwitterSearchService.self], 64 | api: TwitterAPIConfigurationType.self, 65 | auth: TwitterApp.self, 66 | token: TwitterAPIToken.self) 67 | let authService = ServiceLocator.service(forClassName: "TwitterSearchService") 68 | expect(authService).toNot(beNil()) 69 | } 70 | } 71 | } 72 | 73 | describe("Services should get clear after resetting") { 74 | context("when loading a service after resetting") { 75 | ServiceLocator.set(services: [TwitterAuthenticationService.self], 76 | api: TwitterAPIConfigurationType.self, 77 | auth: TwitterApp.self, 78 | token: TwitterAPIToken.self) 79 | ServiceLocator.reset() 80 | let authService = ServiceLocator.service(forType: TwitterAuthenticationService.self) 81 | expect(authService).to(beNil()) 82 | } 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Example/Tests/TwitterAuthenticationServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterAuthenticationServiceTests.swift 3 | // NetworkingServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/5/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import NetworkingServiceKit 12 | 13 | class TwitterAuthenticationServiceTests: QuickSpec, ServiceLocatorDelegate { 14 | 15 | var delegateGot401 = false 16 | 17 | override func spec() { 18 | 19 | let authStub = ServiceStub(execute: ServiceStubRequest(path: "/oauth2/token"), 20 | with: .success(code: 200, response: ["token_type" : "access", "access_token" : "KWALI"]), 21 | when: .unauthenticated, 22 | react:.immediate) 23 | let logoutStub = ServiceStub(execute: ServiceStubRequest(path: "/oauth2/invalidate_token"), 24 | with: .success(code: 200, response: [:]), 25 | when: .unauthenticated, 26 | react:.immediate) 27 | let logoutStubUnauthenticated = ServiceStub(execute: ServiceStubRequest(path: "/oauth2/invalidate_token"), 28 | with: .failure(code:401, response:[:]), 29 | when: .unauthenticated, 30 | react:.immediate) 31 | let logoutStubAuthenticated = ServiceStub(execute: ServiceStubRequest(path: "/oauth2/invalidate_token"), 32 | with: .failure(code:401, response:[:]), 33 | when: .authenticated(tokenInfo: ["token_type" : "access", "access_token" : "KWALI"]), 34 | react:.immediate) 35 | 36 | beforeEach { 37 | ServiceLocator.defaultNetworkClientType = StubNetworkManager.self 38 | self.delegateGot401 = false 39 | UserDefaults.clearServiceLocatorUserDefaults() 40 | APITokenManager.clearAuthentication() 41 | ServiceLocator.reset() 42 | ServiceLocator.set(services: [TwitterAuthenticationService.self], 43 | api: TwitterAPIConfigurationType.self, 44 | auth: TwitterApp.self, 45 | token: TwitterAPIToken.self) 46 | } 47 | 48 | describe("when a user is authenticating through our Authenticate service") { 49 | context("and the credentials are correct") { 50 | 51 | it("should be authenticated") { 52 | 53 | let authenticationService = ServiceLocator.service(forType: TwitterAuthenticationService.self, stubs: [authStub]) 54 | authenticationService?.authenticateTwitterClient(completion: { authenticated in 55 | expect(authenticated).to(beTrue()) 56 | }) 57 | } 58 | } 59 | } 60 | 61 | describe("when the current user is already authenticated") { 62 | context("and is trying to logout") { 63 | it("should clear token data") { 64 | let authenticationService = ServiceLocator.service(forType: TwitterAuthenticationService.self, stubs: [logoutStub]) 65 | authenticationService?.logoutTwitterClient(completion: { loggedOut in 66 | expect(loggedOut).to(beTrue()) 67 | }) 68 | } 69 | } 70 | } 71 | 72 | describe("when the current user is NOT authenticated") { 73 | context("and is trying to logout") { 74 | it("should not return succesfully") { 75 | let authenticationService = ServiceLocator.service(forType: TwitterAuthenticationService.self, stubs: [logoutStubUnauthenticated]) 76 | authenticationService?.logoutTwitterClient(completion: { loggedOut in 77 | expect(loggedOut).to(beFalse()) 78 | }) 79 | } 80 | } 81 | } 82 | 83 | describe("when we are authenticated but the token has expired") { 84 | context("and is trying to logout") { 85 | it("should report to the delegate there is a token expired") { 86 | 87 | //set ourselves as delegate 88 | ServiceLocator.setDelegate(delegate: self) 89 | let authenticationService = ServiceLocator.service(forType: TwitterAuthenticationService.self, stubs: [logoutStubAuthenticated]) 90 | authenticationService?.logoutTwitterClient(completion: { loggedOut in 91 | expect(loggedOut).to(beFalse()) 92 | expect(self.delegateGot401).to(beTrue()) 93 | }) 94 | } 95 | } 96 | } 97 | } 98 | 99 | // MARK: ServiceLocatorDelegate 100 | func authenticationTokenDidExpire(forService service: Service) { 101 | self.delegateGot401 = true 102 | } 103 | 104 | func shouldInterceptRequest(with request: URLRequest) -> Bool { 105 | return false 106 | } 107 | 108 | func processIntercept(for request: NSMutableURLRequest) -> URLRequest? { 109 | return nil 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Example/Tests/TwitterSearchServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TwitterSearchServiceTests.swift 3 | // MakespaceServiceKit 4 | // 5 | // Created by Phillipe Casorla Sagot on 4/25/17. 6 | // Copyright © 2017 Makespace Inc. All rights reserved. 7 | // 8 | 9 | import Quick 10 | import Nimble 11 | import NetworkingServiceKit 12 | import Mockingjay 13 | 14 | class TwitterSearchServiceTestsStubbed: QuickSpec { 15 | 16 | var searchService:TwitterSearchService? 17 | override func spec() { 18 | 19 | let searchStub = ServiceStub(execute: ServiceStubRequest(path: "/1.1/search/tweets.json", parameters: ["q" : "#makespace"]), 20 | with: .success(code: 200, response: ["statuses" : [["text" : "tweet1" , 21 | "user" : ["screen_name" : "darkzlave", 22 | "profile_image_url_https" : "https://lol.png", 23 | "location" : "Stockholm, Sweden"]], 24 | ["text" : "tweet2" , 25 | "user" : ["screen_name" : "makespace", 26 | "profile_image_url_https" : "https://lol2.png", 27 | "location" : "New York"]]], 28 | "search_metadata" : ["next_results" : "https://search.com/next?pageId=2"] 29 | ]), when: .unauthenticated, 30 | react:.delayed(seconds: 0.5)) 31 | let searchStubFromJSON = ServiceStub(execute: ServiceStubRequest(path: "/1.1/search/tweets.json", parameters: ["q" : "#makespaceFromJSON"]), 32 | with: ServiceStubType(buildWith: "twitterSearch",http:200), 33 | when: .unauthenticated, 34 | react:.delayed(seconds: 0.5)) 35 | let searchEmptyStub = ServiceStub(execute: ServiceStubRequest(path: "/1.1/search/tweets.json", parameters: ["q" : ""]), 36 | with: .success(code: 200, response: [:]), 37 | when: .unauthenticated, 38 | react:.immediate) 39 | let searchFail = ServiceStub(execute: ServiceStubRequest(path: "/1.1/search/tweets.json", parameters: ["q" : "#random"]), 40 | with: .failure(code:500, response:[:]), 41 | when: .unauthenticated, 42 | react:.immediate) 43 | 44 | beforeEach { 45 | ServiceLocator.defaultNetworkClientType = StubNetworkManager.self 46 | ServiceLocator.reset() 47 | ServiceLocator.set(services: [TwitterSearchService.self], 48 | api: TwitterAPIConfigurationType.self, 49 | auth: TwitterApp.self, 50 | token: TwitterAPIToken.self) 51 | self.searchService = ServiceLocator.service(forType: TwitterSearchService.self, stubs: [searchStub,searchEmptyStub,searchFail,searchStubFromJSON]) 52 | } 53 | describe("when doing a search request") { 54 | context("with a proper query") { 55 | it("should correctly parse and return the search results as objects") { 56 | waitUntil { done in 57 | self.searchService?.searchRecents(by: "#makespace", completion: { results in 58 | expect(results.count).to(equal(2)) 59 | let resultFirst = results.first 60 | expect(resultFirst).toNot(beNil()) 61 | expect(resultFirst?.tweet).to(equal("tweet1")) 62 | expect(resultFirst?.user.handle).to(equal("darkzlave")) 63 | expect(resultFirst?.user.imagePath).to(equal("https://lol.png")) 64 | done() 65 | }) 66 | } 67 | } 68 | } 69 | 70 | 71 | context("with an empty query") { 72 | it("should return immediatly with no results") { 73 | self.searchService?.searchRecents(by: "", completion: { results in 74 | expect(results.count).to(equal(0)) 75 | }) 76 | } 77 | } 78 | } 79 | 80 | describe("when doing a failed search request") { 81 | context("with a proper query") { 82 | it("should return no results") { 83 | self.searchService?.searchRecents(by: "#random", completion: { results in 84 | expect(results.count).to(equal(0)) 85 | }) 86 | } 87 | } 88 | } 89 | 90 | describe("when doing a search request through a JSON response") { 91 | context("with a proper query") { 92 | it("should correctly parse and return the search results as objects") { 93 | waitUntil { done in 94 | self.searchService?.searchRecents(by: "#makespaceFromJSON", completion: { results in 95 | expect(results.count).to(equal(3)) 96 | let resultFirst = results.first 97 | expect(resultFirst).toNot(beNil()) 98 | expect(resultFirst?.tweet).to(equal("tweet1")) 99 | expect(resultFirst?.user.handle).to(equal("darkzlave")) 100 | expect(resultFirst?.user.imagePath).to(equal("https://lol.png")) 101 | done() 102 | }) 103 | } 104 | } 105 | } 106 | 107 | } 108 | } 109 | } 110 | 111 | class TwitterSearchServiceTestsAlamo: QuickSpec { 112 | override func spec() { 113 | 114 | beforeEach { 115 | ServiceLocator.defaultNetworkClientType = AlamoNetworkManager.self 116 | ServiceLocator.reset() 117 | ServiceLocator.set(services: [TwitterSearchService.self], 118 | api: TwitterAPIConfigurationType.self, 119 | auth: TwitterApp.self, 120 | token: TwitterAPIToken.self) 121 | } 122 | 123 | describe("when doing a search request") { 124 | context("with a next page available") { 125 | it("should correctly parse and return the search results as objects") { 126 | let searchResponse = ["statuses" : [["text" : "tweet1" , 127 | "user" : ["screen_name" : "darkzlave", 128 | "profile_image_url_https" : "https://lol.png", 129 | "location" : "Stockholm, Sweden"]], 130 | ["text" : "tweet2" , 131 | "user" : ["screen_name" : "makespace", 132 | "profile_image_url_https" : "https://lol2.png", 133 | "location" : "New York"]]], 134 | "search_metadata" : ["next_results" : "?id=2"] 135 | ] as [String : Any] 136 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/1.1/search/tweets.json"), builder: json(searchResponse)) 137 | MockingjayProtocol.addStub(matcher: http(.get, uri: "/1.1/search/tweets.json?id=2"), builder: json(searchResponse)) 138 | 139 | waitUntil { done in 140 | let searchService = ServiceLocator.service(forType: TwitterSearchService.self) 141 | searchService?.searchRecents(by: "#makespace", completion: { results in 142 | searchService?.searchNextRecentsPageProducer().on(value: { results in 143 | expect(results.count).to(equal(2)) 144 | let resultFirst = results.first 145 | expect(resultFirst).toNot(beNil()) 146 | expect(resultFirst?.tweet).to(equal("tweet1")) 147 | expect(resultFirst?.user.handle).to(equal("darkzlave")) 148 | expect(resultFirst?.user.imagePath).to(equal("https://lol.png")) 149 | done() 150 | }).start() 151 | }) 152 | } 153 | } 154 | } 155 | 156 | context("with a next page NON available") { 157 | it("return empty results") { 158 | let searchService = ServiceLocator.service(forType: TwitterSearchService.self) 159 | searchService?.searchNextRecentsPageProducer().on(value: { results in 160 | expect(results.count).to(equal(0)) 161 | }).start() 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Example/Tests/twitterSearch.json: -------------------------------------------------------------------------------- 1 | { 2 | "statuses": [{ 3 | "text": "tweet1", 4 | "user": { 5 | "screen_name": "darkzlave", 6 | "profile_image_url_https": "https://lol.png", 7 | "location": "Stockholm, Sweden" 8 | } 9 | }, 10 | { 11 | "text": "tweet2", 12 | "user": { 13 | "screen_name": "makespace", 14 | "profile_image_url_https": "https://lol2.png", 15 | "location": "New York" 16 | } 17 | }, 18 | { 19 | "text": "tweet3", 20 | "user": { 21 | "screen_name": "makespace3", 22 | "profile_image_url_https": "https://lol3.png", 23 | "location": "San Francisco, CA" 24 | } 25 | } 26 | ], 27 | "search_metadata": { 28 | "next_results": "https://search.com/next?pageId=2" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 darkzlave 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /NetworkingServiceKit.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod lib lint NetworkingServiceKit.podspec' to ensure this is a 3 | # valid spec before submitting. 4 | # 5 | # Any lines starting with a # are optional, but their use is encouraged 6 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | s.name = 'NetworkingServiceKit' 11 | s.version = '1.8.2' 12 | s.summary = 'A service layer of networking microservices for iOS.' 13 | 14 | # This description is used to generate tags and improve search results. 15 | # * Think: What does it do? Why did you write it? What is the focus? 16 | # * Try to keep it short, snappy and to the point. 17 | # * Write the description between the DESC delimiters below. 18 | # * Finally, don't worry about the indent, CocoaPods strips it! 19 | 20 | s.description = <<-DESC 21 | NetworkingServiceKit is the reincarnation of the standard iOS monolith api client. Using a modular approach to services, the framework enables the user to select which services they will need to have running. Also, NetworkingServiceKit takes a different approach when it comes to using Network Clients like AFNetworking/Alamofire. All requests are routed through a protocol, which makes the library loosely coupled from the networking implementation. 22 | DESC 23 | 24 | s.homepage = 'https://github.com/makingspace/NetworkingServiceKit' 25 | s.license = { :type => 'MIT', :file => 'LICENSE' } 26 | s.author = { 'darkzlave' => 'phillipe@makespace.com' } 27 | s.source = { :git => 'https://github.com/makingspace/NetworkingServiceKit.git', :tag => s.version.to_s } 28 | s.social_media_url = 'https://twitter.com/darkzlave' 29 | s.pod_target_xcconfig = { 30 | 'OTHER_SWIFT_FLAGS[config=Debug]' => '-D DEBUG', 31 | 'OTHER_SWIFT_FLAGS[config=Staging]' => '-D STAGING' 32 | } 33 | s.ios.deployment_target = '10.0' 34 | s.default_subspec = 'Core' 35 | 36 | s.subspec 'Core' do |core| 37 | core.source_files = 'NetworkingServiceKit/Classes/**/*' 38 | 39 | core.dependency 'Alamofire' 40 | core.dependency 'SwiftyJSON' 41 | end 42 | 43 | s.subspec 'ReactiveSwift' do |reactive_swift| 44 | reactive_swift.source_files = 'NetworkingServiceKit/Classes/**/*', 'NetworkingServiceKit/ReactiveSwift/*' 45 | 46 | reactive_swift.dependency 'Alamofire' 47 | reactive_swift.dependency 'AlamofireImage' 48 | reactive_swift.dependency 'CryptoSwift' 49 | reactive_swift.dependency 'SwiftyJSON' 50 | reactive_swift.dependency 'ReactiveSwift' 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/NetworkingServiceKit/Assets/.gitkeep -------------------------------------------------------------------------------- /NetworkingServiceKit/Assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/NetworkingServiceKit/Assets/logo.png -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/makingspace/NetworkingServiceKit/32c07840b80c2de762c0e72c53af51a3aeeae715/NetworkingServiceKit/Classes/.gitkeep -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/ArrayExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayExtensions.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 3/13/17. 6 | // 7 | // 8 | 9 | import UIKit 10 | 11 | import Foundation 12 | import Alamofire 13 | 14 | internal let arrayParametersKey = "arrayParametersKey" 15 | 16 | /// Extenstion that allows an array be sent as a request parameters 17 | extension Array { 18 | /// Convert the receiver array to a `Parameters` object. 19 | public func asParameters() -> Parameters { 20 | return [arrayParametersKey: self] 21 | } 22 | } 23 | 24 | /// Convert the parameters into a json array, and it is added as the request body. 25 | /// The array must be sent as parameters using its `asParameters` method. 26 | public struct ArrayEncoding: ParameterEncoding { 27 | 28 | /// The options for writing the parameters as JSON data. 29 | public let options: JSONSerialization.WritingOptions 30 | 31 | /// Creates a new instance of the encoding using the given options 32 | /// 33 | /// - parameter options: The options used to encode the json. Default is `[]` 34 | /// 35 | /// - returns: The new instance 36 | public init(options: JSONSerialization.WritingOptions = []) { 37 | self.options = options 38 | } 39 | 40 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { 41 | var urlRequest = try urlRequest.asURLRequest() 42 | 43 | guard let parameters = parameters, 44 | let array = parameters[arrayParametersKey] else { 45 | return urlRequest 46 | } 47 | 48 | do { 49 | let data = try JSONSerialization.data(withJSONObject: array, options: options) 50 | 51 | if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil { 52 | urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type") 53 | } 54 | 55 | urlRequest.httpBody = data 56 | 57 | } catch { 58 | throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error)) 59 | } 60 | 61 | return urlRequest 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/BundleExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BundleExtensions.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/6/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Bundle { 12 | 13 | /// Returns a non optional bundle identifier, if bundle identifier is nil it will return `"com.app"` 14 | public var appBundleIdentifier: String { 15 | return self.bundleIdentifier ?? "com.app" 16 | } 17 | 18 | //Returns a non optional executable name of the target 19 | public var bundleExecutableName: String { 20 | return self.infoDictionary?["CFBundleExecutable"] as? String ?? "com.app" 21 | } 22 | 23 | //Returns a a non optional bundleVersion, it uses the marketing version, e.g. 1.2.0, and if not found it uses the bundle version, which is normally the build number, e.g. 7. in 1.2.0(7) 24 | public var bundleVersion: String { 25 | let marketingVersion = infoDictionary?["CFBundleShortVersionString"] as? String //e.g. 1.2.0 26 | let buildNumber = self.infoDictionary?["CFBundleVersion"] as? String//e.g. 0 27 | 28 | return marketingVersion ?? buildNumber ?? "1.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/DictionaryExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DictionaryExtensions.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/24/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Dictionary where Key: ExpressibleByStringLiteral, Key.StringLiteralType == String, Value: Any { 12 | 13 | /// Validates if this dictionary is a masked array with key arrayParametersKey 14 | /// 15 | /// - Returns: true if the dictionary includes an array for the expected key 16 | func validArrayRequest() -> Bool { 17 | let key = Key(stringLiteral: arrayParametersKey) 18 | if self[key] != nil { 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | /// Validates if this dictionary is a masked string with key stringParametersKey 25 | /// 26 | /// - Returns: true if the dictionary includes a string with the expected key 27 | func validStringRequest() -> Bool { 28 | let key = Key(stringLiteral: stringParametersKey) 29 | if self[key] != nil { 30 | return true 31 | } 32 | return false 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/HTTPURLResponseExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HTTPURLResponseExtensions.swift 3 | // Pods 4 | // 5 | // Created by Phillipe Casorla Sagot on 7/12/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension HTTPURLResponse { 12 | 13 | /// By default our URLSessionConfiguration uses NSURLRequest.CachePolicy.useProtocolCachePolicy 14 | /// this means our policy for caching requests is interpreted according to the "Cache-Control" header field in the response. 15 | /// This property indicates if the response has no cache policy 16 | internal var isResponseCachePolicyDisabled:Bool { 17 | let responseHeaders = self.allHeaderFields 18 | if let cacheControl = responseHeaders["Cache-Control"] as? String { 19 | return cacheControl.contains("no-cache") || cacheControl.contains("max-age=0") 20 | } else if responseHeaders["Cache-Control"] == nil { 21 | return true 22 | } 23 | return false 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/NSErrorExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSErrorExtensions 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/5/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension NSError { 12 | 13 | /// A key for userInfo to save our error message from the server 14 | @objc static var messageKey: String { 15 | return "message" 16 | } 17 | 18 | /// Returns a user friendly error message from the error response 19 | @objc public var errorMessage: String { 20 | return self.userInfo[NSError.messageKey] as? String ?? "" 21 | } 22 | 23 | /// Builds an NSError object from our MSError 24 | /// 25 | /// - Parameters: 26 | /// - msError: an error type 27 | /// - Returns: A valid NSError with attached extra information on the userInfo 28 | @objc public static func make(from msError: MSError) -> NSError { 29 | var errorMessage = msError.details.message 30 | errorMessage = errorMessage.replacingOccurrences(of: "[u\'", with: "") 31 | errorMessage = errorMessage.replacingOccurrences(of: "\']", with: "") 32 | return NSError(domain: Bundle.main.appBundleIdentifier, 33 | code: msError.details.code, 34 | userInfo: [NSError.messageKey: errorMessage]) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/StringExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StringExtensions.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/24/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | internal let stringParametersKey = "stringParametersKey" 13 | 14 | /// Extenstion that allows a string to be sent as a request parameters 15 | extension String { 16 | /// Convert the receiver string to a `Parameters` object. 17 | public func asParameters() -> Parameters { 18 | return [stringParametersKey: self] 19 | } 20 | 21 | /// Returns a dictionary representing the JSON response if the string conforms to the format 22 | var jsonDictionary : [String: Any]? { 23 | if let data = data(using: .utf8) { 24 | do { 25 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 26 | } catch { 27 | print(error.localizedDescription) 28 | } 29 | } 30 | return nil 31 | 32 | } 33 | } 34 | 35 | /// Convert the parameters into a string body, and it is added as the request body. 36 | /// The string must be sent as parameters using its `asParameters` method. 37 | public struct StringEncoding: ParameterEncoding { 38 | 39 | /// The options for writing the parameters as JSON data. 40 | public let options: JSONSerialization.WritingOptions 41 | 42 | /// Creates a new instance of the encoding using the given options 43 | /// 44 | /// - parameter options: The options used to encode the json. Default is `[]` 45 | /// 46 | /// - returns: The new instance 47 | public init(options: JSONSerialization.WritingOptions = []) { 48 | self.options = options 49 | } 50 | 51 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { 52 | var urlRequest = try urlRequest.asURLRequest() 53 | 54 | guard let parameters = parameters, 55 | let stringBody = parameters[stringParametersKey] as? String else { 56 | return urlRequest 57 | } 58 | 59 | urlRequest.httpBody = stringBody.data(using: .utf8) 60 | 61 | return urlRequest 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/URLRequestExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URLRequestExtensions.swift 3 | // Pods 4 | // 5 | // Created by Phillipe Casorla Sagot on 7/12/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension URLRequest { 12 | 13 | /// Returns a cached JSON response for the the current request if we have stored it before and it hasn't expired 14 | /// 15 | /// - Returns: the cached JSON response 16 | public func cachedJSONResponse() -> Any? { 17 | guard let cachedResponse = URLCache.shared.cachedResponse(for: self), 18 | let userInfo = cachedResponse.userInfo, 19 | let startTime = userInfo[CachedURLResponse.CacheURLResponseTime] as? Double, 20 | let maxAge = userInfo[CachedURLResponse.CacheURLResponseMaxAge] as? Double, 21 | CFAbsoluteTimeGetCurrent() - startTime <= maxAge else { 22 | return nil 23 | } 24 | 25 | // Don't process requests that should be intercepted 26 | if ServiceLocator.shouldInterceptRequests, 27 | let delegate = ServiceLocator.shared.delegate, 28 | delegate.shouldInterceptRequest(with: self) { 29 | return nil 30 | } 31 | 32 | let data = cachedResponse.data 33 | let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments) 34 | return json 35 | } 36 | 37 | /// Extracts a path and a body from a URLRequest 38 | var components: (path: String?, body: String?) { 39 | let path = url?.absoluteString 40 | var body:String? = nil 41 | 42 | if let httpBody = httpBody { 43 | body = String(data: httpBody, encoding: String.Encoding.utf8) 44 | } 45 | 46 | return (path: path, body: body) 47 | } 48 | } 49 | 50 | extension CachedURLResponse { 51 | 52 | internal static let CacheURLResponseMaxAge = "maxAge" 53 | internal static let CacheURLResponseTime = "time" 54 | 55 | 56 | /// Creates a CachedURLResponse with attached information for maxAge and current time the response got saved 57 | /// 58 | /// - Parameters: 59 | /// - response: a network responwe 60 | /// - data: response's data 61 | /// - maxAge: max age the response will be valid before expiring 62 | internal convenience init(response: URLResponse, data: Data, maxAge:Double) { 63 | self.init(response: response, 64 | data: data, 65 | userInfo: [ 66 | CachedURLResponse.CacheURLResponseTime : CFAbsoluteTimeGetCurrent(), 67 | CachedURLResponse.CacheURLResponseMaxAge : maxAge], 68 | storagePolicy: .allowed) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Extensions/UserDefaultsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDefaultsExtensions.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 4/25/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension UserDefaults { 12 | 13 | /// Defines a custom UserDefaults to be used by the serviceLocator 14 | open class var serviceLocator: UserDefaults? { 15 | return UserDefaults(suiteName: "\(Bundle.main.appBundleIdentifier).serviceLocator)") 16 | } 17 | 18 | /// Clears our service locator's user defaults 19 | open class func clearServiceLocatorUserDefaults() { 20 | UserDefaults().removePersistentDomain(forName: "\(Bundle.main.appBundleIdentifier).serviceLocator)") 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/APIConfigurations.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIConfiguration.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | let APIConfigurationKey = "Server" 13 | 14 | /// Protocol for describing the necessary authentication key/secret to use when authenticating a client 15 | public protocol APIConfigurationAuth { 16 | var secret: String { get } 17 | var key: String { get } 18 | init(bundleId: String?) 19 | } 20 | 21 | /// Protocol for describing a server connection 22 | public protocol APIConfigurationType { 23 | var URL: String { get } 24 | var webURL: String { get } 25 | static var defaultConfiguration: APIConfigurationType { get } 26 | init(stringKey: String) 27 | } 28 | 29 | /// Handles the current server connection and authentication for a valid APIConfigurationType and APIConfigurationAuth 30 | @objc public class APIConfiguration: NSObject { 31 | public static var apiConfigurationType: APIConfigurationType.Type? 32 | public static var authConfigurationType: APIConfigurationAuth.Type? 33 | @objc public let baseURL: String 34 | @objc public let webURL: String 35 | @objc public let APIKey: String 36 | @objc public let APISecret: String 37 | 38 | internal init(type: APIConfigurationType, auth: APIConfigurationAuth) { 39 | self.baseURL = type.URL 40 | self.webURL = type.webURL 41 | self.APIKey = auth.key 42 | self.APISecret = auth.secret 43 | } 44 | private override init() { 45 | self.baseURL = "" 46 | self.webURL = "" 47 | self.APIKey = "" 48 | self.APISecret = "" 49 | } 50 | 51 | /// Returns the current APIConfiguration, either staging or production 52 | @objc public static var current: APIConfiguration { 53 | return current() 54 | } 55 | 56 | public static func currentConfigurationType(with configuration: APIConfigurationType.Type) -> APIConfigurationType { 57 | let environmentDictionary = ProcessInfo.processInfo.environment 58 | if let environmentConfiguration = environmentDictionary[APIConfigurationKey] { 59 | return configuration.init(stringKey: environmentConfiguration) 60 | } 61 | 62 | return configuration.defaultConfiguration 63 | } 64 | 65 | @objc public static func current(bundleId: String = Bundle.main.appBundleIdentifier) -> APIConfiguration { 66 | guard let configurationType = APIConfiguration.apiConfigurationType, 67 | let authType = APIConfiguration.authConfigurationType else { 68 | fatalError("Error: ServiceLocator couldn't find the current APIConfiguration, make sure to define your own types for APIConfiguration.apiConfigurationType and APIConfiguration.authConfigurationType") 69 | } 70 | return APIConfiguration(type: self.currentConfigurationType(with: configurationType), 71 | auth: authType.init(bundleId: bundleId)) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/APIToken.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIToken.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol for describing token handling for our service locator 12 | @objc 13 | public protocol APIToken { 14 | var authorization: String { get } 15 | var refresh: String? { get } 16 | static func makePersistedToken() -> APIToken? 17 | static func make(from tokenResponse: Any) -> APIToken? 18 | static func clearToken() 19 | } 20 | 21 | /// Handles storing and clearing an existing APIToken implementation 22 | @objc 23 | open class APITokenManager: NSObject { 24 | @objc public static var tokenType: APIToken.Type? 25 | 26 | @objc public static var currentToken: APIToken? { 27 | if let tokenType = APITokenManager.tokenType { 28 | return tokenType.makePersistedToken() 29 | } 30 | return nil 31 | } 32 | 33 | public static var isAuthenticated:Bool { 34 | return APITokenManager.currentToken != nil 35 | } 36 | 37 | @discardableResult 38 | public static func store(tokenInfo: Any) -> APIToken? { 39 | if let type = APITokenManager.tokenType { 40 | return type.make(from: tokenInfo) 41 | } 42 | return nil 43 | } 44 | 45 | public static func clearAuthentication() { 46 | if let tokenType = APITokenManager.tokenType { 47 | tokenType.clearToken() 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/AlamoAuthenticationAdapter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlamoNetworkRetrierAdapter.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 3/8/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | class AlamoAuthenticationAdapter: RequestInterceptor { 13 | func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result) -> Void) { 14 | var urlRequest = urlRequest 15 | //attach authentication if any token has been stored 16 | if let token = APITokenManager.currentToken, (urlRequest.value(forHTTPHeaderField: "Authorization") == nil) { 17 | urlRequest.setValue("Bearer " + token.authorization, forHTTPHeaderField: "Authorization") 18 | } 19 | //specify our custom user agent 20 | urlRequest.setValue(AlamoAuthenticationAdapter.agent, forHTTPHeaderField: "User-Agent") 21 | 22 | completion(Result.success(urlRequest)) 23 | } 24 | 25 | /// Custom makespace agent header 26 | private static var agent: String { 27 | let name = UIDevice.current.name 28 | let version = UIDevice.current.systemVersion 29 | let model = UIDevice.current.model 30 | let bundleExecutableName = Bundle.main.bundleExecutableName 31 | let agent = "UserAgent:(AppName:\(bundleExecutableName), DeviceName:\(name), Version:\(version), Model:\(model))" 32 | return agent 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/AlamoNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlamoNetworkManager.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | open class AlamoNetworkManager: NetworkManager { 13 | private static let validStatusCodes = (200...299) 14 | public var configuration: APIConfiguration 15 | 16 | required public init(with configuration: APIConfiguration, 17 | interceptor: RequestInterceptor?) { 18 | self.configuration = configuration 19 | 20 | let sessionConfiguration = URLSessionConfiguration.default 21 | sessionConfiguration.headers = HTTPHeaders.default 22 | sessionConfiguration.httpShouldSetCookies = false 23 | let protocolClasses = sessionConfiguration.protocolClasses ?? [AnyClass]() 24 | sessionConfiguration.protocolClasses = [NetworkURLProtocol.self] as [AnyClass] + protocolClasses 25 | //Setup our cache 26 | let capacity = 100 * 1024 * 1024 // 100 MBs 27 | let urlCache = URLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil) 28 | sessionConfiguration.urlCache = urlCache 29 | //This is the default value but let's make it clear caching by default depends on the response Cache-Control header 30 | sessionConfiguration.requestCachePolicy = URLRequest.CachePolicy.useProtocolCachePolicy 31 | 32 | self.session = Session(configuration: sessionConfiguration, 33 | startRequestsImmediately: false, 34 | interceptor: interceptor ?? AlamoAuthenticationAdapter()) 35 | 36 | } 37 | 38 | /// default session manager to be use with Alamofire 39 | private let session: Session! 40 | 41 | public func cancelAllRequests() { 42 | session.cancelAllRequests() 43 | } 44 | 45 | public func upload(path: String, 46 | withConstructingBlock constructingBlock: @escaping (MultipartFormData) -> Void, 47 | progressBlock: @escaping (Progress) -> Void, 48 | headers: CustomHTTPHeaders, 49 | stubs: [ServiceStub], 50 | success: @escaping SuccessResponseBlock, 51 | failure: @escaping ErrorResponseBlock) { 52 | session.upload(multipartFormData: constructingBlock, to: path).uploadProgress { progress in 53 | progressBlock(progress) 54 | }.responseJSON { response in 55 | if let json = response.value as? [String: Any] { 56 | success(json) 57 | } 58 | else { 59 | failure(MSError.dataError(description: (response.error?.localizedDescription ?? "Upload Failed"))) 60 | } 61 | }.resume() 62 | } 63 | 64 | // MARK: - Request handling 65 | 66 | /// Creates and executes a request using Alamofire 67 | /// 68 | /// - Parameters: 69 | /// - path: full path to the URL 70 | /// - method: HTTP method, default is GET 71 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 72 | /// - paginated: if the request should follow pagination, success only if all pages are completed 73 | /// - cachePolicy: specifices the policy to follow for caching responses 74 | /// - headers: custom headers that should be attached with this request 75 | /// - success: success block with a response 76 | /// - failure: failure block with an error 77 | public func request(path: String, 78 | method: NetworkingServiceKit.HTTPMethod = .get, 79 | with parameters: RequestParameters = RequestParameters(), 80 | paginated: Bool = false, 81 | cachePolicy:CacheResponsePolicy, 82 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 83 | stubs: [ServiceStub] = [], 84 | success: @escaping SuccessResponseBlock, 85 | failure: @escaping ErrorResponseBlock) { 86 | 87 | let httpMethod = Alamofire.HTTPMethod(rawValue: method.string) 88 | 89 | //lets use URL encoding only for GETs / DELETEs OR use a specific encoding for arrays 90 | var encoding: ParameterEncoding = method == .get || method == .delete ? URLEncoding.default : JSONEncoding.default 91 | encoding = parameters.validArrayRequest() ? ArrayEncoding() : encoding 92 | encoding = parameters.validStringRequest() ? StringEncoding() : encoding 93 | 94 | let request = session.request(path, 95 | method: httpMethod, 96 | parameters: parameters, 97 | encoding: encoding, 98 | headers: headers).validate(statusCode: AlamoNetworkManager.validStatusCodes).responseJSON { rawResponse in 99 | 100 | //Print response if we have a verbose log mode 101 | AlamoNetworkManager.logNetwork(rawResponse: rawResponse) 102 | //Return valid response 103 | if rawResponse.error == nil { 104 | // Cached our response through URLCache 105 | AlamoNetworkManager.cache(response: rawResponse.response, 106 | with: rawResponse.data, 107 | for: rawResponse.request, 108 | using: cachePolicy) 109 | // Process valid response 110 | self.process(rawResponse: rawResponse.value, 111 | method: httpMethod, 112 | parameters: parameters, 113 | encoding: encoding, 114 | paginated: paginated, 115 | cachePolicy: cachePolicy, 116 | headers: headers, 117 | success: success, 118 | failure: failure) 119 | } else if let error = self.buildError(fromResponse: rawResponse) { 120 | //Figure out if we have an error and an error response 121 | failure(error) 122 | } else { 123 | //if the request is succesful but has no response (validation for http code has passed also) 124 | success([String: Any]()) 125 | } 126 | } 127 | 128 | if cachePolicy.type == .forceCacheDataElseLoad, 129 | let urlRequest = try? request.convertible.asURLRequest(), 130 | let cachedResponse = urlRequest.cachedJSONResponse() { 131 | //Process valid response 132 | self.process(rawResponse: cachedResponse, 133 | method: httpMethod, 134 | parameters: parameters, 135 | encoding: encoding, 136 | paginated: paginated, 137 | cachePolicy: cachePolicy, 138 | headers: headers, 139 | success: success, 140 | failure: failure) 141 | AlamoNetworkManager.log("CACHED \(urlRequest.description)") 142 | } else { 143 | request.resume() 144 | if let desc = try? request.convertible.asURLRequest().description { 145 | AlamoNetworkManager.log(desc) 146 | } 147 | } 148 | } 149 | 150 | 151 | // MARK: - Pagination 152 | 153 | /// Request the next page, indicated in the response from the first request 154 | /// 155 | /// - Parameters: 156 | /// - urlString: full path to the url that has the next page 157 | /// - method: HTTP method to follow 158 | /// - parameters: URL or body parameters depending on the HTTP method 159 | /// - aggregateResponse: existing response from first call 160 | /// - encoding: encoding for the request 161 | /// - headers: headers for the request 162 | /// - cachePolicy: specifices the policy to follow for caching responses 163 | /// - success: success block with a response 164 | /// - failure: failure block with an error 165 | func getNextPage(forURL urlString: String, 166 | method: Alamofire.HTTPMethod, 167 | with parameters: RequestParameters, 168 | onExistingResponse aggregateResponse: [String: Any], 169 | encoding: ParameterEncoding = URLEncoding.default, 170 | headers: CustomHTTPHeaders? = nil, 171 | cachePolicy:CacheResponsePolicy, 172 | success: @escaping SuccessResponseBlock, 173 | failure: @escaping ErrorResponseBlock) { 174 | 175 | let request = session.request(urlString, 176 | method: method, 177 | parameters: parameters, 178 | encoding: encoding, 179 | headers: headers).validate(statusCode: AlamoNetworkManager.validStatusCodes).responseJSON { rawResponse in 180 | 181 | //Print response if we have a verbose log mode 182 | AlamoNetworkManager.logNetwork(rawResponse: rawResponse) 183 | 184 | //Return valid response 185 | if let response = rawResponse.value as? [String:Any], 186 | rawResponse.error == nil { 187 | // Cached our response through URLCache 188 | AlamoNetworkManager.cache(response: rawResponse.response, 189 | with: rawResponse.data, 190 | for: rawResponse.request, 191 | using: cachePolicy) 192 | 193 | //Process valid response 194 | self.processNextPage(response: response, 195 | forURL: urlString, 196 | method: method, 197 | parameters: parameters, 198 | onExistingResponse: aggregateResponse, 199 | encoding: encoding, 200 | headers: headers, 201 | cachePolicy: cachePolicy, 202 | success: success, 203 | failure: failure) 204 | } else if let error = self.buildError(fromResponse: rawResponse) { 205 | //Figure out if we have an error and an error response 206 | failure(error) 207 | } else { 208 | //if the request is succesful but has no response (validation for http code has passed also) 209 | success([String: Any]()) 210 | } 211 | } 212 | 213 | if cachePolicy.type == .forceCacheDataElseLoad, 214 | let urlRequest = try? request.convertible.asURLRequest(), 215 | let cachedResponse = urlRequest.cachedJSONResponse() as? [String:Any] { 216 | 217 | //Process valid response 218 | self.processNextPage(response: cachedResponse, 219 | forURL: urlString, 220 | method: method, 221 | parameters: parameters, 222 | onExistingResponse: aggregateResponse, 223 | encoding: encoding, 224 | headers: headers, 225 | cachePolicy: cachePolicy, 226 | success: success, 227 | failure: failure) 228 | AlamoNetworkManager.log("CACHED \(urlRequest.description)") 229 | } else { 230 | request.resume() 231 | if let desc = try? request.convertible.asURLRequest().description { 232 | AlamoNetworkManager.log(desc) 233 | } 234 | } 235 | } 236 | 237 | // MARK: - Response Processing 238 | 239 | 240 | /// Process a network response 241 | /// 242 | /// - Parameters: 243 | /// - rawResponse: parsed response 244 | /// - method: HTTP method, default is GET 245 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 246 | /// - encoding: the defined encoding for the request 247 | /// - paginated: if the request should follow pagination, success only if all pages are completed 248 | /// - cachePolicy: specifices the policy to follow for caching responses 249 | /// - headers: custom headers that should be attached with this request 250 | /// - success: success block with a response 251 | /// - failure: failure block with an error 252 | private func process(rawResponse:Any?, 253 | method: Alamofire.HTTPMethod, 254 | parameters: RequestParameters, 255 | encoding: ParameterEncoding, 256 | paginated: Bool, 257 | cachePolicy:CacheResponsePolicy, 258 | headers: CustomHTTPHeaders, 259 | success: @escaping SuccessResponseBlock, 260 | failure: @escaping ErrorResponseBlock) { 261 | 262 | if let response = rawResponse as? [Any] { 263 | let responseDic = ["results": response] 264 | success(responseDic) 265 | } 266 | else { 267 | let response = rawResponse as? [String:Any] ?? [:] 268 | 269 | if let nextPage = response["next"] as? String, 270 | paginated { 271 | //call a request with the next page 272 | self.getNextPage(forURL: nextPage, 273 | method: method, 274 | with: [:], 275 | onExistingResponse: response, 276 | encoding: encoding, 277 | headers: headers, 278 | cachePolicy: cachePolicy, 279 | success: success, 280 | failure: failure) 281 | } 282 | else { 283 | success(response) 284 | } 285 | } 286 | } 287 | 288 | /// Process a paginated network response 289 | /// 290 | /// - Parameters: 291 | /// - response: parsed response list of objects 292 | /// - urlString: next page URL 293 | /// - method: HTTP method, default is GET 294 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 295 | /// - aggregateResponse: our agregated response from previous responses 296 | /// - encoding: the defined encoding for the request 297 | /// - paginated: if the request should follow pagination, success only if all pages are completed 298 | /// - cachePolicy: specifices the policy to follow for caching responses 299 | /// - headers: custom headers that should be attached with this request 300 | /// - success: success block with a response 301 | /// - failure: failure block with an error 302 | private func processNextPage(response:[String:Any], 303 | forURL urlString: String, 304 | method: Alamofire.HTTPMethod, 305 | parameters: RequestParameters, 306 | onExistingResponse aggregateResponse: [String: Any], 307 | encoding: ParameterEncoding, 308 | headers: CustomHTTPHeaders?, 309 | cachePolicy:CacheResponsePolicy, 310 | success: @escaping SuccessResponseBlock, 311 | failure: @escaping ErrorResponseBlock) { 312 | var currentResponse = aggregateResponse 313 | 314 | //attach new response into existing response 315 | if var currentResults = currentResponse["results"] as? [[String: Any]], 316 | let newResults = response["results"] as? [[String : Any]] { 317 | currentResults.append(contentsOf: newResults) 318 | currentResponse["results"] = currentResults 319 | } 320 | 321 | //iterate on the next page if any 322 | if let nextPage = response["next"] as? String, 323 | !nextPage.isEmpty { 324 | self.getNextPage(forURL: nextPage, 325 | method: method, 326 | with: parameters, 327 | onExistingResponse: currentResponse, 328 | encoding: encoding, 329 | headers: headers, 330 | cachePolicy: cachePolicy, 331 | success: success, 332 | failure: failure) 333 | } else { 334 | success(currentResponse) 335 | } 336 | 337 | } 338 | 339 | // MARK: - Error Handling 340 | 341 | /// Returns an MSError if the rawResponse is a valid error and has an error response 342 | /// 343 | /// - Parameter rawResponse: response from the request 344 | /// - Returns: a valid error reason and details if they were returned in the correct format 345 | func buildError(fromResponse rawResponse: DataResponse) -> MSError? 346 | { 347 | if let afError = rawResponse.error { 348 | if let statusCode = rawResponse.response?.statusCode, 349 | !AlamoNetworkManager.validStatusCodes.contains(statusCode) { 350 | 351 | let details = MSErrorDetails(data: rawResponse.data, request: rawResponse.request, code: statusCode, error: afError) 352 | let reason = MSErrorType.ResponseFailureReason(code: statusCode) 353 | 354 | return MSError(type: .responseValidation(reason: reason), details: details) 355 | } 356 | else { 357 | let error = (afError.underlyingError ?? afError) as NSError 358 | if error.code.isNetworkErrorCode { 359 | 360 | let components = rawResponse.request?.components 361 | let details = MSErrorDetails(message: error.localizedDescription, body: components?.body, path: components?.path, code: error.code, underlyingError: error) 362 | return MSError(type: .responseValidation(reason: .connectivity), details: details) 363 | } 364 | } 365 | } 366 | 367 | return nil 368 | } 369 | 370 | // MARK: - Debugging 371 | 372 | /// Prints a debug of a network response 373 | /// 374 | /// - Parameter rawResponse: response to get printed 375 | private static func logNetwork(rawResponse:Any) { 376 | if ServiceLocator.logLevel == .verbose { 377 | print("🔵 ServiceLocator: ") 378 | debugPrint(rawResponse) 379 | } 380 | } 381 | 382 | /// Print a service locator log if we have logging enabled 383 | /// 384 | /// - Parameter text: log to print 385 | private static func log(_ text:String) { 386 | //Print request if we have log mode enabled 387 | if ServiceLocator.logLevel != .none { 388 | print("🔵 ServiceLocator: " + text) 389 | } 390 | } 391 | 392 | // MARK: - Caching 393 | 394 | /// Handles caching a response based on a cache policy 395 | /// 396 | /// - Parameters: 397 | /// - response: the network response 398 | /// - data: response's data 399 | /// - request: original request 400 | /// - cachePolicy: cache policy 401 | private static func cache(response:HTTPURLResponse?, with data:Data?, for request:URLRequest?, using cachePolicy: CacheResponsePolicy) { 402 | // Cached our response through URLCache if we are force caching and the response doesn't specify a Cache-Control 403 | if cachePolicy.type.supportsForceCache, 404 | let response = response, 405 | response.isResponseCachePolicyDisabled, 406 | let request = request, 407 | let data = data, 408 | cachePolicy.maxAge > 0 { 409 | let cachedURLResponse = CachedURLResponse(response: response, 410 | data: data, 411 | maxAge: cachePolicy.maxAge) 412 | URLCache.shared.storeCachedResponse(cachedURLResponse, for: request) 413 | } 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/NetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManagerProtocol.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | //Custom Makespace Error Response Object 13 | @objc public class MSErrorDetails: NSObject { 14 | /// Message describing the error 15 | public let message: String 16 | /// Body of network request 17 | public let body: String? 18 | /// Path of network request 19 | public let path: String? 20 | /// Error code 21 | public let code: Int 22 | /// Original error object 23 | public let underlyingError: Error? 24 | 25 | public init(message: String, body: String?, path: String?, code: Int, underlyingError: Error?) { 26 | self.message = message 27 | self.body = body 28 | self.path = path 29 | self.code = code 30 | self.underlyingError = underlyingError 31 | } 32 | 33 | public convenience init(error: NSError) { 34 | self.init(message: error.localizedDescription, body: nil, path: nil, code: error.code, underlyingError: error) 35 | } 36 | 37 | public convenience init(data: Data?, request: URLRequest?, code: Int, error: Error?) { 38 | let components = request?.components 39 | let body = components?.body 40 | let path = components?.path 41 | 42 | var errorMessage: String? 43 | 44 | if let responseData = data, 45 | let responseDataString = String(data: responseData, encoding:String.Encoding.utf8), 46 | let responseDictionary = responseDataString.jsonDictionary { 47 | 48 | if let responseError = responseDictionary["error"] as? [String: Any], 49 | let message = responseError[NSError.messageKey] as? String { 50 | errorMessage = message 51 | } else if let responseError = responseDictionary["errors"] as? [[String: Any]], 52 | let responseFirstError = responseError.first, 53 | let message = responseFirstError[NSError.messageKey] as? String { 54 | //multiple error 55 | errorMessage = message 56 | } 57 | } 58 | 59 | self.init(message: errorMessage ?? (error?.localizedDescription ?? ""), body: body, path: path, code: code, underlyingError: error) 60 | } 61 | } 62 | 63 | public enum MSErrorType { 64 | /// Enum describing the network failure 65 | /// 66 | /// - tokenExpired: request received a 401 from a backend 67 | /// - badRequest: generic error for responses 68 | /// - internalServerError: a 500 request 69 | /// - badStatusCode: a request that could not validate a status code 70 | /// - notConnected: user not connected to the internet 71 | /// - forbidden: received a 403 72 | /// - notFound: 404, path not found 73 | /// - invalidResponse: server returns a response, but not in the expected format 74 | public enum ResponseFailureReason { 75 | case tokenExpired 76 | case badRequest 77 | case internalServerError 78 | case badStatusCode 79 | case connectivity 80 | case forbidden 81 | case notFound 82 | case invalidResponse 83 | 84 | public var description: String { 85 | switch self { 86 | case .tokenExpired: 87 | return "Expired Auth Token" 88 | case .badRequest: 89 | return "Invalid Network Request" 90 | case .forbidden: 91 | return "Invalid Credentials" 92 | case .notFound: 93 | return "Invalid Request Path" 94 | case .internalServerError: 95 | return "Internal Server Error" 96 | case .badStatusCode: 97 | return "Invalid Status Code" 98 | case .connectivity: 99 | return "Network Connectivity Issues" 100 | case .invalidResponse: 101 | return "Unable to Parse Server Response" 102 | } 103 | } 104 | } 105 | 106 | case responseValidation(reason: ResponseFailureReason) 107 | case persistenceFailure 108 | case invalidData(description: String) 109 | 110 | public var description: String { 111 | switch self { 112 | case .responseValidation(let reason): return reason.description 113 | case .persistenceFailure: return "Core Data Failure" 114 | case .invalidData(let description): return description 115 | } 116 | } 117 | } 118 | 119 | //Custom Error Wrapper 120 | @objc open class MSError: NSObject, Error { 121 | /// Enum value describing the failure 122 | public let type: MSErrorType 123 | /// Details of the error 124 | public let details: MSErrorDetails 125 | 126 | /// Designated initializer 127 | /// 128 | /// - Parameters: 129 | /// - type: type of failure 130 | /// - details: error details 131 | public init(type: MSErrorType, details: MSErrorDetails) { 132 | self.type = type 133 | self.details = details 134 | } 135 | 136 | /// Convenience initializer 137 | /// 138 | /// - Parameters: 139 | /// - type: type of failure 140 | /// - error: NSError object associated with failure 141 | public init(type: MSErrorType, error: NSError?) { 142 | self.type = type 143 | 144 | if let error = error { 145 | self.details = MSErrorDetails(error: error) 146 | } 147 | else { 148 | self.details = MSErrorDetails(message: type.description, body: nil, path: nil, code: 0, underlyingError: nil) 149 | } 150 | } 151 | } 152 | 153 | extension MSError: CustomNSError { 154 | public var errorCode: Int { 155 | return details.code 156 | } 157 | 158 | public var errorUserInfo: [String : Any] { 159 | return [NSLocalizedDescriptionKey: details.message] 160 | } 161 | } 162 | 163 | // Mapped Error response failures 164 | public extension MSErrorType.ResponseFailureReason { 165 | 166 | /// Conveience initializer 167 | /// 168 | /// - Parameter code: response code of the error 169 | init(code: Int) { 170 | switch code { 171 | case 401: 172 | self = .tokenExpired 173 | case 403: 174 | self = .forbidden 175 | case 404: 176 | self = .notFound 177 | case 500: 178 | self = .internalServerError 179 | 180 | default: 181 | self = code.isNetworkErrorCode ? .connectivity : .badRequest 182 | } 183 | } 184 | } 185 | 186 | public extension MSError { 187 | /// Shortcut for identifying token expiration errors 188 | @objc var hasTokenExpired: Bool { 189 | switch type { 190 | case .responseValidation(let reason): 191 | return reason == .tokenExpired 192 | default: 193 | return false 194 | } 195 | } 196 | 197 | /// Shortcut for identifying connectivity errors 198 | @objc var isNetworkError: Bool { 199 | switch type { 200 | case .responseValidation(let reason): 201 | return reason == .connectivity 202 | default: 203 | return false 204 | } 205 | } 206 | 207 | /// Returns a generic error to describe Core Data problems 208 | @objc static var genericPersistenceError: MSError { 209 | return MSError(type: .persistenceFailure, error: nil) 210 | } 211 | 212 | static func dataError(description: String) -> MSError { 213 | return MSError(type: .invalidData(description: description), error: nil) 214 | } 215 | } 216 | 217 | internal extension Int { 218 | var isNetworkErrorCode: Bool { 219 | switch self { 220 | case NSURLErrorTimedOut, NSURLErrorNotConnectedToInternet, NSURLErrorNetworkConnectionLost: 221 | return true 222 | default: 223 | return false 224 | } 225 | } 226 | } 227 | 228 | /// Custom HTTP enum types for our network protocol 229 | @objc 230 | public enum HTTPMethod: Int32 { 231 | case options 232 | case get 233 | case head 234 | case post 235 | case put 236 | case patch 237 | case delete 238 | case trace 239 | case connect 240 | 241 | var string: String { 242 | switch self { 243 | case .options: return "OPTIONS" 244 | case .get: return "GET" 245 | case .head: return "HEAD" 246 | case .post: return "POST" 247 | case .put: return "PUT" 248 | case .patch: return "PATCH" 249 | case .delete: return "DELETE" 250 | case .trace: return "TRACE" 251 | case .connect: return "CONNECT" 252 | } 253 | } 254 | } 255 | 256 | /// Custom type of cache policy 257 | @objc 258 | public enum CacheResponsePolicyType : UInt { 259 | 260 | /// Cache based on HTTP-Header: Cache-Control 261 | case cacheControlBased 262 | 263 | /// Force Cache this response, return from the cache immediatly otherwise load network 264 | case forceCacheDataElseLoad 265 | 266 | /// Revalidates the cache, returns from network 267 | case reloadRevalidatingForceCacheData 268 | 269 | /// If the cache policy supports force caching 270 | var supportsForceCache:Bool { 271 | switch self { 272 | case .forceCacheDataElseLoad, .reloadRevalidatingForceCacheData: return true 273 | default: return false 274 | } 275 | } 276 | } 277 | 278 | /// Describe how the cache should behave when receiving a network response 279 | @objc 280 | open class CacheResponsePolicy: NSObject { 281 | // Cache policy type 282 | let type:CacheResponsePolicyType 283 | // Max age we would hold a cache, only used for forceCache, otherwise this is specified in the server response 284 | let maxAge:Double 285 | 286 | public init(type:CacheResponsePolicyType, maxAge:Double) { 287 | self.type = type 288 | self.maxAge = maxAge 289 | } 290 | 291 | /// Returns the default cache policy 292 | public static var `default`:CacheResponsePolicy { 293 | return CacheResponsePolicy(type:.cacheControlBased, maxAge:0) 294 | } 295 | } 296 | 297 | /// Success/Error blocks for a NetworkManager response 298 | public typealias SuccessResponseBlock = ([String:Any]) -> Void 299 | public typealias ErrorResponseBlock = (MSError) -> Void 300 | //Custom parameter typealias 301 | public typealias CustomHTTPHeaders = HTTPHeaders 302 | public typealias RequestParameters = [String: Any] 303 | 304 | /// Protocol for defining a Network Manager 305 | public protocol NetworkManager { 306 | var configuration: APIConfiguration {get set} 307 | func request(path: String, 308 | method: NetworkingServiceKit.HTTPMethod, 309 | with parameters: RequestParameters, 310 | paginated: Bool, 311 | cachePolicy:CacheResponsePolicy, 312 | headers: CustomHTTPHeaders, 313 | stubs: [ServiceStub], 314 | success: @escaping SuccessResponseBlock, 315 | failure: @escaping ErrorResponseBlock) 316 | 317 | func upload(path: String, 318 | withConstructingBlock constructingBlock: @escaping (MultipartFormData) -> Void, 319 | progressBlock: @escaping (Progress) -> Void, 320 | headers: CustomHTTPHeaders, 321 | stubs: [ServiceStub], 322 | success: @escaping SuccessResponseBlock, 323 | failure: @escaping ErrorResponseBlock) 324 | 325 | func cancelAllRequests() 326 | 327 | init(with configuration: APIConfiguration, 328 | interceptor: RequestInterceptor?) 329 | } 330 | 331 | extension NetworkManager { 332 | public func cancelAllRequests() { } // optional method 333 | } 334 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Networking/NetworkURLProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkURLProtocol.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot on 9/20/17. 6 | // 7 | 8 | import UIKit 9 | 10 | /// Custom URLProtocol for intercepting requests 11 | class NetworkURLProtocol: URLProtocol { 12 | 13 | private var session: URLSession? 14 | private var sessionTask: URLSessionDataTask? 15 | 16 | override init(request: URLRequest, cachedResponse: CachedURLResponse?, client: URLProtocolClient?) { 17 | super.init(request: request, cachedResponse: cachedResponse, client: client) 18 | 19 | if session == nil { 20 | session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) 21 | } 22 | } 23 | 24 | /// Check if we should intercept a request 25 | /// 26 | /// - Parameter request: request to be intercepted based on our delegate and baseURL 27 | /// - Returns: if we should intercept 28 | override class func canInit(with request: URLRequest) -> Bool { 29 | let baseURL = APIConfiguration.current.baseURL 30 | if ServiceLocator.shouldInterceptRequests, 31 | let delegate = ServiceLocator.shared.delegate, 32 | let urlString = request.url?.absoluteString, 33 | urlString.contains(baseURL) { 34 | return delegate.shouldInterceptRequest(with: request) 35 | } 36 | return false 37 | } 38 | 39 | override class func canonicalRequest(for request: URLRequest) -> URLRequest { 40 | return request 41 | } 42 | 43 | override var cachedResponse: CachedURLResponse? { 44 | return nil 45 | } 46 | 47 | /// Load the request, if we find this request is an intercepted one, execute the new request 48 | override func startLoading() { 49 | if let delegate = ServiceLocator.shared.delegate, 50 | let newRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest, 51 | let modifiedRequest = delegate.processIntercept(for: newRequest) { 52 | sessionTask = session?.dataTask(with: modifiedRequest as URLRequest) 53 | sessionTask?.resume() 54 | 55 | if ServiceLocator.logLevel != .none { 56 | print("☢️ ServiceLocator: Intercepted request, NEW: \(modifiedRequest.url?.absoluteString ?? "")") 57 | } 58 | } 59 | } 60 | 61 | override func stopLoading() { 62 | sessionTask?.cancel() 63 | } 64 | } 65 | 66 | // MARK: - URLSessionDataDelegate 67 | extension NetworkURLProtocol: URLSessionDataDelegate { 68 | 69 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { 70 | client?.urlProtocol(self, didLoad: data) 71 | } 72 | 73 | func urlSession(_ session: URLSession, 74 | dataTask: URLSessionDataTask, 75 | didReceive response: URLResponse, 76 | completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { 77 | let policy = URLCache.StoragePolicy(rawValue: request.cachePolicy.rawValue) ?? .notAllowed 78 | client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: policy) 79 | completionHandler(.allow) 80 | } 81 | 82 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { 83 | if let error = error { 84 | client?.urlProtocol(self, didFailWithError: error) 85 | } else { 86 | client?.urlProtocolDidFinishLoading(self) 87 | } 88 | } 89 | 90 | func urlSession(_ session: URLSession, 91 | task: URLSessionTask, 92 | willPerformHTTPRedirection response: HTTPURLResponse, 93 | newRequest request: URLRequest, 94 | completionHandler: @escaping (URLRequest?) -> Void) { 95 | client?.urlProtocol(self, wasRedirectedTo: request, redirectResponse: response) 96 | completionHandler(request) 97 | } 98 | 99 | func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?) { 100 | guard let error = error else { return } 101 | client?.urlProtocol(self, didFailWithError: error) 102 | } 103 | 104 | func urlSession(_ session: URLSession, 105 | didReceive challenge: URLAuthenticationChallenge, 106 | completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { 107 | let protectionSpace = challenge.protectionSpace 108 | let sender = challenge.sender 109 | 110 | if protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { 111 | if let serverTrust = protectionSpace.serverTrust { 112 | let credential = URLCredential(trust: serverTrust) 113 | sender?.use(credential, for: challenge) 114 | completionHandler(.useCredential, credential) 115 | return 116 | } 117 | } 118 | } 119 | 120 | func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession) { 121 | client?.urlProtocolDidFinishLoading(self) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Service.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | /// Defines the necessary methods that a service should implement 13 | public protocol Service { 14 | 15 | /// Builds a service with an auth token and a networkManager implementation 16 | /// 17 | /// - Parameters: 18 | /// - token: an auth token (if we are authenticated) 19 | /// - networkManager: the networkManager we are currently using 20 | init(token: APIToken?, networkManager: NetworkManager) 21 | 22 | /// Current auth token this service has 23 | var token: APIToken? { get set } 24 | 25 | /// Current network manger this service is using 26 | var networkManager: NetworkManager { get set } 27 | 28 | /// Current API configuration 29 | var currentConfiguration: APIConfiguration { get } 30 | 31 | /// Default service version 32 | var serviceVersion: String { get } 33 | 34 | /// Default service path 35 | var servicePath: String { get } 36 | 37 | /// Builds a service query path with our version and default root path 38 | /// 39 | /// - Parameters: 40 | /// - query: the query to build 41 | /// - overrideURL: manual override of default base URL 42 | /// - overrideVersion: manual override of the service version 43 | /// - Returns: a compose query with the baseURL, service version and service path included 44 | func servicePath(for query: String, baseUrlOverride overrideURL: String?, serviceVersionOverride overrideVersion: String?) -> String 45 | 46 | /// True if our auth token is valid 47 | var isAuthenticated: Bool { get } 48 | 49 | /// Executes a request with out current network Manager 50 | /// 51 | /// - Parameters: 52 | /// - path: a full URL 53 | /// - baseUrlOverride: manual override of default base URL 54 | /// - serviceVersionOverride: manual override of the service version 55 | /// - method: HTTP method 56 | /// - parameters: parameters for the request 57 | /// - paginated: if we have to merge this request pagination 58 | /// - cachePolicy: specifices the policy to follow for caching responses 59 | /// - headers: optional headers to include in the request 60 | /// - success: success block 61 | /// - failure: failure block 62 | func request(path: String, 63 | baseUrlOverride: String?, 64 | serviceVersionOverride: String?, 65 | method: NetworkingServiceKit.HTTPMethod, 66 | with parameters: RequestParameters, 67 | paginated: Bool, 68 | cachePolicy: CacheResponsePolicy, 69 | headers: CustomHTTPHeaders, 70 | success: @escaping SuccessResponseBlock, 71 | failure: @escaping ErrorResponseBlock) 72 | 73 | 74 | /// Returns a list of Service Stubs (api paths with a stub type) 75 | var stubs:[ServiceStub] { get set } 76 | } 77 | 78 | /// Abstract Base Service, sets up a default implementations of the Service protocol. Defaults the service path and version into empty strings. 79 | open class AbstractBaseService: NSObject, Service { 80 | open var networkManager: NetworkManager 81 | 82 | open var token: APIToken? 83 | 84 | open var stubs:[ServiceStub] = [ServiceStub]() 85 | 86 | open var currentConfiguration: APIConfiguration { 87 | return self.networkManager.configuration 88 | } 89 | 90 | /// Init method for an Service, each service must have the current token auth and access to the networkManager to execute requests 91 | /// 92 | /// - Parameters: 93 | /// - token: an existing APIToken 94 | /// - networkManager: an object that supports our NetworkManager protocol 95 | public required init(token: APIToken?, networkManager: NetworkManager) { 96 | self.token = token 97 | self.networkManager = networkManager 98 | } 99 | 100 | /// Currently supported version of the service 101 | open var serviceVersion: String { 102 | return "" 103 | } 104 | 105 | /// Name here your service path 106 | open var servicePath: String { 107 | return "" 108 | } 109 | 110 | /// Returns the baseURL for this service, default is the current configuration URL 111 | open var baseURL: String { 112 | return currentConfiguration.baseURL 113 | } 114 | 115 | /// Returns a local path for an API request, this includes the service version and name. i.e v4/accounts/user_profile 116 | /// 117 | /// - Parameters: 118 | /// - query: api local path 119 | /// - overrideURL: manual override of default base URL 120 | /// - overrideVersion: manual override of the service version 121 | /// - Returns: local path to the api for the given query 122 | open func servicePath(for query: String, baseUrlOverride overrideURL: String?, serviceVersionOverride overrideVersion: String?) -> String { 123 | var fullPath = overrideURL ?? self.baseURL 124 | if (!self.servicePath.isEmpty) { 125 | fullPath += "/" + self.servicePath 126 | } 127 | if let version = overrideVersion { 128 | fullPath += "/" + version 129 | } 130 | else if (!self.serviceVersion.isEmpty) { 131 | fullPath += "/" + self.serviceVersion 132 | } 133 | fullPath += "/" + query 134 | return fullPath 135 | } 136 | 137 | /// Returns if this service has a valid token for authentication with our systems 138 | open var isAuthenticated: Bool { 139 | return (APITokenManager.currentToken != nil) 140 | } 141 | 142 | /// Creates and executes a request using our default Network Manager 143 | /// 144 | /// - Parameters: 145 | /// - path: full path to the URL 146 | /// - baseUrlOverride: manual override of default base URL 147 | /// - method: HTTP method, default is GET 148 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 149 | /// - paginated: if the request should follow pagination, success only if all pages are completed 150 | /// - cachePolicy: specifices the policy to follow for caching responses 151 | /// - headers: custom headers that should be attached with this request 152 | /// - success: success block with a response 153 | /// - failure: failure block with an error 154 | open func request(path: String, 155 | baseUrlOverride: String? = nil, 156 | serviceVersionOverride: String? = nil, 157 | method: NetworkingServiceKit.HTTPMethod = .get, 158 | with parameters: RequestParameters = RequestParameters(), 159 | paginated: Bool = false, 160 | cachePolicy: CacheResponsePolicy = CacheResponsePolicy.default, 161 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 162 | success: @escaping SuccessResponseBlock, 163 | failure: @escaping ErrorResponseBlock) { 164 | networkManager.request(path: servicePath(for: path, 165 | baseUrlOverride: baseUrlOverride, 166 | serviceVersionOverride: serviceVersionOverride), 167 | method: method, 168 | with: parameters, 169 | paginated: paginated, 170 | cachePolicy: cachePolicy, 171 | headers: headers, 172 | stubs: self.stubs, 173 | success: success, 174 | failure: { error in 175 | if error.hasTokenExpired && self.isAuthenticated { 176 | //If our error response was because our token expired, then lets tell the delegate 177 | ServiceLocator.shared.delegate?.authenticationTokenDidExpire(forService: self) 178 | } 179 | failure(error) 180 | }) 181 | } 182 | 183 | open func upload(path: String, 184 | baseUrlOverride: String? = nil, 185 | serviceVersionOverride: String? = nil, 186 | withConstructingBlock constructingBlock: @escaping (MultipartFormData) -> Void, 187 | progressBlock: ((Progress) -> Void)? = nil, 188 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 189 | success: @escaping SuccessResponseBlock, 190 | failure: @escaping ErrorResponseBlock) { 191 | networkManager.upload(path: servicePath(for: path, baseUrlOverride: baseUrlOverride, serviceVersionOverride: serviceVersionOverride), 192 | withConstructingBlock: constructingBlock, 193 | progressBlock: { progressBlock?($0) }, 194 | headers: headers, 195 | stubs: self.stubs, 196 | success: success, 197 | failure: { error in 198 | if error.hasTokenExpired && self.isAuthenticated { 199 | //If our error response was because our token expired, then lets tell the delegate 200 | ServiceLocator.shared.delegate?.authenticationTokenDidExpire(forService: self) 201 | } 202 | failure(error) 203 | }) 204 | } 205 | 206 | } 207 | 208 | 209 | public extension AbstractBaseService { 210 | static var resolved : Self { 211 | guard let service = ServiceLocator.service(forType: self) else { 212 | fatalError("Service of type \(Self.self) not found. Make sure you register it in the ServiceLocator first") 213 | } 214 | return service 215 | } 216 | 217 | static func stubbed(_ stubs: [ServiceStub]) -> Self { 218 | guard let service = ServiceLocator.service(forType: self, stubs: stubs) else { 219 | fatalError("Service of type \(Self.self) not found. Make sure you register it in the ServiceLocator first") 220 | } 221 | return service 222 | } 223 | 224 | } 225 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/ServiceLocator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // APIServiceLocator.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/23/17. 6 | // 7 | // 8 | 9 | import UIKit 10 | import Alamofire 11 | public protocol ServiceLocatorDelegate { 12 | 13 | /// Handle tokens that just expire 14 | func authenticationTokenDidExpire(forService service: Service) 15 | 16 | /// Assess if this request should be intercepted for any kinda of reason through. Makespace app filter intercepts based on predefined regexes 17 | /// NOTE: This intercepts all URLRequests been made on the app that match a baseURL of the APIConfiguration.current.baseURL 18 | /// 19 | /// - Parameter request: request to be intercepted¿ 20 | /// - Returns: if we want or not to intercept 21 | func shouldInterceptRequest(with request: URLRequest) -> Bool 22 | 23 | /// Intercept a request that has been previously marked as we should intercept 24 | /// 25 | /// - Parameter request: request to be process as intercepted 26 | /// - Returns: updated request 27 | func processIntercept(for request: NSMutableURLRequest) -> URLRequest? 28 | } 29 | 30 | public extension ServiceLocatorDelegate { 31 | 32 | func shouldInterceptRequest(with request: URLRequest) -> Bool { 33 | return false 34 | } 35 | 36 | func processIntercept(for request: NSMutableURLRequest) -> URLRequest? { 37 | return nil 38 | } 39 | } 40 | 41 | /// Defines the level of loggin we want for our network manager 42 | @objc 43 | public enum SLLogLevel: Int { 44 | case none //no printing 45 | case short //print only requests 46 | case verbose //print request and response bodies 47 | } 48 | 49 | open class ServiceLocator: NSObject { 50 | /// Our Default Networking Client for Alamofire, replace to override our network request implementation 51 | public static var defaultNetworkClientType: NetworkManager.Type = AlamoNetworkManager.self 52 | 53 | /// Our Default behavior for printing network logs 54 | public static var logLevel:SLLogLevel = .short 55 | 56 | /// If any requests going out of the app should be intercepted 57 | public static var shouldInterceptRequests:Bool = false { 58 | didSet { 59 | if shouldInterceptRequests { 60 | /// Register our custom URL protocol for intercepting requests 61 | URLProtocol.registerClass(NetworkURLProtocol.self) 62 | } else { 63 | /// Register our custom URL protocol for intercepting requests 64 | URLProtocol.unregisterClass(NetworkURLProtocol.self) 65 | } 66 | } 67 | } 68 | 69 | /// Defines a private singleton, all interactions should be done through static methods 70 | internal static var shared: ServiceLocator = ServiceLocator() 71 | internal var delegate: ServiceLocatorDelegate? 72 | 73 | internal var currentServices: [String:Service] 74 | private var loadedServiceTypes: [Service.Type] 75 | private var configuration: APIConfiguration! 76 | private var networkManager: NetworkManager! 77 | private var token: APIToken? 78 | 79 | /// Inits default configuration and network manager 80 | private init(delegate: ServiceLocatorDelegate? = nil) { 81 | self.delegate = delegate 82 | self.currentServices = [String: Service]() 83 | self.loadedServiceTypes = [Service.Type]() 84 | } 85 | 86 | /// Resets the ServiceLocator singleton instance 87 | open class func reset() { 88 | ServiceLocator.shared = ServiceLocator(delegate: ServiceLocator.shared.delegate) 89 | ServiceLocator.shouldInterceptRequests = true 90 | } 91 | 92 | /// Reloads token, networkManager and configuration with existing hooked services 93 | open class func reloadExistingServices(withInterceptor interceptor: RequestInterceptor?) { 94 | let serviceTypes = ServiceLocator.shared.loadedServiceTypes 95 | reset() 96 | 97 | if let configType = APIConfiguration.apiConfigurationType, 98 | let authType = APIConfiguration.authConfigurationType, 99 | let tokenType = APITokenManager.tokenType { 100 | ServiceLocator.set(services: serviceTypes, 101 | api: configType, 102 | auth: authType, 103 | token: tokenType, 104 | interceptor: interceptor) 105 | } 106 | } 107 | 108 | /// Load a custom list of services 109 | /// 110 | /// - Parameter serviceTypes: list of services types that are going to get hooked 111 | 112 | /// Sets the current supported services 113 | /// 114 | /// - Parameters: 115 | /// - serviceTypes: an array of servide types that will be supported when asking for a service 116 | /// - apiConfigurationType: the type of APIConfigurationType this services will access 117 | /// - authConfigurationType: the type of APIConfigurationAuth this services will include in their requests 118 | /// - tokenType: the type of APIToken that will guarantee auth for our service requests 119 | /// - interceptor: the optional request interceptor, should we wish to override the default AlamoRequestAdapter 120 | open class func set(services serviceTypes: [Service.Type], 121 | api apiConfigurationType: APIConfigurationType.Type, 122 | auth authConfigurationType: APIConfigurationAuth.Type, 123 | token tokenType: APIToken.Type, 124 | bundleId: String = Bundle.main.appBundleIdentifier, 125 | interceptor: RequestInterceptor?) { 126 | //Set of services that we support 127 | ServiceLocator.shared.loadedServiceTypes = serviceTypes 128 | 129 | //Hold references to the defined API Configuration and Auth 130 | APIConfiguration.apiConfigurationType = apiConfigurationType 131 | APIConfiguration.authConfigurationType = authConfigurationType 132 | APITokenManager.tokenType = tokenType 133 | 134 | //Build Auth tokenType 135 | ServiceLocator.shared.token = APITokenManager.currentToken 136 | 137 | //Init our Default Network Client 138 | let configuration = APIConfiguration.current(bundleId: bundleId) 139 | ServiceLocator.shared.configuration = configuration 140 | ServiceLocator.shared.networkManager = ServiceLocator.defaultNetworkClientType.init(with: configuration, 141 | interceptor: interceptor) 142 | 143 | //Allocate services 144 | ServiceLocator.shared.currentServices = ServiceLocator.shared.loadedServiceTypes.reduce( 145 | [String: Service]()) { (dict, entry) in 146 | var dict = dict 147 | dict[String(describing: entry.self)] = entry.init(token: ServiceLocator.shared.token, 148 | networkManager: ServiceLocator.shared.networkManager) 149 | return dict 150 | } 151 | } 152 | 153 | /// Returns a service for a specific type 154 | /// 155 | /// - Parameter type: type of service 156 | /// - Returns: service object 157 | open class func service(forType type: T.Type) -> T? { 158 | if let service = ServiceLocator.shared.currentServices[String(describing: type.self)] as? T { 159 | return service 160 | } 161 | return nil 162 | } 163 | 164 | /// Returns a service given a string class name (Useful to be call from Obj-c) 165 | /// 166 | /// - Parameter className: string name for the class we are looking 167 | /// - Returns: a working service 168 | @objc 169 | open class func service(forClassName className: String) -> NSObject? { 170 | //check if className contains framework name 171 | let components = className.components(separatedBy: ".") 172 | var realClassName = className 173 | if components.count == 2 { 174 | realClassName = components[1] 175 | } 176 | if let service = ServiceLocator.shared.currentServices[realClassName] as? NSObject { 177 | return service 178 | } 179 | return nil 180 | } 181 | 182 | /// Sets a global delegate for the service locator 183 | open class func setDelegate(delegate: ServiceLocatorDelegate) { 184 | self.shared.delegate = delegate 185 | } 186 | 187 | // Returns current NetworkManager 188 | open class var currentNetworkManager: NetworkManager { 189 | return ServiceLocator.shared.networkManager 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Testing/ServiceLocator+Stub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceLocator+Stub.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 6/2/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension ServiceLocator 12 | { 13 | /// Returns a service for a specific type 14 | /// 15 | /// - Parameter type: type of service 16 | /// - Returns: service object 17 | open class func service(forType type: T.Type, stubs:[ServiceStub]) -> T? { 18 | if var service = ServiceLocator.shared.currentServices[String(describing: type.self)] as? T { 19 | service.stubs = stubs 20 | return service 21 | } 22 | return nil 23 | } 24 | 25 | /// Sets the current auth token into all currently loaded services 26 | internal class func reloadTokenForServices() { 27 | for (serviceClass, service) in ServiceLocator.shared.currentServices { 28 | var service = service 29 | service.token = APITokenManager.currentToken 30 | ServiceLocator.shared.currentServices[serviceClass] = service 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Testing/ServiceStub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Service.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 2/27/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | /// Defines the Behavior of a stub response 12 | /// 13 | /// - immediate: the sub returns inmediatly 14 | /// - delayed: the stub returns after a defined number of seconds 15 | public enum ServiceStubBehavior { 16 | /// Return a response immediately. 17 | case immediate 18 | 19 | /// Return a response after a delay. 20 | case delayed(seconds: TimeInterval) 21 | } 22 | 23 | private let validStatusCodes = (200...299) 24 | /// Used for stubbing responses. 25 | public enum ServiceStubType { 26 | 27 | /// Builds a ServiceStubType from a HTTP Code and a local JSON file 28 | /// 29 | /// - Parameters: 30 | /// - jsonFile: the name of the bundled json file 31 | /// - code: the http code. Success codes are (200..299) 32 | public init(buildWith jsonFile:String, http code:Int) { 33 | let response = JSONSerialization.JSONObjectWithFileName(fileName: jsonFile) 34 | if validStatusCodes.contains(code) { 35 | self = .success(code: code, response: response) 36 | } else { 37 | self = .failure(code: code, response: response) 38 | } 39 | } 40 | 41 | /// The network returned a response, including status code and response. 42 | case success(code:Int, response:[String:Any]?) 43 | 44 | /// The network request failed with an error 45 | case failure(code:Int, response:[String:Any]?) 46 | } 47 | 48 | /// Defines the scenario case that this request expects 49 | /// 50 | /// - authenticated: service is authenticated 51 | /// - unauthenticated: service is unauthenticated 52 | public enum ServiceStubCase { 53 | case authenticated(tokenInfo:[String:Any]) 54 | case unauthenticated 55 | } 56 | 57 | /// Defines a stub request case 58 | public struct ServiceStubRequest { 59 | 60 | /// URL Path to regex against upcoming requests 61 | public let path:String 62 | 63 | /// Optional parameters that will get compare as well against requests with the same kinda of parameters 64 | public let parameters:[String:Any]? 65 | 66 | public init(path:String, parameters:[String:Any]? = nil) { 67 | self.path = path 68 | self.parameters = parameters 69 | } 70 | } 71 | 72 | /// Defines stub response for a matching API path 73 | public struct ServiceStub { 74 | /// A stubbed request 75 | public let request:ServiceStubRequest 76 | 77 | /// The type of stubbing we want to do, either a success or a failure 78 | public let stubType:ServiceStubType 79 | 80 | /// The behavior for this stub, if we want the request to come back sync or async 81 | public let stubBehavior:ServiceStubBehavior 82 | 83 | /// The type of case we want when the request gets executed, either authenticated with a token or unauthenticated 84 | public let stubCase:ServiceStubCase 85 | 86 | public init(execute request:ServiceStubRequest, 87 | with type:ServiceStubType, 88 | when stubCase:ServiceStubCase, 89 | react behavior:ServiceStubBehavior) 90 | { 91 | self.request = request 92 | self.stubType = type 93 | self.stubBehavior = behavior 94 | self.stubCase = stubCase 95 | } 96 | } 97 | 98 | extension JSONSerialization 99 | { 100 | 101 | /// Builds a JSON Dictionary from a bundled json file 102 | /// 103 | /// - Parameter fileName: the name of the json file 104 | /// - Returns: returns a JSON dictionary 105 | public class func JSONObjectWithFileName(fileName:String) -> [String:Any]? 106 | { 107 | if let path = Bundle.currentTestBundle?.path(forResource: fileName, ofType: "json"), 108 | let jsonData = NSData(contentsOfFile: path), 109 | let jsonResult = try! JSONSerialization.jsonObject(with: jsonData as Data, options: ReadingOptions.mutableContainers) as? [String:Any] 110 | { 111 | return jsonResult 112 | } 113 | return nil 114 | } 115 | } 116 | 117 | extension Bundle { 118 | 119 | /// Locates the first bundle with a '.xctest' file extension. 120 | internal static var currentTestBundle: Bundle? { 121 | return allBundles.first { $0.bundlePath.hasSuffix(".xctest") } 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /NetworkingServiceKit/Classes/Testing/StubNetworkManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StubNetworkManager.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 6/1/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | open class StubNetworkManager: NetworkManager { 13 | open var configuration: APIConfiguration 14 | 15 | public required init(with configuration: APIConfiguration, 16 | interceptor: RequestInterceptor?) { 17 | self.configuration = configuration 18 | } 19 | 20 | // MARK: - Request handling 21 | 22 | /// Creates and executes a stubbed request through the stubbed cases 23 | /// 24 | /// - Parameters: 25 | /// - path: full path to the URL 26 | /// - method: HTTP method, default is GET 27 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 28 | /// - paginated: if the request should follow pagination, success only if all pages are completed 29 | /// - cachePolicy: specifices the policy to follow for caching responses (unused for stub network responses) 30 | /// - headers: custom headers that should be attached with this request 31 | /// - stubs: a list of stubbed cases for this request to get compare against 32 | /// - success: success block with a response 33 | /// - failure: failure block with an error 34 | open func request(path: String, 35 | method: NetworkingServiceKit.HTTPMethod = .get, 36 | with parameters: RequestParameters = RequestParameters(), 37 | paginated: Bool = false, 38 | cachePolicy: CacheResponsePolicy = CacheResponsePolicy.default, 39 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 40 | stubs: [ServiceStub], 41 | success: @escaping SuccessResponseBlock, 42 | failure: @escaping ErrorResponseBlock) { 43 | executeStub(forPath: path, withParameters: parameters, andStubs: stubs, success: success, failure: failure) 44 | } 45 | 46 | public func upload(path: String, 47 | withConstructingBlock 48 | constructingBlock: @escaping (MultipartFormData) -> Void, 49 | progressBlock: @escaping (Progress) -> Void, 50 | headers: CustomHTTPHeaders, 51 | stubs: [ServiceStub], 52 | success: @escaping SuccessResponseBlock, 53 | failure: @escaping ErrorResponseBlock) { 54 | executeStub(forPath: path, andStubs: stubs, success: success, failure: failure) 55 | } 56 | 57 | private func executeStub(forPath path: String, 58 | withParameters parameters: RequestParameters = RequestParameters(), 59 | andStubs stubs: [ServiceStub], 60 | success: @escaping SuccessResponseBlock, 61 | failure: @escaping ErrorResponseBlock) { 62 | let matchingRequests = stubs.filter { path.contains($0.request.path) && ($0.request.parameters == nil || 63 | NSDictionary(dictionary: parameters).isEqual(to: NSDictionary(dictionary: $0.request.parameters ?? [:]) as! [AnyHashable : Any])) } 64 | 65 | if let matchingRequest = matchingRequests.first { 66 | 67 | switch matchingRequest.stubCase { 68 | case .authenticated(let tokenInfo): 69 | APITokenManager.store(tokenInfo: tokenInfo) 70 | case .unauthenticated: 71 | APITokenManager.clearAuthentication() 72 | } 73 | // Make sure all services are up to date with the auth state 74 | ServiceLocator.reloadTokenForServices() 75 | 76 | //lets take the first stubbed that works with this request 77 | switch matchingRequest.stubType { 78 | case .success(_, let response): 79 | executeBlock(with: matchingRequest.stubBehavior) { 80 | success(response ?? [:]) 81 | } 82 | 83 | case .failure(let code, let response): 84 | executeBlock(with: matchingRequest.stubBehavior) { 85 | let reason = MSErrorType.ResponseFailureReason(code: code) 86 | let details = self.buildErrorDetails(from: response, path: path, code: code) 87 | failure(MSError(type: .responseValidation(reason: reason), details: details)) 88 | } 89 | } 90 | } else { 91 | fatalError("A request for \(path) got executed through StubNetworkManager but there was no valid stubs for it, make sure you have a valid path and parameters.") 92 | } 93 | } 94 | 95 | 96 | /// Builds a MSErrorDetails from the given stubbed error response 97 | /// 98 | /// - Parameters: 99 | /// - response: an error response 100 | /// - path: current api path 101 | /// - Returns: a MSErrorDetails if the response had valid data 102 | private func buildErrorDetails(from response:[String:Any]?, path:String, code: Int) -> MSErrorDetails { 103 | var message: String? = nil 104 | var body: String? = nil 105 | 106 | if let response = response, 107 | let responseError = response["error"] as? [String: Any] { 108 | message = responseError["message"] as? String 109 | let jsonData = try! JSONSerialization.data(withJSONObject: response, options: .prettyPrinted) 110 | 111 | body = String(data: jsonData, encoding: String.Encoding.utf8) 112 | } 113 | 114 | return MSErrorDetails(message: message ?? "", body: body, path: path, code: code, underlyingError: nil) 115 | } 116 | 117 | /// Executes a block based on a stubbed behavior 118 | /// 119 | /// - Parameters: 120 | /// - behavior: a behavior 121 | /// - closure: the block to execute 122 | private func executeBlock(with behavior:ServiceStubBehavior, closure:@escaping ()->()) { 123 | switch behavior { 124 | case .immediate: 125 | closure() 126 | case .delayed(let delay): 127 | let time = DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC) 128 | DispatchQueue.main.asyncAfter(deadline: time, execute: closure) 129 | } 130 | 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /NetworkingServiceKit/ReactiveSwift/NetworkManager+ReactiveSwift.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkManager+ReactiveSwift.swift 3 | // Makespace Inc. 4 | // 5 | // Created by Phillipe Casorla Sagot (@darkzlave) on 7/31/17. 6 | // 7 | // 8 | 9 | import Foundation 10 | import ReactiveSwift 11 | 12 | public protocol ReactiveSwiftNetworkManager : NetworkManager { 13 | func request(path: String, 14 | method: NetworkingServiceKit.HTTPMethod, 15 | with parameters: RequestParameters, 16 | paginated: Bool, 17 | cachePolicy:CacheResponsePolicy, 18 | headers: CustomHTTPHeaders, 19 | stubs: [ServiceStub]) -> SignalProducer<[String:Any]?, MSError> 20 | } 21 | 22 | extension AlamoNetworkManager: ReactiveSwiftNetworkManager { 23 | 24 | /// Creates and executes a request using Alamofire in a Reactive form 25 | /// 26 | /// - Parameters: 27 | /// - path: full path to the URL 28 | /// - method: HTTP method, default is GET 29 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 30 | /// - paginated: if the request should follow pagination, success only if all pages are completed 31 | /// - cachePolicy: specifices the policy to follow for caching responses 32 | /// - headers: custom headers that should be attached with this request 33 | public func request(path: String, 34 | method: NetworkingServiceKit.HTTPMethod = .get, 35 | with parameters: RequestParameters = RequestParameters(), 36 | paginated: Bool = false, 37 | cachePolicy:CacheResponsePolicy = CacheResponsePolicy.default, 38 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 39 | stubs: [ServiceStub] = [ServiceStub]()) -> SignalProducer<[String:Any]?, MSError> { 40 | return SignalProducer { [weak self] observer, lifetime in 41 | self?.request(path: path, 42 | method: method, 43 | with: parameters, 44 | paginated: paginated, 45 | cachePolicy: cachePolicy, 46 | headers: headers, 47 | stubs: stubs, success: { response in 48 | observer.send(value: response) 49 | observer.sendCompleted() 50 | }, failure: { error in 51 | observer.send(error: error) 52 | }) 53 | } 54 | } 55 | } 56 | 57 | extension StubNetworkManager: ReactiveSwiftNetworkManager { 58 | 59 | /// Creates and executes a request using StubNetworkManager in a Reactive form 60 | /// 61 | /// - Parameters: 62 | /// - path: full path to the URL 63 | /// - method: HTTP method, default is GET 64 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 65 | /// - paginated: if the request should follow pagination, success only if all pages are completed 66 | /// - cachePolicy: specifices the policy to follow for caching responses 67 | /// - headers: custom headers that should be attached with this request 68 | public func request(path: String, 69 | method: NetworkingServiceKit.HTTPMethod = .get, 70 | with parameters: RequestParameters = RequestParameters(), 71 | paginated: Bool = false, 72 | cachePolicy:CacheResponsePolicy = CacheResponsePolicy.default, 73 | headers: CustomHTTPHeaders = CustomHTTPHeaders(), 74 | stubs: [ServiceStub] = [ServiceStub]()) -> SignalProducer<[String:Any]?, MSError> { 75 | return SignalProducer { [weak self] observer, lifetime in 76 | self?.request(path: path, 77 | method: method, 78 | with: parameters, 79 | paginated: paginated, 80 | cachePolicy: cachePolicy, 81 | headers: headers, 82 | stubs: stubs, success: { response in 83 | observer.send(value: response) 84 | observer.sendCompleted() 85 | }, failure: { error in 86 | observer.send(error: error) 87 | }) 88 | } 89 | } 90 | 91 | } 92 | 93 | public extension Service { 94 | 95 | /// Lets a service execute a request using the default networking client in a Reactive form 96 | /// 97 | /// - Parameters: 98 | /// - path: full path to the URL 99 | /// - method: HTTP method, default is GET 100 | /// - parameters: URL or body parameters depending on the HTTP method, default is empty 101 | /// - paginated: if the request should follow pagination, success only if all pages are completed 102 | /// - cachePolicy: specifices the policy to follow for caching responses 103 | /// - headers: custom headers that should be attached with this request 104 | func request(path: String, 105 | baseUrlOverride: String? = nil, 106 | serviceVersionOverride: String? = nil, 107 | method: NetworkingServiceKit.HTTPMethod = .get, 108 | with parameters: RequestParameters = RequestParameters(), 109 | paginated: Bool = false, 110 | cachePolicy:CacheResponsePolicy = CacheResponsePolicy.default, 111 | headers: CustomHTTPHeaders = CustomHTTPHeaders()) -> SignalProducer<[String:Any]?, MSError> { 112 | 113 | if let ReactiveSwiftNetworkManager = self.networkManager as? ReactiveSwiftNetworkManager { 114 | return ReactiveSwiftNetworkManager.request(path: servicePath(for: path, 115 | baseUrlOverride: baseUrlOverride, 116 | serviceVersionOverride: serviceVersionOverride), 117 | method: method, 118 | with: parameters, 119 | paginated: paginated, 120 | cachePolicy: cachePolicy, 121 | headers: headers, 122 | stubs: self.stubs).on(failed: { error in 123 | if error.hasTokenExpired && self.isAuthenticated { 124 | //If our error response was because our token expired, then lets tell the delegate 125 | ServiceLocator.shared.delegate?.authenticationTokenDidExpire(forService: self) 126 | } 127 | }) 128 | } 129 | 130 | return SignalProducer.empty 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![BuddyBuild](https://dashboard.buddybuild.com/api/statusImage?appID=58e4111d378b330001f0228e&branch=master&build=latest)](https://dashboard.buddybuild.com/apps/58e4111d378b330001f0228e/build/latest?branch=master) 2 | 3 |

4 | NetworkingServiceKit 5 |

6 | 7 | 8 | ## Description 9 | 10 | NetworkingServiceKit is a library for building modular microservices. It is built 100% in swift and follows a design pattern known as the [Service Locator](https://msdn.microsoft.com/en-us/library/ff648968.aspx). 11 | 12 | NetworkingServiceKit works as a full fledge replacement for the standard iOS monolith API client. Using a modular approach to services, the framework enables the app to select which services it requires to run so that networking can happen seamlessly. 13 | 14 | Networking is usually one of the biggest responsibilities a service layer requires and is a standard requirement in most apps. NetworkingServiceKit includes out-of-the-box authentication for requests. It uses a decoupled AlamoFire client along with a set of protocols that define your authentication needs to seamlessly execute requests while encapsulating token authentication. This makes changes to your network architecture a breeze - updating your Alamofire version, or using a stub networking library, becomes a painless task instead of a complete rewrite. 15 | 16 | To launch our **ServiceLocator** class you will need to define your list of services plus implementations of your authentication, your token and server details. For example: 17 | 18 | ```swift 19 | ServiceLocator.set(services: [TwitterAuthenticationService.self,LocationService.self,TwitterSearchService.self], 20 | api: TwitterAPIConfigurationType.self, 21 | auth: TwitterApp.self, 22 | token: TwitterAPIToken.self) 23 | 24 | ``` 25 | For this example we have created a simple Twitter client and implemented Twitter token handling, authentication and defined their server details. Our [TwitterAPIConfigurationType](https://github.com/makingspace/NetworkingServiceKit/blob/feature/OpenSourceExample2/Example/NetworkingServiceKit/TwitterAPIConfiguration.swift#L57) tells the service layer information about our server base URL. [TwitterApp](https://github.com/makingspace/NetworkingServiceKit/blob/feature/OpenSourceExample2/Example/NetworkingServiceKit/TwitterAPIConfiguration.swift#L12) gives the details needed for signing a request with a key and a secret. [TwitterAPIToken](https://github.com/makingspace/NetworkingServiceKit/blob/feature/OpenSourceExample2/Example/NetworkingServiceKit/TwitterAPIToken.swift#L13) implements how we are going to parse and store token information once we have been authenticated. 26 | 27 | Once our client has been authenticated, all requests going through one of our implemented Service will get automatically signed by our implementation of an APIToken. 28 | 29 | For requesting one of the loaded services you simply ask the service locator, for example: 30 | 31 | ```swift 32 | let twitterSearchService = ServiceLocator.service(forType: TwitterSearchService.self) 33 | twitterSearchService?.searchRecents(by: searchText, completion: { [weak self] results in 34 | 35 | if let annotations = self?.mapView.annotations { 36 | self?.mapView.removeAnnotations(annotations) 37 | } 38 | self?.currentResults = results 39 | self?.tweetsTableView.reloadData() 40 | self?.showTweetsLocationsOnMap() 41 | }) 42 | ``` 43 | Each defined service can be linked to a specific microservice path and version by overriding the servicePath and serviceVersion properties, for example: 44 | 45 | ```swift 46 | open class TwitterSearchService: AbstractBaseService { 47 | 48 | public override var serviceVersion: String { 49 | return "1.1" 50 | } 51 | 52 | public override var servicePath:String { 53 | return "search" 54 | } 55 | 56 | public func searchRecents(by hashtag:String, 57 | completion:@escaping (_ results:[TwitterSearchResult])-> Void) { 58 | } 59 | } 60 | ``` 61 | This will automatically prefix all request URLs in **TwitterSearchService** start with **search/1.1/**, so for the example func above, the full URL for the executed request will be something like https://api.twitter.com/search/v4/tweets.json. 62 | 63 | ## Stubbing 64 | 65 | NetworkingServiceKit supports out of the box stubbing request through a custom API Client: **StubNetworkManager**. 66 | 67 | ```swift 68 | ServiceLocator.defaultNetworkClientType = StubNetworkManager.self 69 | ``` 70 | Once you have set up our Stub client, all you need is to request a service with a set of stubs, this stubs will get automatically link to a request if they matches the same criteria the stub it's defining. For Example: 71 | 72 | ```swift 73 | let searchStub = ServiceStub(execute: 74 | ServiceStubRequest(path: "/1.1/search/tweets.json", 75 | parameters: ["q" : "#makespace"]), 76 | with: .success(code: 200, 77 | response: ["statuses" : [["text" : "tweet1" , 78 | "user" : ["screen_name" : "darkzlave", 79 | "profile_image_url_https" : "https://lol.png", 80 | "location" : "Stockholm, Sweden"]], 81 | ["text" : "tweet2" , 82 | "user" : ["screen_name" : "makespace", 83 | "profile_image_url_https" : "https://lol2.png", 84 | "location" : "New York"]]], 85 | "search_metadata" : ["next_results" : "https://search.com/next?pageId=2"] 86 | ]), when: .authenticated(tokenInfo: ["token_type" : "access", 87 | "access_token" : "KWALI"]), 88 | react:.delayed(seconds: 0.5)) 89 | let searchService = ServiceLocator.service(forType: TwitterSearchService.self, stubs: [searchStub]) 90 | ``` 91 | 92 | Our example searchStub will get returned for all .get authenticated requests that are under the path **/1.1/search/tweets.json?q=#makespace** and the request will return after 0.5sec. For our TwitterSearchService this occurs all seemlessly without having to do any changes on the code for it to be tested. 93 | 94 | ## ReactiveSwift 95 | NSK includes supports for [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift) through `ReactiveSwiftNetworkManager`. This enable services to return `SignalProducer` instead of relying on completion blocks. This can easily enable chaining and mapping of network responses, for example: 96 | 97 | ```swift 98 | 99 | func searchNextRecentsPageProducer() -> SignalProducer<[TwitterSearchResult],MSError> { 100 | guard let nextPage = self.nextResultsPage else { 101 | return SignalProducer.empty 102 | } 103 | let request = request(path: "search/tweets.json\(nextPage)") 104 | return request.map { response -> [TwitterSearchResult] in 105 | var searchResults = [TwitterSearchResult]() 106 | 107 | guard let response = response else { return searchResults } 108 | 109 | if let results = response["statuses"] as? [[String:Any]] { 110 | for result in results { 111 | if let twitterResult = TwitterSearchResult(dictionary: result) { 112 | searchResults.append(twitterResult) 113 | } 114 | } 115 | } 116 | 117 | return searchResults 118 | } 119 | } 120 | 121 | ``` 122 | 123 | 124 | ## Example 125 | 126 | To run the example project, clone the repo, and run `pod install` from the Example directory first. 127 | 128 | The example project consists of a very simple Twitter client that authenticates automatically using Twitter's Application-only authentication (https://dev.twitter.com/oauth/application-only). The client supports searching for tweets, pagination of results and showing the location of the listed tweets in a map. 129 | 130 | ## Requirements 131 | 132 | ## Installation 133 | 134 | NetworkingServiceKit is available through [CocoaPods](http://cocoapods.org). To install 135 | it, simply add the following line to your Podfile: 136 | 137 | ```ruby 138 | pod "NetworkingServiceKit" 139 | ``` 140 | 141 | ## Author 142 | 143 | darkzlave, phillipe@makespace.com 144 | 145 | ## License 146 | 147 | NetworkingServiceKit is available under the MIT license. See the LICENSE file for more info. 148 | -------------------------------------------------------------------------------- /_Pods.xcodeproj: -------------------------------------------------------------------------------- 1 | Example/Pods/Pods.xcodeproj -------------------------------------------------------------------------------- /update-spec.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | pod repo push makingspace NetworkingServiceKit.podspec --allow-warnings 3 | --------------------------------------------------------------------------------