├── .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 |
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 |
--------------------------------------------------------------------------------
/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 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
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 | [](https://dashboard.buddybuild.com/apps/58e4111d378b330001f0228e/build/latest?branch=master)
2 |
3 |
4 |
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 |
--------------------------------------------------------------------------------