├── .github └── workflows │ └── swift.yml ├── .gitignore ├── .swiftlint.yml ├── Cartfile ├── Cartfile.resolved ├── LICENSE ├── README.md ├── TryNetworkLayer.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── TryNetworkLayer.xcscheme ├── TryNetworkLayer.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── swiftpm │ └── Package.resolved ├── TryNetworkLayer ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Controllers │ ├── UserDetail │ │ ├── UserDetailCoordinator.swift │ │ ├── UserDetailViewController.swift │ │ └── UserDetailViewModel.swift │ └── Users │ │ ├── UsersCoordinator.swift │ │ ├── UsersViewController.swift │ │ └── UsersViewModel.swift ├── Coordinator │ └── AppCoordinator.swift ├── Extension.swift ├── Info.plist ├── Models │ ├── GHSearchResponse.swift │ ├── GHUser.swift │ └── GHUserDetail.swift ├── Network │ ├── Dispatcher.swift │ ├── Environment.swift │ ├── NetworkDispatcher.swift │ ├── Operations │ │ ├── Operation.swift │ │ ├── UserDetailOperation.swift │ │ └── UsersOperation.swift │ ├── Requests │ │ ├── Request.swift │ │ └── UserRequests.swift │ └── Response.swift ├── Repository │ └── UsersRepo.swift ├── SceneDelegate.swift ├── Storage │ ├── RealmStorageContext.swift │ ├── Storable.swift │ └── StorageContext.swift └── Storyboards │ ├── Base.lproj │ └── Main.storyboard │ └── Storyboards.swift ├── TryNetworkLayerTests ├── Controllers │ ├── UserDetail │ │ └── UserDetailViewModelTests.swift │ └── Users │ │ └── UsersViewModelTests.swift ├── Info.plist ├── Mocks │ ├── MockGHUser.swift │ ├── MockSearchResponse.swift │ ├── MockUserDetailOperation.swift │ ├── MockUserOperation.swift │ └── MockUsersRepo.swift ├── Network │ ├── UserDetailOperationTests.swift │ └── UsersOperationTests.swift ├── Repository │ └── UsersRepoTests.swift └── TestUtils.swift └── wiremock ├── README.md ├── __files └── users │ ├── detail.json │ ├── search.json │ └── users.json ├── mappings └── users │ ├── detail.json │ ├── search.json │ └── users.json ├── start_server.sh └── wiremock-standalone-2.22.0.jar /.github/workflows/swift.yml: -------------------------------------------------------------------------------- 1 | name: Swift 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: macos-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Build 17 | run: swift build -v 18 | - name: Run tests 19 | run: swift test -v 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData/ 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xccheckout 23 | *.xcscmblueprint 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | ## Playgrounds 32 | timeline.xctimeline 33 | playground.xcworkspace 34 | 35 | # Swift Package Manager 36 | # 37 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 38 | # Packages/ 39 | # Package.pins 40 | .build/ 41 | 42 | # CocoaPods 43 | # 44 | # We recommend against adding the Pods directory to your .gitignore. However 45 | # you should judge for yourself, the pros and cons are mentioned at: 46 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 47 | # 48 | # Pods/ 49 | 50 | # Carthage 51 | # 52 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 53 | # Carthage/Checkouts 54 | 55 | Carthage/Build 56 | 57 | # fastlane 58 | # 59 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 60 | # screenshots whenever they are needed. 61 | # For more information about the recommended setup visit: 62 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 63 | 64 | fastlane/report.xml 65 | fastlane/Preview.html 66 | fastlane/screenshots 67 | fastlane/test_output 68 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - force_cast 3 | - trailing_whitespace 4 | 5 | opt_in_rules: 6 | - array_init 7 | - closure_spacing 8 | - contains_over_first_not_nil 9 | - explicit_init 10 | - extension_access_modifier 11 | - fatal_error_message 12 | - first_where 13 | - implicit_return 14 | - joined_default_parameter 15 | - let_var_whitespace 16 | - literal_expression_end_indentation 17 | - operator_usage_whitespace 18 | - overridden_super_call 19 | - prohibited_super_call 20 | - quick_discouraged_call 21 | - sorted_imports 22 | - vertical_parameter_alignment_on_call 23 | - todo 24 | - unowned_variable_capture 25 | - duplicate_enum_cases 26 | - unused_capture_list 27 | - toggle_bool 28 | - vertical_whitespace_closing_braces 29 | - required_enum_case 30 | - private_outlet 31 | - switch_case_on_newline 32 | - empty_xctest_method 33 | 34 | included: # paths to include during linting. `--path` is ignored if present. 35 | - TryNetworkLayer 36 | 37 | excluded: # paths to ignore during linting. Takes precedence over `included`. 38 | - Pods 39 | 40 | 41 | # These properties are marked as error by default. 42 | force_try: warning 43 | 44 | file_length: 45 | warning: 500 46 | error: 700 47 | 48 | function_body_length: 49 | warning: 150 50 | error: 200 51 | 52 | type_body_length: 53 | warning: 300 54 | error: 350 55 | 56 | function_parameter_count: 57 | warning: 10 58 | error: 15 59 | 60 | line_length: 61 | warning: 160 62 | error: 170 63 | 64 | type_name: 65 | min_length: 4 66 | max_length: 67 | warning: 50 68 | error: 60 69 | allowed_symbols: ["_"] 70 | excluded: 71 | - R 72 | - D 73 | 74 | identifier_name: 75 | min_length: 2 76 | max_length: 77 | warning: 50 78 | error: 60 79 | allowed_symbols: ["_"] 80 | excluded: 81 | - id 82 | 83 | reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) 84 | 85 | custom_rules: 86 | comments_space: # From https://github.com/brandenr/swiftlintconfig 87 | name: "Space After Comment" 88 | regex: "(^ *//\\w+)" 89 | message: "There should be a space after //" 90 | severity: warning 91 | 92 | double_space: # From https://github.com/IBM-Swift/Package-Builder 93 | include: "*.swift" 94 | name: "Double space" 95 | regex: "([a-z,A-Z] \\s+)" 96 | message: "Double space between keywords" 97 | match_kinds: keyword 98 | severity: warning 99 | 100 | mark_newline: 101 | include: "*.swift" 102 | name: "MARK new line" 103 | regex: "(^ *\\/\\/ MARK:\ [ a-zA-Z0-9=?.\\(\\)\\{\\}:,>"; }; 69 | 65197A4F1F5AA9DC007C8C64 /* StorageContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageContext.swift; sourceTree = ""; }; 70 | 65197A521F5AAA52007C8C64 /* UsersRepo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersRepo.swift; sourceTree = ""; }; 71 | 65197A541F5AAD26007C8C64 /* RealmStorageContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmStorageContext.swift; sourceTree = ""; }; 72 | 65197A581F5AAD9D007C8C64 /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = .swiftlint.yml; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 73 | 65197A5B1F5AB4BB007C8C64 /* UsersRepoTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersRepoTests.swift; sourceTree = ""; }; 74 | 65197A5F1F5C1360007C8C64 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 75 | 65197A611F5CAE49007C8C64 /* GHUserDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GHUserDetail.swift; sourceTree = ""; }; 76 | 65197A631F5CB0DD007C8C64 /* UserDetailOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetailOperation.swift; sourceTree = ""; }; 77 | 65197A651F5CB131007C8C64 /* UserDetailOperationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetailOperationTests.swift; sourceTree = ""; }; 78 | 65197A6D1F5CB269007C8C64 /* UserDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetailViewController.swift; sourceTree = ""; }; 79 | 65197A6E1F5CB269007C8C64 /* UserDetailViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDetailViewModel.swift; sourceTree = ""; }; 80 | 65197A6F1F5CB269007C8C64 /* UsersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersViewController.swift; sourceTree = ""; }; 81 | 65197A701F5CB269007C8C64 /* UsersViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersViewModel.swift; sourceTree = ""; }; 82 | 6530425D20C6C196002B373E /* Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extension.swift; sourceTree = ""; }; 83 | 65574E4E1F4CB28B000D717E /* GHSearchResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GHSearchResponse.swift; sourceTree = ""; }; 84 | 65BBB9471F4B3425001A378C /* TryNetworkLayer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TryNetworkLayer.app; sourceTree = BUILT_PRODUCTS_DIR; }; 85 | 65BBB94A1F4B3425001A378C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 86 | 65BBB94F1F4B3425001A378C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 87 | 65BBB9511F4B3425001A378C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 88 | 65BBB9541F4B3425001A378C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 89 | 65BBB9561F4B3425001A378C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 90 | 65BBB95B1F4B3425001A378C /* TryNetworkLayerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TryNetworkLayerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 91 | 65BBB95F1F4B3425001A378C /* UsersOperationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersOperationTests.swift; sourceTree = ""; }; 92 | 65BBB9611F4B3425001A378C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 93 | 65BBB97B1F4C1BFE001A378C /* Dispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Dispatcher.swift; sourceTree = ""; }; 94 | 65BBB97C1F4C1BFE001A378C /* Environment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Environment.swift; sourceTree = ""; }; 95 | 65BBB97D1F4C1BFE001A378C /* NetworkDispatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkDispatcher.swift; sourceTree = ""; }; 96 | 65BBB97F1F4C1BFE001A378C /* UsersOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UsersOperation.swift; sourceTree = ""; }; 97 | 65BBB9801F4C1BFE001A378C /* Operation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; 98 | 65BBB9821F4C1BFE001A378C /* Request.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; 99 | 65BBB9831F4C1BFE001A378C /* UserRequests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserRequests.swift; sourceTree = ""; }; 100 | 65BBB9841F4C1BFE001A378C /* Response.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 101 | 65BBB98E1F4C20EF001A378C /* GHUser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GHUser.swift; sourceTree = ""; }; 102 | FC1520F4241B96FD000B6071 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 103 | FC1520F7241B981B000B6071 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; 104 | FC1520F9241B9C04000B6071 /* UsersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersCoordinator.swift; sourceTree = ""; }; 105 | FC1520FB241B9C1A000B6071 /* UserDetailCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailCoordinator.swift; sourceTree = ""; }; 106 | FC1520FD241B9EE0000B6071 /* Storyboards.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Storyboards.swift; sourceTree = ""; }; 107 | FC9561332410054700E47427 /* TestUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestUtils.swift; sourceTree = ""; }; 108 | FC95613924100ADE00E47427 /* Realm.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Realm.framework; path = Carthage/Build/iOS/Realm.framework; sourceTree = ""; }; 109 | FC95613B24100AE100E47427 /* RealmSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RealmSwift.framework; path = Carthage/Build/iOS/RealmSwift.framework; sourceTree = ""; }; 110 | FC95613E2411105000E47427 /* UserDetailViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDetailViewModelTests.swift; sourceTree = ""; }; 111 | FC9561402411106500E47427 /* UsersViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersViewModelTests.swift; sourceTree = ""; }; 112 | FC95614324113AA100E47427 /* MockGHUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockGHUser.swift; sourceTree = ""; }; 113 | FC95614524113AB500E47427 /* MockUserDetailOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserDetailOperation.swift; sourceTree = ""; }; 114 | FC95614724113AEB00E47427 /* MockSearchResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSearchResponse.swift; sourceTree = ""; }; 115 | FC95614924113B0E00E47427 /* MockUserOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserOperation.swift; sourceTree = ""; }; 116 | FC95614B24113B4200E47427 /* MockUsersRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUsersRepo.swift; sourceTree = ""; }; 117 | /* End PBXFileReference section */ 118 | 119 | /* Begin PBXFrameworksBuildPhase section */ 120 | 65BBB9441F4B3425001A378C /* Frameworks */ = { 121 | isa = PBXFrameworksBuildPhase; 122 | buildActionMask = 2147483647; 123 | files = ( 124 | FC95613A24100ADF00E47427 /* Realm.framework in Frameworks */, 125 | FC95613C24100AE100E47427 /* RealmSwift.framework in Frameworks */, 126 | FC9561372410086300E47427 /* Alamofire in Frameworks */, 127 | ); 128 | runOnlyForDeploymentPostprocessing = 0; 129 | }; 130 | 65BBB9581F4B3425001A378C /* Frameworks */ = { 131 | isa = PBXFrameworksBuildPhase; 132 | buildActionMask = 2147483647; 133 | files = ( 134 | ); 135 | runOnlyForDeploymentPostprocessing = 0; 136 | }; 137 | /* End PBXFrameworksBuildPhase section */ 138 | 139 | /* Begin PBXGroup section */ 140 | 65197A4C1F5AA951007C8C64 /* Storage */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 65197A4D1F5AA976007C8C64 /* Storable.swift */, 144 | 65197A4F1F5AA9DC007C8C64 /* StorageContext.swift */, 145 | 65197A541F5AAD26007C8C64 /* RealmStorageContext.swift */, 146 | ); 147 | path = Storage; 148 | sourceTree = ""; 149 | }; 150 | 65197A511F5AAA20007C8C64 /* Repository */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 65197A521F5AAA52007C8C64 /* UsersRepo.swift */, 154 | ); 155 | path = Repository; 156 | sourceTree = ""; 157 | }; 158 | 65197A6C1F5CB250007C8C64 /* Controllers */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | FC1520F2241B9666000B6071 /* Users */, 162 | FC1520F3241B9670000B6071 /* UserDetail */, 163 | ); 164 | path = Controllers; 165 | sourceTree = ""; 166 | }; 167 | 65262AE121AED069005AE7C9 /* Network */ = { 168 | isa = PBXGroup; 169 | children = ( 170 | 65BBB95F1F4B3425001A378C /* UsersOperationTests.swift */, 171 | 65197A651F5CB131007C8C64 /* UserDetailOperationTests.swift */, 172 | ); 173 | path = Network; 174 | sourceTree = ""; 175 | }; 176 | 65262AE221AED089005AE7C9 /* Repository */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 65197A5B1F5AB4BB007C8C64 /* UsersRepoTests.swift */, 180 | ); 181 | path = Repository; 182 | sourceTree = ""; 183 | }; 184 | 65BBB93E1F4B3425001A378C = { 185 | isa = PBXGroup; 186 | children = ( 187 | 65197A5F1F5C1360007C8C64 /* README.md */, 188 | 65197A581F5AAD9D007C8C64 /* .swiftlint.yml */, 189 | 65BBB9491F4B3425001A378C /* TryNetworkLayer */, 190 | 65BBB95E1F4B3425001A378C /* TryNetworkLayerTests */, 191 | 65BBB9481F4B3425001A378C /* Products */, 192 | A68FFA4D1D6D3AD10C59AADB /* Frameworks */, 193 | ); 194 | sourceTree = ""; 195 | }; 196 | 65BBB9481F4B3425001A378C /* Products */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 65BBB9471F4B3425001A378C /* TryNetworkLayer.app */, 200 | 65BBB95B1F4B3425001A378C /* TryNetworkLayerTests.xctest */, 201 | ); 202 | name = Products; 203 | sourceTree = ""; 204 | }; 205 | 65BBB9491F4B3425001A378C /* TryNetworkLayer */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 65BBB9511F4B3425001A378C /* Assets.xcassets */, 209 | 65BBB94A1F4B3425001A378C /* AppDelegate.swift */, 210 | FC1520F4241B96FD000B6071 /* SceneDelegate.swift */, 211 | 6530425D20C6C196002B373E /* Extension.swift */, 212 | FC1520FF241B9EED000B6071 /* Storyboards */, 213 | FC1520F6241B9809000B6071 /* Coordinator */, 214 | 65197A6C1F5CB250007C8C64 /* Controllers */, 215 | 65BBB98D1F4C20EF001A378C /* Models */, 216 | 65197A511F5AAA20007C8C64 /* Repository */, 217 | 65197A4C1F5AA951007C8C64 /* Storage */, 218 | 65BBB96A1F4B354F001A378C /* Network */, 219 | 65BBB9531F4B3425001A378C /* LaunchScreen.storyboard */, 220 | 65BBB9561F4B3425001A378C /* Info.plist */, 221 | ); 222 | path = TryNetworkLayer; 223 | sourceTree = ""; 224 | }; 225 | 65BBB95E1F4B3425001A378C /* TryNetworkLayerTests */ = { 226 | isa = PBXGroup; 227 | children = ( 228 | FC9561332410054700E47427 /* TestUtils.swift */, 229 | FC152101241BF64E000B6071 /* Coordinator */, 230 | FC95613D2411103100E47427 /* Controllers */, 231 | 65262AE221AED089005AE7C9 /* Repository */, 232 | 65262AE121AED069005AE7C9 /* Network */, 233 | FC95614224113A8000E47427 /* Mocks */, 234 | 65BBB9611F4B3425001A378C /* Info.plist */, 235 | ); 236 | path = TryNetworkLayerTests; 237 | sourceTree = ""; 238 | }; 239 | 65BBB96A1F4B354F001A378C /* Network */ = { 240 | isa = PBXGroup; 241 | children = ( 242 | 65BBB97C1F4C1BFE001A378C /* Environment.swift */, 243 | 65BBB9841F4C1BFE001A378C /* Response.swift */, 244 | 65BBB97B1F4C1BFE001A378C /* Dispatcher.swift */, 245 | 65BBB97D1F4C1BFE001A378C /* NetworkDispatcher.swift */, 246 | 65BBB9811F4C1BFE001A378C /* Requests */, 247 | 65BBB97E1F4C1BFE001A378C /* Operations */, 248 | ); 249 | path = Network; 250 | sourceTree = ""; 251 | }; 252 | 65BBB97E1F4C1BFE001A378C /* Operations */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 65BBB9801F4C1BFE001A378C /* Operation.swift */, 256 | 65BBB97F1F4C1BFE001A378C /* UsersOperation.swift */, 257 | 65197A631F5CB0DD007C8C64 /* UserDetailOperation.swift */, 258 | ); 259 | path = Operations; 260 | sourceTree = ""; 261 | }; 262 | 65BBB9811F4C1BFE001A378C /* Requests */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | 65BBB9821F4C1BFE001A378C /* Request.swift */, 266 | 65BBB9831F4C1BFE001A378C /* UserRequests.swift */, 267 | ); 268 | path = Requests; 269 | sourceTree = ""; 270 | }; 271 | 65BBB98D1F4C20EF001A378C /* Models */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | 65197A611F5CAE49007C8C64 /* GHUserDetail.swift */, 275 | 65BBB98E1F4C20EF001A378C /* GHUser.swift */, 276 | 65574E4E1F4CB28B000D717E /* GHSearchResponse.swift */, 277 | ); 278 | path = Models; 279 | sourceTree = ""; 280 | }; 281 | A68FFA4D1D6D3AD10C59AADB /* Frameworks */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | FC95613B24100AE100E47427 /* RealmSwift.framework */, 285 | FC95613924100ADE00E47427 /* Realm.framework */, 286 | ); 287 | name = Frameworks; 288 | sourceTree = ""; 289 | }; 290 | FC1520F2241B9666000B6071 /* Users */ = { 291 | isa = PBXGroup; 292 | children = ( 293 | FC1520F9241B9C04000B6071 /* UsersCoordinator.swift */, 294 | 65197A6F1F5CB269007C8C64 /* UsersViewController.swift */, 295 | 65197A701F5CB269007C8C64 /* UsersViewModel.swift */, 296 | ); 297 | path = Users; 298 | sourceTree = ""; 299 | }; 300 | FC1520F3241B9670000B6071 /* UserDetail */ = { 301 | isa = PBXGroup; 302 | children = ( 303 | FC1520FB241B9C1A000B6071 /* UserDetailCoordinator.swift */, 304 | 65197A6D1F5CB269007C8C64 /* UserDetailViewController.swift */, 305 | 65197A6E1F5CB269007C8C64 /* UserDetailViewModel.swift */, 306 | ); 307 | path = UserDetail; 308 | sourceTree = ""; 309 | }; 310 | FC1520F6241B9809000B6071 /* Coordinator */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | FC1520F7241B981B000B6071 /* AppCoordinator.swift */, 314 | ); 315 | path = Coordinator; 316 | sourceTree = ""; 317 | }; 318 | FC1520FF241B9EED000B6071 /* Storyboards */ = { 319 | isa = PBXGroup; 320 | children = ( 321 | FC1520FD241B9EE0000B6071 /* Storyboards.swift */, 322 | 65BBB94E1F4B3425001A378C /* Main.storyboard */, 323 | ); 324 | path = Storyboards; 325 | sourceTree = ""; 326 | }; 327 | FC152101241BF64E000B6071 /* Coordinator */ = { 328 | isa = PBXGroup; 329 | children = ( 330 | ); 331 | path = Coordinator; 332 | sourceTree = ""; 333 | }; 334 | FC152102241BF70C000B6071 /* Users */ = { 335 | isa = PBXGroup; 336 | children = ( 337 | FC9561402411106500E47427 /* UsersViewModelTests.swift */, 338 | ); 339 | path = Users; 340 | sourceTree = ""; 341 | }; 342 | FC152103241BF716000B6071 /* UserDetail */ = { 343 | isa = PBXGroup; 344 | children = ( 345 | FC95613E2411105000E47427 /* UserDetailViewModelTests.swift */, 346 | ); 347 | path = UserDetail; 348 | sourceTree = ""; 349 | }; 350 | FC95613D2411103100E47427 /* Controllers */ = { 351 | isa = PBXGroup; 352 | children = ( 353 | FC152102241BF70C000B6071 /* Users */, 354 | FC152103241BF716000B6071 /* UserDetail */, 355 | ); 356 | path = Controllers; 357 | sourceTree = ""; 358 | }; 359 | FC95614224113A8000E47427 /* Mocks */ = { 360 | isa = PBXGroup; 361 | children = ( 362 | FC95614724113AEB00E47427 /* MockSearchResponse.swift */, 363 | FC95614324113AA100E47427 /* MockGHUser.swift */, 364 | FC95614524113AB500E47427 /* MockUserDetailOperation.swift */, 365 | FC95614924113B0E00E47427 /* MockUserOperation.swift */, 366 | FC95614B24113B4200E47427 /* MockUsersRepo.swift */, 367 | ); 368 | path = Mocks; 369 | sourceTree = ""; 370 | }; 371 | /* End PBXGroup section */ 372 | 373 | /* Begin PBXNativeTarget section */ 374 | 65BBB9461F4B3425001A378C /* TryNetworkLayer */ = { 375 | isa = PBXNativeTarget; 376 | buildConfigurationList = 65BBB9641F4B3425001A378C /* Build configuration list for PBXNativeTarget "TryNetworkLayer" */; 377 | buildPhases = ( 378 | 4E252DEC61C97C04A6BAE960 /* Timing START */, 379 | 65BBB9431F4B3425001A378C /* Sources */, 380 | 65BBB9441F4B3425001A378C /* Frameworks */, 381 | 65BBB9451F4B3425001A378C /* Resources */, 382 | 65197A5A1F5AADC3007C8C64 /* SwiftLint */, 383 | FC95613824100A9900E47427 /* Carthage */, 384 | 6457C2E31F4AD98BCFBE52AA /* Timing END */, 385 | ); 386 | buildRules = ( 387 | ); 388 | dependencies = ( 389 | ); 390 | name = TryNetworkLayer; 391 | packageProductDependencies = ( 392 | FC9561362410086300E47427 /* Alamofire */, 393 | ); 394 | productName = TryNetworkLayer; 395 | productReference = 65BBB9471F4B3425001A378C /* TryNetworkLayer.app */; 396 | productType = "com.apple.product-type.application"; 397 | }; 398 | 65BBB95A1F4B3425001A378C /* TryNetworkLayerTests */ = { 399 | isa = PBXNativeTarget; 400 | buildConfigurationList = 65BBB9671F4B3425001A378C /* Build configuration list for PBXNativeTarget "TryNetworkLayerTests" */; 401 | buildPhases = ( 402 | 26E04812A9C40B16357A5A6C /* Timing START */, 403 | 65BBB9571F4B3425001A378C /* Sources */, 404 | 65BBB9581F4B3425001A378C /* Frameworks */, 405 | 65BBB9591F4B3425001A378C /* Resources */, 406 | FB5B7AE41C9376C5EA21F158 /* Timing END */, 407 | ); 408 | buildRules = ( 409 | ); 410 | dependencies = ( 411 | 65BBB95D1F4B3425001A378C /* PBXTargetDependency */, 412 | ); 413 | name = TryNetworkLayerTests; 414 | productName = TryNetworkLayerTests; 415 | productReference = 65BBB95B1F4B3425001A378C /* TryNetworkLayerTests.xctest */; 416 | productType = "com.apple.product-type.bundle.unit-test"; 417 | }; 418 | /* End PBXNativeTarget section */ 419 | 420 | /* Begin PBXProject section */ 421 | 65BBB93F1F4B3425001A378C /* Project object */ = { 422 | isa = PBXProject; 423 | attributes = { 424 | LastSwiftUpdateCheck = 0830; 425 | LastUpgradeCheck = 1020; 426 | ORGANIZATIONNAME = "Andrea Stevanato"; 427 | TargetAttributes = { 428 | 65BBB9461F4B3425001A378C = { 429 | CreatedOnToolsVersion = 8.3.3; 430 | DevelopmentTeam = A2VGE7F94S; 431 | LastSwiftMigration = 1020; 432 | ProvisioningStyle = Automatic; 433 | }; 434 | 65BBB95A1F4B3425001A378C = { 435 | CreatedOnToolsVersion = 8.3.3; 436 | DevelopmentTeam = A2VGE7F94S; 437 | LastSwiftMigration = 1020; 438 | ProvisioningStyle = Automatic; 439 | TestTargetID = 65BBB9461F4B3425001A378C; 440 | }; 441 | }; 442 | }; 443 | buildConfigurationList = 65BBB9421F4B3425001A378C /* Build configuration list for PBXProject "TryNetworkLayer" */; 444 | compatibilityVersion = "Xcode 3.2"; 445 | developmentRegion = English; 446 | hasScannedForEncodings = 0; 447 | knownRegions = ( 448 | English, 449 | en, 450 | Base, 451 | ); 452 | mainGroup = 65BBB93E1F4B3425001A378C; 453 | packageReferences = ( 454 | FC9561352410086300E47427 /* XCRemoteSwiftPackageReference "Alamofire" */, 455 | ); 456 | productRefGroup = 65BBB9481F4B3425001A378C /* Products */; 457 | projectDirPath = ""; 458 | projectRoot = ""; 459 | targets = ( 460 | 65BBB9461F4B3425001A378C /* TryNetworkLayer */, 461 | 65BBB95A1F4B3425001A378C /* TryNetworkLayerTests */, 462 | ); 463 | }; 464 | /* End PBXProject section */ 465 | 466 | /* Begin PBXResourcesBuildPhase section */ 467 | 65BBB9451F4B3425001A378C /* Resources */ = { 468 | isa = PBXResourcesBuildPhase; 469 | buildActionMask = 2147483647; 470 | files = ( 471 | 65197A591F5AAD9D007C8C64 /* .swiftlint.yml in Resources */, 472 | 65BBB9551F4B3425001A378C /* LaunchScreen.storyboard in Resources */, 473 | 65BBB9521F4B3425001A378C /* Assets.xcassets in Resources */, 474 | 65BBB9501F4B3425001A378C /* Main.storyboard in Resources */, 475 | ); 476 | runOnlyForDeploymentPostprocessing = 0; 477 | }; 478 | 65BBB9591F4B3425001A378C /* Resources */ = { 479 | isa = PBXResourcesBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | ); 483 | runOnlyForDeploymentPostprocessing = 0; 484 | }; 485 | /* End PBXResourcesBuildPhase section */ 486 | 487 | /* Begin PBXShellScriptBuildPhase section */ 488 | 26E04812A9C40B16357A5A6C /* Timing START */ = { 489 | isa = PBXShellScriptBuildPhase; 490 | buildActionMask = 2147483647; 491 | files = ( 492 | ); 493 | inputFileListPaths = ( 494 | ); 495 | inputPaths = ( 496 | ); 497 | name = "Timing START"; 498 | outputFileListPaths = ( 499 | ); 500 | outputPaths = ( 501 | ); 502 | runOnlyForDeploymentPostprocessing = 0; 503 | shellPath = /bin/sh; 504 | shellScript = " DATE=`date \"+%Y-%m-%dT%H:%M:%S.%s\"`\n echo \"{\\\"date\\\":\\\"$DATE\\\", \\\"taskName\\\":\\\"$TARGETNAME\\\", \\\"event\\\":\\\"start\\\"},\" >> ~/.timings.xcode\n"; 505 | }; 506 | 4E252DEC61C97C04A6BAE960 /* Timing START */ = { 507 | isa = PBXShellScriptBuildPhase; 508 | buildActionMask = 2147483647; 509 | files = ( 510 | ); 511 | inputFileListPaths = ( 512 | ); 513 | inputPaths = ( 514 | ); 515 | name = "Timing START"; 516 | outputFileListPaths = ( 517 | ); 518 | outputPaths = ( 519 | ); 520 | runOnlyForDeploymentPostprocessing = 0; 521 | shellPath = /bin/sh; 522 | shellScript = " DATE=`date \"+%Y-%m-%dT%H:%M:%S.%s\"`\n echo \"{\\\"date\\\":\\\"$DATE\\\", \\\"taskName\\\":\\\"$TARGETNAME\\\", \\\"event\\\":\\\"start\\\"},\" >> ~/.timings.xcode\n"; 523 | }; 524 | 6457C2E31F4AD98BCFBE52AA /* Timing END */ = { 525 | isa = PBXShellScriptBuildPhase; 526 | buildActionMask = 2147483647; 527 | files = ( 528 | ); 529 | inputFileListPaths = ( 530 | ); 531 | inputPaths = ( 532 | ); 533 | name = "Timing END"; 534 | outputFileListPaths = ( 535 | ); 536 | outputPaths = ( 537 | ); 538 | runOnlyForDeploymentPostprocessing = 0; 539 | shellPath = /bin/sh; 540 | shellScript = " DATE=`date \"+%Y-%m-%dT%H:%M:%S.%s\"`\n echo \"{\\\"date\\\":\\\"$DATE\\\", \\\"taskName\\\":\\\"$TARGETNAME\\\", \\\"event\\\":\\\"end\\\"},\" >> ~/.timings.xcode\n"; 541 | }; 542 | 65197A5A1F5AADC3007C8C64 /* SwiftLint */ = { 543 | isa = PBXShellScriptBuildPhase; 544 | buildActionMask = 2147483647; 545 | files = ( 546 | ); 547 | inputPaths = ( 548 | ); 549 | name = SwiftLint; 550 | outputPaths = ( 551 | ); 552 | runOnlyForDeploymentPostprocessing = 0; 553 | shellPath = /bin/sh; 554 | shellScript = "if which swiftlint >/dev/null; then\n echo \"\" >/dev/null;\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 555 | }; 556 | FB5B7AE41C9376C5EA21F158 /* Timing END */ = { 557 | isa = PBXShellScriptBuildPhase; 558 | buildActionMask = 2147483647; 559 | files = ( 560 | ); 561 | inputFileListPaths = ( 562 | ); 563 | inputPaths = ( 564 | ); 565 | name = "Timing END"; 566 | outputFileListPaths = ( 567 | ); 568 | outputPaths = ( 569 | ); 570 | runOnlyForDeploymentPostprocessing = 0; 571 | shellPath = /bin/sh; 572 | shellScript = " DATE=`date \"+%Y-%m-%dT%H:%M:%S.%s\"`\n echo \"{\\\"date\\\":\\\"$DATE\\\", \\\"taskName\\\":\\\"$TARGETNAME\\\", \\\"event\\\":\\\"end\\\"},\" >> ~/.timings.xcode\n"; 573 | }; 574 | FC95613824100A9900E47427 /* Carthage */ = { 575 | isa = PBXShellScriptBuildPhase; 576 | buildActionMask = 12; 577 | files = ( 578 | ); 579 | inputFileListPaths = ( 580 | "$(SRCROOT)/Carthage/Build/iOS/Realm.framework", 581 | "$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework", 582 | ); 583 | inputPaths = ( 584 | "$(SRCROOT)/Carthage/Build/iOS/Realm.framework", 585 | "$(SRCROOT)/Carthage/Build/iOS/RealmSwift.framework", 586 | ); 587 | name = Carthage; 588 | outputFileListPaths = ( 589 | ); 590 | outputPaths = ( 591 | ); 592 | runOnlyForDeploymentPostprocessing = 0; 593 | shellPath = /bin/sh; 594 | shellScript = "/usr/local/bin/carthage copy-frameworks\n"; 595 | }; 596 | /* End PBXShellScriptBuildPhase section */ 597 | 598 | /* Begin PBXSourcesBuildPhase section */ 599 | 65BBB9431F4B3425001A378C /* Sources */ = { 600 | isa = PBXSourcesBuildPhase; 601 | buildActionMask = 2147483647; 602 | files = ( 603 | 6530425E20C6C196002B373E /* Extension.swift in Sources */, 604 | 65BBB98F1F4C20EF001A378C /* GHUser.swift in Sources */, 605 | FC1520FA241B9C04000B6071 /* UsersCoordinator.swift in Sources */, 606 | 65197A501F5AA9DC007C8C64 /* StorageContext.swift in Sources */, 607 | 65197A721F5CB269007C8C64 /* UserDetailViewModel.swift in Sources */, 608 | 65574E4F1F4CB28B000D717E /* GHSearchResponse.swift in Sources */, 609 | 65BBB98B1F4C1BFE001A378C /* UserRequests.swift in Sources */, 610 | FC1520F5241B96FD000B6071 /* SceneDelegate.swift in Sources */, 611 | 65197A641F5CB0DD007C8C64 /* UserDetailOperation.swift in Sources */, 612 | 65197A621F5CAE49007C8C64 /* GHUserDetail.swift in Sources */, 613 | 65BBB98A1F4C1BFE001A378C /* Request.swift in Sources */, 614 | 65BBB9891F4C1BFE001A378C /* Operation.swift in Sources */, 615 | 65197A731F5CB269007C8C64 /* UsersViewController.swift in Sources */, 616 | FC1520F8241B981B000B6071 /* AppCoordinator.swift in Sources */, 617 | 65197A741F5CB269007C8C64 /* UsersViewModel.swift in Sources */, 618 | FC152100241BA0EA000B6071 /* Storyboards.swift in Sources */, 619 | 65BBB98C1F4C1BFE001A378C /* Response.swift in Sources */, 620 | 65197A531F5AAA52007C8C64 /* UsersRepo.swift in Sources */, 621 | 65BBB9881F4C1BFE001A378C /* UsersOperation.swift in Sources */, 622 | 65BBB9861F4C1BFE001A378C /* Environment.swift in Sources */, 623 | 65BBB94B1F4B3425001A378C /* AppDelegate.swift in Sources */, 624 | 65BBB9871F4C1BFE001A378C /* NetworkDispatcher.swift in Sources */, 625 | 65197A4E1F5AA976007C8C64 /* Storable.swift in Sources */, 626 | 65197A711F5CB269007C8C64 /* UserDetailViewController.swift in Sources */, 627 | 65BBB9851F4C1BFE001A378C /* Dispatcher.swift in Sources */, 628 | FC1520FC241B9C1A000B6071 /* UserDetailCoordinator.swift in Sources */, 629 | 65197A551F5AAD26007C8C64 /* RealmStorageContext.swift in Sources */, 630 | ); 631 | runOnlyForDeploymentPostprocessing = 0; 632 | }; 633 | 65BBB9571F4B3425001A378C /* Sources */ = { 634 | isa = PBXSourcesBuildPhase; 635 | buildActionMask = 2147483647; 636 | files = ( 637 | FC95614C24113B4200E47427 /* MockUsersRepo.swift in Sources */, 638 | FC95614824113AEB00E47427 /* MockSearchResponse.swift in Sources */, 639 | 65197A5C1F5AB4BB007C8C64 /* UsersRepoTests.swift in Sources */, 640 | FC9561412411106500E47427 /* UsersViewModelTests.swift in Sources */, 641 | 65197A661F5CB131007C8C64 /* UserDetailOperationTests.swift in Sources */, 642 | FC95614424113AA100E47427 /* MockGHUser.swift in Sources */, 643 | FC95614A24113B0E00E47427 /* MockUserOperation.swift in Sources */, 644 | 65BBB9601F4B3425001A378C /* UsersOperationTests.swift in Sources */, 645 | FC95614624113AB500E47427 /* MockUserDetailOperation.swift in Sources */, 646 | FC9561342410054700E47427 /* TestUtils.swift in Sources */, 647 | FC95613F2411105000E47427 /* UserDetailViewModelTests.swift in Sources */, 648 | ); 649 | runOnlyForDeploymentPostprocessing = 0; 650 | }; 651 | /* End PBXSourcesBuildPhase section */ 652 | 653 | /* Begin PBXTargetDependency section */ 654 | 65BBB95D1F4B3425001A378C /* PBXTargetDependency */ = { 655 | isa = PBXTargetDependency; 656 | target = 65BBB9461F4B3425001A378C /* TryNetworkLayer */; 657 | targetProxy = 65BBB95C1F4B3425001A378C /* PBXContainerItemProxy */; 658 | }; 659 | /* End PBXTargetDependency section */ 660 | 661 | /* Begin PBXVariantGroup section */ 662 | 65BBB94E1F4B3425001A378C /* Main.storyboard */ = { 663 | isa = PBXVariantGroup; 664 | children = ( 665 | 65BBB94F1F4B3425001A378C /* Base */, 666 | ); 667 | name = Main.storyboard; 668 | sourceTree = ""; 669 | }; 670 | 65BBB9531F4B3425001A378C /* LaunchScreen.storyboard */ = { 671 | isa = PBXVariantGroup; 672 | children = ( 673 | 65BBB9541F4B3425001A378C /* Base */, 674 | ); 675 | name = LaunchScreen.storyboard; 676 | sourceTree = ""; 677 | }; 678 | /* End PBXVariantGroup section */ 679 | 680 | /* Begin XCBuildConfiguration section */ 681 | 65BBB9621F4B3425001A378C /* Debug */ = { 682 | isa = XCBuildConfiguration; 683 | buildSettings = { 684 | ALWAYS_SEARCH_USER_PATHS = NO; 685 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 686 | CLANG_ANALYZER_NONNULL = YES; 687 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 688 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 689 | CLANG_CXX_LIBRARY = "libc++"; 690 | CLANG_ENABLE_MODULES = YES; 691 | CLANG_ENABLE_OBJC_ARC = YES; 692 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 693 | CLANG_WARN_BOOL_CONVERSION = YES; 694 | CLANG_WARN_COMMA = YES; 695 | CLANG_WARN_CONSTANT_CONVERSION = YES; 696 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 697 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 698 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 699 | CLANG_WARN_EMPTY_BODY = YES; 700 | CLANG_WARN_ENUM_CONVERSION = YES; 701 | CLANG_WARN_INFINITE_RECURSION = YES; 702 | CLANG_WARN_INT_CONVERSION = YES; 703 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 704 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 705 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 706 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 707 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 708 | CLANG_WARN_STRICT_PROTOTYPES = YES; 709 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 710 | CLANG_WARN_UNREACHABLE_CODE = YES; 711 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 712 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 713 | COPY_PHASE_STRIP = NO; 714 | DEBUG_INFORMATION_FORMAT = dwarf; 715 | ENABLE_STRICT_OBJC_MSGSEND = YES; 716 | ENABLE_TESTABILITY = YES; 717 | GCC_C_LANGUAGE_STANDARD = gnu99; 718 | GCC_DYNAMIC_NO_PIC = NO; 719 | GCC_NO_COMMON_BLOCKS = YES; 720 | GCC_OPTIMIZATION_LEVEL = 0; 721 | GCC_PREPROCESSOR_DEFINITIONS = ( 722 | "DEBUG=1", 723 | "$(inherited)", 724 | ); 725 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 726 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 727 | GCC_WARN_UNDECLARED_SELECTOR = YES; 728 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 729 | GCC_WARN_UNUSED_FUNCTION = YES; 730 | GCC_WARN_UNUSED_VARIABLE = YES; 731 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 732 | MTL_ENABLE_DEBUG_INFO = YES; 733 | ONLY_ACTIVE_ARCH = YES; 734 | SDKROOT = iphoneos; 735 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 736 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 737 | TARGETED_DEVICE_FAMILY = "1,2"; 738 | }; 739 | name = Debug; 740 | }; 741 | 65BBB9631F4B3425001A378C /* Release */ = { 742 | isa = XCBuildConfiguration; 743 | buildSettings = { 744 | ALWAYS_SEARCH_USER_PATHS = NO; 745 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 746 | CLANG_ANALYZER_NONNULL = YES; 747 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 748 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 749 | CLANG_CXX_LIBRARY = "libc++"; 750 | CLANG_ENABLE_MODULES = YES; 751 | CLANG_ENABLE_OBJC_ARC = YES; 752 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 753 | CLANG_WARN_BOOL_CONVERSION = YES; 754 | CLANG_WARN_COMMA = YES; 755 | CLANG_WARN_CONSTANT_CONVERSION = YES; 756 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 757 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 758 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 759 | CLANG_WARN_EMPTY_BODY = YES; 760 | CLANG_WARN_ENUM_CONVERSION = YES; 761 | CLANG_WARN_INFINITE_RECURSION = YES; 762 | CLANG_WARN_INT_CONVERSION = YES; 763 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 764 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 765 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 766 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 767 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 768 | CLANG_WARN_STRICT_PROTOTYPES = YES; 769 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 770 | CLANG_WARN_UNREACHABLE_CODE = YES; 771 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 772 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 773 | COPY_PHASE_STRIP = NO; 774 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 775 | ENABLE_NS_ASSERTIONS = NO; 776 | ENABLE_STRICT_OBJC_MSGSEND = YES; 777 | GCC_C_LANGUAGE_STANDARD = gnu99; 778 | GCC_NO_COMMON_BLOCKS = YES; 779 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 780 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 781 | GCC_WARN_UNDECLARED_SELECTOR = YES; 782 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 783 | GCC_WARN_UNUSED_FUNCTION = YES; 784 | GCC_WARN_UNUSED_VARIABLE = YES; 785 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 786 | MTL_ENABLE_DEBUG_INFO = NO; 787 | SDKROOT = iphoneos; 788 | SWIFT_COMPILATION_MODE = wholemodule; 789 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 790 | TARGETED_DEVICE_FAMILY = "1,2"; 791 | VALIDATE_PRODUCT = YES; 792 | }; 793 | name = Release; 794 | }; 795 | 65BBB9651F4B3425001A378C /* Debug */ = { 796 | isa = XCBuildConfiguration; 797 | buildSettings = { 798 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 799 | DEVELOPMENT_TEAM = A2VGE7F94S; 800 | FRAMEWORK_SEARCH_PATHS = ( 801 | "$(inherited)", 802 | "$(PROJECT_DIR)/Carthage/Build/iOS", 803 | ); 804 | INFOPLIST_FILE = TryNetworkLayer/Info.plist; 805 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 806 | LD_RUNPATH_SEARCH_PATHS = ( 807 | "$(inherited)", 808 | "@executable_path/Frameworks", 809 | ); 810 | PRODUCT_BUNDLE_IDENTIFIER = com.as.TryNetworkLayer; 811 | PRODUCT_NAME = "$(TARGET_NAME)"; 812 | SWIFT_VERSION = 5.0; 813 | }; 814 | name = Debug; 815 | }; 816 | 65BBB9661F4B3425001A378C /* Release */ = { 817 | isa = XCBuildConfiguration; 818 | buildSettings = { 819 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 820 | DEVELOPMENT_TEAM = A2VGE7F94S; 821 | FRAMEWORK_SEARCH_PATHS = ( 822 | "$(inherited)", 823 | "$(PROJECT_DIR)/Carthage/Build/iOS", 824 | ); 825 | INFOPLIST_FILE = TryNetworkLayer/Info.plist; 826 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 827 | LD_RUNPATH_SEARCH_PATHS = ( 828 | "$(inherited)", 829 | "@executable_path/Frameworks", 830 | ); 831 | PRODUCT_BUNDLE_IDENTIFIER = com.as.TryNetworkLayer; 832 | PRODUCT_NAME = "$(TARGET_NAME)"; 833 | SWIFT_VERSION = 5.0; 834 | }; 835 | name = Release; 836 | }; 837 | 65BBB9681F4B3425001A378C /* Debug */ = { 838 | isa = XCBuildConfiguration; 839 | buildSettings = { 840 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 841 | BUNDLE_LOADER = "$(TEST_HOST)"; 842 | DEVELOPMENT_TEAM = A2VGE7F94S; 843 | INFOPLIST_FILE = TryNetworkLayerTests/Info.plist; 844 | LD_RUNPATH_SEARCH_PATHS = ( 845 | "$(inherited)", 846 | "@executable_path/Frameworks", 847 | "@loader_path/Frameworks", 848 | ); 849 | PRODUCT_BUNDLE_IDENTIFIER = com.as.TryNetworkLayerTests; 850 | PRODUCT_NAME = "$(TARGET_NAME)"; 851 | SWIFT_VERSION = 5.0; 852 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TryNetworkLayer.app/TryNetworkLayer"; 853 | }; 854 | name = Debug; 855 | }; 856 | 65BBB9691F4B3425001A378C /* Release */ = { 857 | isa = XCBuildConfiguration; 858 | buildSettings = { 859 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 860 | BUNDLE_LOADER = "$(TEST_HOST)"; 861 | DEVELOPMENT_TEAM = A2VGE7F94S; 862 | INFOPLIST_FILE = TryNetworkLayerTests/Info.plist; 863 | LD_RUNPATH_SEARCH_PATHS = ( 864 | "$(inherited)", 865 | "@executable_path/Frameworks", 866 | "@loader_path/Frameworks", 867 | ); 868 | PRODUCT_BUNDLE_IDENTIFIER = com.as.TryNetworkLayerTests; 869 | PRODUCT_NAME = "$(TARGET_NAME)"; 870 | SWIFT_VERSION = 5.0; 871 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/TryNetworkLayer.app/TryNetworkLayer"; 872 | }; 873 | name = Release; 874 | }; 875 | /* End XCBuildConfiguration section */ 876 | 877 | /* Begin XCConfigurationList section */ 878 | 65BBB9421F4B3425001A378C /* Build configuration list for PBXProject "TryNetworkLayer" */ = { 879 | isa = XCConfigurationList; 880 | buildConfigurations = ( 881 | 65BBB9621F4B3425001A378C /* Debug */, 882 | 65BBB9631F4B3425001A378C /* Release */, 883 | ); 884 | defaultConfigurationIsVisible = 0; 885 | defaultConfigurationName = Release; 886 | }; 887 | 65BBB9641F4B3425001A378C /* Build configuration list for PBXNativeTarget "TryNetworkLayer" */ = { 888 | isa = XCConfigurationList; 889 | buildConfigurations = ( 890 | 65BBB9651F4B3425001A378C /* Debug */, 891 | 65BBB9661F4B3425001A378C /* Release */, 892 | ); 893 | defaultConfigurationIsVisible = 0; 894 | defaultConfigurationName = Release; 895 | }; 896 | 65BBB9671F4B3425001A378C /* Build configuration list for PBXNativeTarget "TryNetworkLayerTests" */ = { 897 | isa = XCConfigurationList; 898 | buildConfigurations = ( 899 | 65BBB9681F4B3425001A378C /* Debug */, 900 | 65BBB9691F4B3425001A378C /* Release */, 901 | ); 902 | defaultConfigurationIsVisible = 0; 903 | defaultConfigurationName = Release; 904 | }; 905 | /* End XCConfigurationList section */ 906 | 907 | /* Begin XCRemoteSwiftPackageReference section */ 908 | FC9561352410086300E47427 /* XCRemoteSwiftPackageReference "Alamofire" */ = { 909 | isa = XCRemoteSwiftPackageReference; 910 | repositoryURL = "https://github.com/Alamofire/Alamofire"; 911 | requirement = { 912 | kind = upToNextMinorVersion; 913 | minimumVersion = 5.0.2; 914 | }; 915 | }; 916 | /* End XCRemoteSwiftPackageReference section */ 917 | 918 | /* Begin XCSwiftPackageProductDependency section */ 919 | FC9561362410086300E47427 /* Alamofire */ = { 920 | isa = XCSwiftPackageProductDependency; 921 | package = FC9561352410086300E47427 /* XCRemoteSwiftPackageReference "Alamofire" */; 922 | productName = Alamofire; 923 | }; 924 | /* End XCSwiftPackageProductDependency section */ 925 | }; 926 | rootObject = 65BBB93F1F4B3425001A378C /* Project object */; 927 | } 928 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcodeproj/xcshareddata/xcschemes/TryNetworkLayer.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 71 | 73 | 79 | 80 | 81 | 82 | 84 | 85 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /TryNetworkLayer.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Alamofire", 6 | "repositoryURL": "https://github.com/Alamofire/Alamofire", 7 | "state": { 8 | "branch": null, 9 | "revision": "b02c4ee7f1659090f7ac543c022f922beeb04bc6", 10 | "version": "5.0.2" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /TryNetworkLayer/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | true 16 | } 17 | 18 | // MARK: UISceneSession Lifecycle 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, 21 | options: UIScene.ConnectionOptions) -> UISceneConfiguration { 22 | // Called when a new scene session is being created. 23 | // Use this method to select a configuration to create the new scene with. 24 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 25 | } 26 | 27 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 28 | // Called when the user discards a scene session. 29 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 30 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /TryNetworkLayer/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /TryNetworkLayer/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/UserDetail/UserDetailCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailCoordinator.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 13/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class UserDetailCoordinator: NavigationCoordinator { 12 | 13 | let navigationController: UINavigationController 14 | private let user: GHUser 15 | 16 | init(navigationController: UINavigationController, user: GHUser) { 17 | self.navigationController = navigationController 18 | self.user = user 19 | } 20 | 21 | func start() { 22 | let userDetailViewController = Storyboard.main.instantiate(UserDetailViewController.self) 23 | userDetailViewController.viewModel = UserDetailViewModel(userLogin: self.user.login) 24 | self.navigationController.pushViewController(userDetailViewController, animated: true) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/UserDetail/UserDetailViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailViewController.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 03/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | final class UserDetailViewController: UIViewController { 13 | 14 | @IBOutlet weak private var usernameLabel: UILabel! 15 | 16 | var viewModel: UserDetailViewModel! 17 | private var bindings = Set() 18 | 19 | override func viewDidLoad() { 20 | super.viewDidLoad() 21 | 22 | self.setupObservers() 23 | self.viewModel.fecthUser() 24 | } 25 | 26 | func setupObservers() { 27 | let usernameHandler: (String) -> Void = { [weak self] username in 28 | self?.usernameLabel.text = username 29 | } 30 | 31 | viewModel.$username 32 | .receive(on: RunLoop.main) 33 | .sink(receiveValue: usernameHandler) 34 | .store(in: &bindings) 35 | 36 | // not working 37 | // viewModel.$username.assign(to: \.text, on: usernameLabel) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/UserDetail/UserDetailViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailViewModel.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 03/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | final class UserDetailViewModel { 12 | 13 | // MARK: Observables 14 | 15 | @Published var username: String = "" 16 | 17 | // MARK: Properties 18 | 19 | private(set) var user: GHUserDetail? 20 | let usersRepo: UsersRepoProtocol 21 | 22 | // MARK: Methods 23 | 24 | init(usersRepo: UsersRepoProtocol = UsersRepo(), userLogin: String) { 25 | self.usersRepo = usersRepo 26 | self.username = userLogin 27 | } 28 | 29 | func fecthUser() { 30 | usersRepo.fetchDetail(username: username) { [weak self] (user) in 31 | if let user = user { 32 | self?.user = user 33 | self?.username = user.login! 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/Users/UsersCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersCoordinator.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 13/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class UsersCoordinator: Coordinator { 12 | 13 | var window: UIWindow 14 | var navigationController: UINavigationController! 15 | 16 | var userDetailCoordinator: UserDetailCoordinator? 17 | 18 | init(window: UIWindow) { 19 | self.window = window 20 | } 21 | 22 | func start() { 23 | let usersViewController = Storyboard.main.instantiate(UsersViewController.self) 24 | usersViewController.viewModel = UsersViewModel() 25 | usersViewController.viewModel.coordinatorDelegate = self 26 | 27 | self.navigationController = UINavigationController(rootViewController: usersViewController) 28 | self.window.rootViewController = navigationController 29 | } 30 | } 31 | 32 | extension UsersCoordinator: UsersViewModelCoordinatorDelegate { 33 | func usersViewModelPresent(user: GHUser) { 34 | userDetailCoordinator = UserDetailCoordinator(navigationController: navigationController, user: user) 35 | userDetailCoordinator?.start() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/Users/UsersViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Combine 10 | import UIKit 11 | 12 | final class UsersViewController: UITableViewController { 13 | 14 | // MARK: Properties 15 | 16 | let searchController = UISearchController(searchResultsController: nil) 17 | 18 | var viewModel: UsersViewModel! 19 | private var bindings = Set() 20 | 21 | // MARK: View Management 22 | 23 | override func viewDidLoad() { 24 | super.viewDidLoad() 25 | 26 | setupObservers() 27 | setupSearchController() 28 | viewModel.fecthUsers() 29 | } 30 | 31 | func setupObservers() { 32 | 33 | let updateTableHandler: (Bool) -> Void = { [weak self] state in 34 | self?.tableView.reloadData() 35 | } 36 | 37 | viewModel.$updateView 38 | .receive(on: RunLoop.main) 39 | .sink(receiveValue: updateTableHandler) 40 | .store(in: &bindings) 41 | } 42 | 43 | func setupSearchController() { 44 | searchController.searchBar.delegate = self 45 | searchController.searchResultsUpdater = self 46 | definesPresentationContext = true 47 | tableView.tableHeaderView = searchController.searchBar 48 | 49 | navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .trash, target: self, action: #selector(deleleAllUsers)) 50 | } 51 | 52 | @objc func deleleAllUsers() { 53 | viewModel.deleleAllUsers() 54 | } 55 | 56 | // MARK: Table view data source 57 | 58 | override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 59 | viewModel.numberOfUsers() 60 | } 61 | 62 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 63 | let cell = tableView.dequeueReusableCell(withIdentifier: "user_cell", for: indexPath) 64 | 65 | let user = viewModel.userAt(indexPath: indexPath) 66 | cell.textLabel?.text = user.login 67 | cell.detailTextLabel?.text = user.type 68 | 69 | return cell 70 | } 71 | 72 | override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 73 | let user = viewModel.userAt(indexPath: indexPath) 74 | self.viewModel.presentUserDetail(user: user) 75 | } 76 | } 77 | 78 | extension UsersViewController: UISearchBarDelegate { 79 | 80 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 81 | guard let filter = searchBar.text?.lowercased() else { return } 82 | viewModel.searchUsers(query: filter) 83 | } 84 | 85 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 86 | viewModel.fecthUsers() 87 | } 88 | } 89 | 90 | extension UsersViewController: UISearchResultsUpdating { 91 | 92 | func updateSearchResults(for searchController: UISearchController) { 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /TryNetworkLayer/Controllers/Users/UsersViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersViewModel.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 03/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UsersViewModelCoordinatorDelegate: class { 12 | func usersViewModelPresent(user: GHUser) 13 | } 14 | 15 | final class UsersViewModel: ObservableObject { 16 | 17 | // MARK: Observables 18 | 19 | @Published var updateView: Bool = false 20 | 21 | // MARK: Properties 22 | 23 | weak var coordinatorDelegate: UsersViewModelCoordinatorDelegate? 24 | 25 | let usersRepo: UsersRepoProtocol 26 | private(set) var users: [GHUser] = [] 27 | 28 | // MARK: Methods 29 | 30 | init(usersRepo: UsersRepoProtocol = UsersRepo()) { 31 | self.usersRepo = usersRepo 32 | } 33 | 34 | func fecthUsers() { 35 | usersRepo.fetch(fromStorage: true, query: "language:swift") { [weak self] (users) in 36 | self?.users = users 37 | self?.updateView = true 38 | } 39 | } 40 | 41 | func searchUsers(query: String) { 42 | usersRepo.fetch(fromStorage: false, query: query) { [weak self] (users) in 43 | self?.users = users 44 | self?.updateView = true 45 | } 46 | } 47 | 48 | func deleleAllUsers() { 49 | self.users.removeAll() 50 | usersRepo.deleteAll() 51 | self.updateView = true 52 | } 53 | 54 | // MARK: Table view getter 55 | 56 | func numberOfUsers() -> Int { 57 | users.count 58 | } 59 | 60 | func userAt(indexPath: IndexPath) -> GHUser { 61 | users[indexPath.row] 62 | } 63 | 64 | // MARK: Coordinator 65 | 66 | func presentUserDetail(user: GHUser) { 67 | coordinatorDelegate?.usersViewModelPresent(user: user) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /TryNetworkLayer/Coordinator/AppCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppCoordinator.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 13/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | protocol Coordinator { 12 | func start() 13 | } 14 | 15 | /// A coordinator based on a navigation controller 16 | protocol NavigationCoordinator: Coordinator { 17 | var navigationController: UINavigationController { get } 18 | } 19 | 20 | private enum ChildCoordinator { 21 | case root 22 | } 23 | 24 | final class AppCoordinator: Coordinator { 25 | 26 | // MARK: - Properties 27 | 28 | private unowned var sceneDelegate: SceneDelegate 29 | private weak var window: UIWindow! 30 | 31 | // Keep reference of the current coordinators 32 | private var coordinators = [ChildCoordinator: Coordinator]() 33 | 34 | // MARK: - Initializer 35 | 36 | init(sceneDelegate: SceneDelegate) { 37 | self.sceneDelegate = sceneDelegate 38 | self.window = sceneDelegate.window! 39 | } 40 | 41 | // MARK: - Coordinator 42 | 43 | func start() { 44 | let usersCoordinator = UsersCoordinator(window: window) 45 | usersCoordinator.start() 46 | self.coordinators[.root] = usersCoordinator 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /TryNetworkLayer/Extension.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extension.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 05/06/2018. 6 | // Copyright © 2018 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Convert json dictionary, data to Codable objects 12 | public extension JSONDecoder { 13 | 14 | /// Convert json dictionary to Codable object 15 | /// 16 | /// - Parameters: 17 | /// - json: Json dictionary. 18 | /// - type: Type information. 19 | /// - Returns: Codable object 20 | /// - Throws: Error if failed 21 | static func decode(_ json: [String: Any], to type: T.Type) throws -> T { 22 | let data = try JSONSerialization.data(withJSONObject: json, options: []) 23 | return try decode(data, to: type) 24 | } 25 | 26 | /// Convert json data to Codable object 27 | /// 28 | /// - Parameters: 29 | /// - json: Json dictionary. 30 | /// - type: Type information. 31 | /// - Returns: Codable object 32 | /// - Throws: Error if failed 33 | static func decode(_ data: Data, to type: T.Type) throws -> T { 34 | try JSONDecoder().decode(T.self, from: data) 35 | } 36 | } 37 | 38 | public extension UIViewController { 39 | 40 | /// The storyboard identifier for the controller 41 | static var storyboardIdentifier: String { 42 | self.description().components(separatedBy: ".").dropFirst().joined(separator: ".") 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /TryNetworkLayer/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSAppTransportSecurity 24 | 25 | NSExceptionDomains 26 | 27 | localhost 28 | 29 | NSTemporaryExceptionAllowsInsecureHTTPLoads 30 | 31 | 32 | 33 | 34 | UIApplicationSceneManifest 35 | 36 | UIApplicationSupportsMultipleScenes 37 | 38 | UISceneConfigurations 39 | 40 | UIWindowSceneSessionRoleApplication 41 | 42 | 43 | UISceneConfigurationName 44 | Default Configuration 45 | UISceneDelegateClassName 46 | $(PRODUCT_MODULE_NAME).SceneDelegate 47 | UISceneStoryboardFile 48 | Main 49 | 50 | 51 | 52 | 53 | UILaunchStoryboardName 54 | LaunchScreen 55 | UIRequiredDeviceCapabilities 56 | 57 | armv7 58 | 59 | UISupportedInterfaceOrientations 60 | 61 | UIInterfaceOrientationPortrait 62 | UIInterfaceOrientationLandscapeLeft 63 | UIInterfaceOrientationLandscapeRight 64 | 65 | UISupportedInterfaceOrientations~ipad 66 | 67 | UIInterfaceOrientationPortrait 68 | UIInterfaceOrientationPortraitUpsideDown 69 | UIInterfaceOrientationLandscapeLeft 70 | UIInterfaceOrientationLandscapeRight 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /TryNetworkLayer/Models/GHSearchResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GHSearchResponse.swift 3 | // 4 | // Created by Andrea Stevanato on 22/08/2017 5 | // Copyright (c) . All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class GHSearchResponse: Codable { 11 | 12 | enum CodingKeys: String, CodingKey { 13 | case incompleteResults = "incomplete_results" 14 | case totalCount = "total_count" 15 | case items = "items" 16 | } 17 | 18 | // MARK: Properties 19 | 20 | public var incompleteResults: Bool = false 21 | public var totalCount: Int = 0 22 | public var items: [GHUser] = [] 23 | } 24 | -------------------------------------------------------------------------------- /TryNetworkLayer/Models/GHUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GHItems.swift 3 | // 4 | // Created by Andrea Stevanato on 22/08/2017 5 | // Copyright (c) . All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Realm 10 | import RealmSwift 11 | 12 | public final class GHUser: Object, Codable { 13 | 14 | // MARK: Declaration for string constants to be used to decode and also serialize. 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case organizationsUrl = "organizations_url" 18 | case score = "score" 19 | case reposUrl = "repos_url" 20 | case htmlUrl = "html_url" 21 | case gravatarId = "gravatar_id" 22 | case avatarUrl = "avatar_url" 23 | case type = "type" 24 | case login = "login" 25 | case followersUrl = "followers_url" 26 | case id = "id" 27 | case subscriptionsUrl = "subscriptions_url" 28 | case receivedEventsUrl = "received_events_url" 29 | case url = "url" 30 | } 31 | 32 | // MARK: Properties 33 | 34 | @objc dynamic public var organizationsUrl: String? 35 | @objc dynamic public var score: Float = 0 36 | @objc dynamic public var reposUrl: String? 37 | @objc dynamic public var htmlUrl: String? 38 | @objc dynamic public var gravatarId: String? 39 | @objc dynamic public var avatarUrl: String? 40 | @objc dynamic public var type: String? 41 | @objc dynamic public var login: String = "" 42 | @objc dynamic public var followersUrl: String? 43 | @objc dynamic public var id: Int = 0 44 | @objc dynamic public var subscriptionsUrl: String? 45 | @objc dynamic public var receivedEventsUrl: String? 46 | @objc dynamic public var url: String? 47 | 48 | // MARK: Primary Key 49 | 50 | override public static func primaryKey() -> String? { 51 | "id" 52 | } 53 | 54 | // MARK: Init 55 | 56 | required public init() { 57 | super.init() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /TryNetworkLayer/Models/GHUserDetail.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GHUserDetail.swift 3 | // 4 | // Created by Andrea Stevanato on 03/09/2017 5 | // Copyright (c) . All rights reserved. 6 | // 7 | 8 | import Foundation 9 | import Realm 10 | import RealmSwift 11 | 12 | public final class GHUserDetail: Object, Codable { 13 | 14 | // MARK: Declaration for string constants to be used to decode and also serialize. 15 | 16 | enum CodingKeys: String, CodingKey { 17 | case publicRepos = "public_repos" 18 | case organizationsUrl = "organizations_url" 19 | case reposUrl = "repos_url" 20 | case starredUrl = "starred_url" 21 | case type = "type" 22 | case bio = "bio" 23 | case gistsUrl = "gists_url" 24 | case followersUrl = "followers_url" 25 | case id = "id" 26 | case blog = "blog" 27 | case followers = "followers" 28 | case following = "following" 29 | case company = "company" 30 | case url = "url" 31 | case name = "name" 32 | case updatedAt = "updated_at" 33 | case publicGists = "public_gists" 34 | case siteAdmin = "site_admin" 35 | case gravatarId = "gravatar_id" 36 | case htmlUrl = "html_url" 37 | case avatarUrl = "avatar_url" 38 | case login = "login" 39 | case location = "location" 40 | case createdAt = "created_at" 41 | case subscriptionsUrl = "subscriptions_url" 42 | case followingUrl = "following_url" 43 | case receivedEventsUrl = "received_events_url" 44 | case eventsUrl = "events_url" 45 | } 46 | 47 | // MARK: Properties 48 | 49 | @objc dynamic public var publicRepos: Int = 0 50 | @objc dynamic public var organizationsUrl: String? 51 | @objc dynamic public var reposUrl: String? 52 | @objc dynamic public var starredUrl: String? 53 | @objc dynamic public var type: String? 54 | @objc dynamic public var bio: String? 55 | @objc dynamic public var gistsUrl: String? 56 | @objc dynamic public var followersUrl: String? 57 | @objc dynamic public var id: Int = 0 58 | @objc dynamic public var blog: String? 59 | @objc dynamic public var followers: Int = 0 60 | @objc dynamic public var following: Int = 0 61 | @objc dynamic public var company: String? 62 | @objc dynamic public var url: String? 63 | @objc dynamic public var name: String? 64 | @objc dynamic public var updatedAt: String? 65 | @objc dynamic public var publicGists: Int = 0 66 | @objc dynamic public var siteAdmin: Bool = false 67 | @objc dynamic public var gravatarId: String? 68 | @objc dynamic public var htmlUrl: String? 69 | @objc dynamic public var avatarUrl: String? 70 | @objc dynamic public var login: String? 71 | @objc dynamic public var location: String? 72 | @objc dynamic public var createdAt: String? 73 | @objc dynamic public var subscriptionsUrl: String? 74 | @objc dynamic public var followingUrl: String? 75 | @objc dynamic public var receivedEventsUrl: String? 76 | @objc dynamic public var eventsUrl: String? 77 | 78 | // MARK: Primary Key 79 | 80 | override public static func primaryKey() -> String? { 81 | "id" 82 | } 83 | 84 | // MARK: Init 85 | 86 | required public init() { 87 | super.init() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Dispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dispatcher.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Alamofire 10 | import Foundation 11 | 12 | /// The dispatcher is responsible to execute a Request by calling the underlyning layer (Alamofire or just a fake dispatcher which return mocked results). 13 | /// As output for a Request it should provide a Response. 14 | protocol Dispatcher { 15 | 16 | /// Configure the dispatcher with an environment 17 | /// 18 | /// - Parameter environment: environment configuration 19 | init(environment: Environment) 20 | 21 | /// This function execute the request and provide a completion handler with the response 22 | /// 23 | /// - Parameters: 24 | /// - request: request to execute 25 | /// - completion: completion handler for the request 26 | /// - Throws: error 27 | func execute(request: Request, completion: @escaping (_ response: Response) -> Void) throws 28 | } 29 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Environment.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Environment is a struct which encapsulate all the informations we need to perform a setup of our Networking Layer. 12 | public struct Environment { 13 | 14 | /// Name of the environment 15 | public var name: String 16 | 17 | /// Base URL of the environment 18 | public var host: String 19 | 20 | /// This is the list of common headers which will be part of each Request 21 | /// Headers may be overwritten by specific Request's implementation 22 | public var headers: [String: Any] = ["Content-Type": "application/json"] 23 | 24 | /// Cache policy 25 | public var cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalAndRemoteCacheData 26 | 27 | /// Initialize a new Environment 28 | /// 29 | /// - Parameters: 30 | /// - name: name of the environment 31 | /// - host: base url 32 | public init(_ name: String, host: String) { 33 | self.name = name 34 | self.host = host 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/NetworkDispatcher.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkDispatcher.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Alamofire 10 | import Foundation 11 | 12 | public enum NetworkError: Error { 13 | case badInput 14 | case noData 15 | case forbidden 16 | case notAuthorized 17 | } 18 | 19 | extension NetworkError: LocalizedError { 20 | public var errorDescription: String? { 21 | switch self { 22 | case .badInput: 23 | return NSLocalizedString("Bad input", comment: "") 24 | case .noData: 25 | return NSLocalizedString("No data", comment: "") 26 | case .forbidden: 27 | return NSLocalizedString("Forbiddden", comment: "") 28 | case .notAuthorized: 29 | return NSLocalizedString("Not authorized", comment: "") 30 | } 31 | } 32 | } 33 | 34 | public class NetworkDispatcher: Dispatcher { 35 | 36 | private var environment: Environment 37 | private var sessionManager: Session 38 | 39 | required public init(environment: Environment) { 40 | self.environment = environment 41 | 42 | let configuration = URLSessionConfiguration.default 43 | 44 | // Set timeout interval. 45 | configuration.timeoutIntervalForRequest = 30.0 46 | configuration.timeoutIntervalForResource = 30.0 47 | 48 | // Set cookie policies. 49 | configuration.httpCookieAcceptPolicy = HTTPCookie.AcceptPolicy.always 50 | configuration.httpCookieStorage = HTTPCookieStorage.shared 51 | configuration.httpShouldSetCookies = false 52 | 53 | self.sessionManager = Alamofire.Session(configuration: configuration) 54 | } 55 | 56 | public func execute(request: Request, completion: @escaping (_ response: Response) -> Void) throws { 57 | let req = try self.prepareURLRequest(for: request) 58 | self.sessionManager.request(req) 59 | .validate() 60 | .responseJSON { response in 61 | completion(Response(response, for: request)) 62 | } 63 | } 64 | 65 | private func prepareURLRequest(for request: Request) throws -> URLRequest { 66 | // Compose the url 67 | let fullUrl = "\(environment.host)/\(request.path)" 68 | var urlRequest = URLRequest(url: URL(string: fullUrl)!) 69 | 70 | // Working with parameters 71 | if let parameters = request.parameters { 72 | switch parameters { 73 | case .body(let params): 74 | // Parameters are part of the body 75 | if let params = params as? [String: String] { 76 | urlRequest.httpBody = try JSONSerialization.data(withJSONObject: params, options: .init(rawValue: 0)) 77 | } else { 78 | throw NetworkError.badInput 79 | } 80 | case .url(let params): 81 | // Parameters are part of the url 82 | let queryParams = self.getQueryParams(params: params) 83 | guard var components = URLComponents(string: fullUrl) else { 84 | throw NetworkError.badInput 85 | } 86 | components.queryItems = queryParams 87 | urlRequest.url = components.url 88 | } 89 | } 90 | 91 | // Add headers from environment and request 92 | environment.headers.forEach { urlRequest.addValue($0.value as! String, forHTTPHeaderField: $0.key) } 93 | request.headers?.forEach { urlRequest.addValue($0.value as! String, forHTTPHeaderField: $0.key) } 94 | 95 | // Setup HTTP method 96 | urlRequest.httpMethod = request.method.rawValue 97 | 98 | return urlRequest 99 | } 100 | 101 | private func getQueryParams(params: [String: Any?]) -> [URLQueryItem] { 102 | let paramsFiltered = params.filter({ (arg) -> Bool in 103 | let (_, value) = arg 104 | return value != nil ? true : false 105 | }) 106 | var queryItems: [URLQueryItem] = [] 107 | paramsFiltered.forEach({ (key: String, value: Any?) in 108 | if let array = value as? [String] { 109 | for paramValue in array { 110 | queryItems.append(URLQueryItem(name: key, value: paramValue)) 111 | } 112 | } else if let value = value as? String { 113 | queryItems.append(URLQueryItem(name: key, value: value)) 114 | } 115 | }) 116 | return queryItems 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Operations/Operation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Operation.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | enum OperationErrors: Error { 12 | case badMapping 13 | } 14 | 15 | protocol Operation { 16 | 17 | associatedtype D = Dispatcher // Default value 18 | associatedtype R 19 | 20 | /// Request to execute 21 | var request: Request { get } 22 | 23 | /// Execute request in passed dispatcher 24 | /// 25 | /// - Parameter dispatcher: dispatcher 26 | func execute(in dispatcher: D, completion: @escaping (Result) -> Void) 27 | } 28 | 29 | extension Operation { 30 | 31 | /// Execute a request that has response of generic type `T` 32 | /// 33 | /// - Parameters: 34 | /// - dispatcher: The dispatcher 35 | /// - completion: The completion handler 36 | func executeBaseResponse(dispatcher: Dispatcher, completion: @escaping (Result) -> Void) { 37 | do { 38 | try dispatcher.execute(request: self.request, completion: { (response: Response) in 39 | 40 | if let json = response.json { 41 | let decoder = JSONDecoder() 42 | do { 43 | let response = try decoder.decode(T.self, from: json) 44 | completion(.success(response)) 45 | } catch let error { 46 | completion(.failure(error)) 47 | } 48 | } else if let error = response.error { 49 | completion(.failure(error)) 50 | } 51 | }) 52 | } catch let error { 53 | completion(.failure(error)) 54 | } 55 | } 56 | 57 | /// Execute a request that has response of type `BaseDataArrayResponse` 58 | /// 59 | /// - Parameters: 60 | /// - dispatcher: The dispatcher 61 | /// - completion: The completion handler 62 | func executeBaseArrayResponse(dispatcher: Dispatcher, completion: @escaping (Result<[T], Error>) -> Void) { 63 | do { 64 | try dispatcher.execute(request: self.request, completion: { (response: Response) in 65 | 66 | if let json = response.json { 67 | let decoder = JSONDecoder() 68 | do { 69 | let response = try decoder.decode([T].self, from: json) 70 | completion(.success(response)) 71 | } catch let error { 72 | completion(.failure(error)) 73 | } 74 | } else if let error = response.error { 75 | completion(.failure(error)) 76 | } 77 | }) 78 | } catch let error { 79 | completion(.failure(error)) 80 | } 81 | } 82 | } 83 | 84 | private extension JSONDecoder { 85 | 86 | func decode(_ type: T.Type, from json: Response.JSON) throws -> T where T: Decodable { 87 | let data = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) 88 | return try self.decode(type, from: data) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Operations/UserDetailOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailOperation.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 03/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UserDetailOperationType { 12 | var username: String { get set } 13 | var request: Request { get } 14 | 15 | init(username: String) 16 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) 17 | } 18 | 19 | final class UserDetailOperation: Operation, UserDetailOperationType { 20 | 21 | typealias D = Dispatcher 22 | typealias R = GHUserDetail 23 | 24 | // MARK: Request parameters 25 | 26 | var username: String 27 | 28 | init(username: String) { 29 | self.username = username 30 | } 31 | 32 | var request: Request { 33 | UserRequests.detail(username: username) 34 | } 35 | 36 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) { 37 | self.executeBaseResponse(dispatcher: dispatcher, completion: completion) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Operations/UsersOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersOperation.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UsersOperationType { 12 | var request: Request { get } 13 | var query: String { get set } 14 | 15 | init(query: String) 16 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) 17 | } 18 | 19 | final class UsersOperation: Operation, UsersOperationType { 20 | 21 | typealias D = Dispatcher 22 | typealias R = GHSearchResponse 23 | 24 | // MARK: Request parameters 25 | 26 | var query: String 27 | 28 | init(query: String) { 29 | self.query = query 30 | } 31 | 32 | var request: Request { 33 | UserRequests.searchUsers(query: self.query, perPage: 50, page: 1) 34 | } 35 | 36 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) { 37 | self.executeBaseResponse(dispatcher: dispatcher, completion: completion) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Requests/Request.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Define the type of data we expect as response 12 | /// 13 | /// - JSON: json data 14 | /// - data: plain data 15 | public enum DataType { 16 | case JSON 17 | case data 18 | } 19 | 20 | /// This is the Request protocol you may implement as classic class object for each kind of request. 21 | public protocol Request { 22 | 23 | /// Relative path of the endpoint we want to call (ie. `/users/login`) 24 | var path: String { get } 25 | 26 | /// This define the HTTP method we should use to perform the call 27 | /// We have defined it inside an String based enum called `HTTPMethod` 28 | /// just for clarity 29 | var method: HTTPMethod { get } 30 | 31 | /// These are the parameters we need to send along with the call. 32 | /// Params can be passed into the body or along with the URL 33 | var parameters: RequestParameters? { get } 34 | 35 | /// You may also define a list of headers to pass along with each request. 36 | var headers: [String: Any]? { get } 37 | 38 | /// What kind of data we expect as response 39 | var dataType: DataType { get } 40 | } 41 | 42 | /// This define the type of HTTP method used to perform the request 43 | /// 44 | /// - post: POST method 45 | /// - put: PUT method 46 | /// - get: GET method 47 | /// - delete: DELETE method 48 | /// - patch: PATCH method 49 | public enum HTTPMethod: String { 50 | case post = "POST" 51 | case put = "PUT" 52 | case get = "GET" 53 | case delete = "DELETE" 54 | case patch = "PATCH" 55 | } 56 | 57 | /// Define parameters to pass along with the request and how 58 | /// they are encapsulated into the http request itself. 59 | /// 60 | /// - body: part of the body stream 61 | /// - url: as url parameters 62 | public enum RequestParameters { 63 | case body(_: [String: Any]) 64 | case url(_: [String: String?]) 65 | } 66 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Requests/UserRequests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserRequests.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum UserRequests: Request { 12 | 13 | case detail(username: String) 14 | case searchUsers(query: String, perPage: Int, page: Int) 15 | 16 | public var path: String { 17 | switch self { 18 | case .detail(let username): 19 | return "users/\(username)" 20 | case .searchUsers: 21 | return "search/users" 22 | } 23 | } 24 | 25 | public var method: HTTPMethod { 26 | switch self { 27 | case .detail: 28 | return .get 29 | case .searchUsers: 30 | return .get 31 | } 32 | } 33 | 34 | public var parameters: RequestParameters? { 35 | switch self { 36 | case .detail: 37 | return .url([:]) 38 | case .searchUsers(let query, let perPage, let page): 39 | return .url(["q": query, 40 | "per_page": String(perPage), 41 | "page": String(page)]) 42 | } 43 | } 44 | 45 | public var headers: [String: Any]? { 46 | switch self { 47 | default: 48 | return nil 49 | } 50 | } 51 | 52 | public var dataType: DataType { 53 | switch self { 54 | case .detail: 55 | return .JSON 56 | case .searchUsers: 57 | return .JSON 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /TryNetworkLayer/Network/Response.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Response.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Alamofire 10 | import Foundation 11 | 12 | public class Response { 13 | 14 | public typealias JSON = [String: Any] 15 | 16 | var json: JSON? 17 | var data: Data? 18 | var error: Error? 19 | 20 | init(_ dataResponse: AFDataResponse, for request: Request) { 21 | 22 | // response validation 23 | guard let statusCode = dataResponse.response?.statusCode else { 24 | self.error = dataResponse.error 25 | return 26 | } 27 | if statusCode == 401, dataResponse.error == nil { 28 | self.error = NetworkError.notAuthorized 29 | return 30 | } 31 | if statusCode == 403, dataResponse.error == nil { 32 | self.error = NetworkError.forbidden 33 | return 34 | } 35 | guard Array(200 ... 299).contains(statusCode), dataResponse.error == nil else { 36 | self.error = dataResponse.error 37 | return 38 | } 39 | guard let data = dataResponse.data else { 40 | self.error = NetworkError.noData 41 | return 42 | } 43 | guard let json = dataResponse.value as? JSON else { 44 | self.error = NetworkError.noData 45 | return 46 | } 47 | 48 | switch request.dataType { 49 | case .data: 50 | self.data = data 51 | case .JSON: 52 | self.json = json 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /TryNetworkLayer/Repository/UsersRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersRepository.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 02/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | protocol UsersRepoProtocol { 12 | var storage: StorageContext { get } 13 | var dispatcher: Dispatcher { get } 14 | var usersOperation: UsersOperationType { get set } 15 | var userDetailOperation: UserDetailOperationType { get set } 16 | 17 | func fetch(fromStorage: Bool, query: String, block: @escaping (_ users: [GHUser]) -> Void) 18 | func fetchDetail(username: String, block: @escaping (_ user: GHUserDetail?) -> Void) 19 | func create(user: GHUser) 20 | func create(userDetail: GHUserDetail) 21 | func update(block: @escaping () -> Void) 22 | func delete(user: GHUser) 23 | func deleteAll() 24 | } 25 | 26 | final class UsersRepo: UsersRepoProtocol { 27 | 28 | private(set) var storage: StorageContext 29 | private(set) var dispatcher: Dispatcher 30 | 31 | var usersOperation: UsersOperationType 32 | var userDetailOperation: UserDetailOperationType 33 | 34 | init() { 35 | do { 36 | self.storage = try RealmStorageContext(configuration: ConfigurationType.basic(identifier: "database")) 37 | } catch let error { 38 | print("\(error.localizedDescription)") 39 | fatalError("Cannot initialize Storage Context") 40 | } 41 | self.dispatcher = NetworkDispatcher(environment: Environment("Github", host: "https://api.github.com")) 42 | self.usersOperation = UsersOperation(query: "") 43 | self.userDetailOperation = UserDetailOperation(username: "") 44 | } 45 | 46 | func fetchDetail(username: String, block: @escaping (_ user: GHUserDetail?) -> Void) { 47 | userDetailOperation.username = username 48 | userDetailOperation.execute(in: dispatcher) { [weak self] result in 49 | switch result { 50 | case .success(let user): 51 | self?.create(userDetail: user) 52 | block(user) 53 | case .failure(let error): 54 | print("\(String(describing: error.localizedDescription))") 55 | } 56 | } 57 | } 58 | 59 | func fetch(fromStorage: Bool = true, query: String, block: @escaping (_ users: [GHUser]) -> Void) { 60 | 61 | if fromStorage { 62 | let sort = Sorted(key: "login", ascending: false) 63 | self.storage.fetch(GHUser.self, predicate: nil, sorted: sort, completion: { (users) in 64 | block(users) 65 | }) 66 | return 67 | } 68 | 69 | usersOperation.query = query 70 | usersOperation.execute(in: dispatcher) { [weak self] result in 71 | switch result { 72 | case .success(let response): 73 | for user in response.items { 74 | self?.create(user: user) 75 | } 76 | block(response.items) 77 | case .failure(let error): 78 | print("\(String(describing: error.localizedDescription))") 79 | } 80 | } 81 | } 82 | 83 | func create(user: GHUser) { 84 | do { 85 | try self.storage.create(object: user, completion: { _ in 86 | }) 87 | } catch _ as NSError { 88 | } 89 | } 90 | 91 | func create(userDetail: GHUserDetail) { 92 | do { 93 | try self.storage.create(object: userDetail, completion: { _ in 94 | }) 95 | } catch _ as NSError { 96 | } 97 | } 98 | 99 | func update(block: @escaping () -> Void) { 100 | do { 101 | try self.storage.update(block: { 102 | block() 103 | }) 104 | } catch _ as NSError { 105 | } 106 | } 107 | 108 | func delete(user: GHUser) { 109 | do { 110 | try self.storage.delete(object: user) 111 | } catch _ as NSError { 112 | } 113 | } 114 | 115 | func deleteAll() { 116 | do { 117 | try self.storage.deleteAll(GHUser.self) 118 | } catch _ as NSError { 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /TryNetworkLayer/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 13/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | var appCoordinator: AppCoordinator! 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | guard let scene = (scene as? UIWindowScene) else { return } 18 | 19 | window = UIWindow(windowScene: scene) 20 | window!.makeKeyAndVisible() 21 | 22 | appCoordinator = AppCoordinator(sceneDelegate: self) 23 | appCoordinator.start() 24 | } 25 | 26 | func sceneDidDisconnect(_ scene: UIScene) { 27 | // Called as the scene is being released by the system. 28 | // This occurs shortly after the scene enters the background, or when its session is discarded. 29 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 30 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 31 | } 32 | 33 | func sceneDidBecomeActive(_ scene: UIScene) { 34 | // Called when the scene has moved from an inactive state to an active state. 35 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 36 | } 37 | 38 | func sceneWillResignActive(_ scene: UIScene) { 39 | // Called when the scene will move from an active state to an inactive state. 40 | // This may occur due to temporary interruptions (ex. an incoming phone call). 41 | } 42 | 43 | func sceneWillEnterForeground(_ scene: UIScene) { 44 | // Called as the scene transitions from the background to the foreground. 45 | // Use this method to undo the changes made on entering the background. 46 | } 47 | 48 | func sceneDidEnterBackground(_ scene: UIScene) { 49 | // Called as the scene transitions from the foreground to the background. 50 | // Use this method to save data, release shared resources, and store enough scene-specific state information 51 | // to restore the scene back to its current state. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /TryNetworkLayer/Storage/RealmStorageContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RealmStorageContext.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 02/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | /// Realm configuration type 13 | public enum ConfigurationType { 14 | case basic(identifier: String?) 15 | case inMemory(identifier: String?) 16 | 17 | var associated: String? { 18 | switch self { 19 | case .basic(let identifier): 20 | return identifier 21 | case .inMemory(let identifier): 22 | return identifier 23 | } 24 | } 25 | } 26 | 27 | class RealmStorageContext: StorageContext { 28 | var realm: Realm? 29 | 30 | static fileprivate let schemaVersion: UInt64 = 1 31 | 32 | required init(configuration: ConfigurationType = .basic(identifier: nil)) throws { 33 | var rmConfig = Realm.Configuration() 34 | rmConfig.readOnly = false 35 | switch configuration { 36 | case .basic: 37 | rmConfig = Realm.Configuration.defaultConfiguration 38 | if let identifier = configuration.associated { 39 | 40 | rmConfig.fileURL = RealmStorageContext.getPath(identifier) as URL? 41 | rmConfig.schemaVersion = RealmStorageContext.schemaVersion 42 | rmConfig.migrationBlock = { migration, oldSchemaVersion in 43 | if oldSchemaVersion < RealmStorageContext.schemaVersion { 44 | } 45 | } 46 | } 47 | case .inMemory: 48 | rmConfig = Realm.Configuration() 49 | if let identifier = configuration.associated { 50 | rmConfig.inMemoryIdentifier = identifier 51 | } else { 52 | throw NSError() 53 | } 54 | } 55 | print("\(rmConfig.fileURL?.absoluteString ?? "Realm in memory")") 56 | try self.realm = Realm(configuration: rmConfig) 57 | } 58 | 59 | public func safeWrite(_ block: (() throws -> Void)) throws { 60 | guard let realm = self.realm else { 61 | throw NSError() 62 | } 63 | 64 | if realm.isInWriteTransaction { 65 | try block() 66 | } else { 67 | try realm.write(block) 68 | } 69 | } 70 | 71 | func create(object: T, completion: @escaping ((T) -> Void)) throws { 72 | guard let realm = self.realm else { 73 | throw NSError() 74 | } 75 | 76 | try self.safeWrite { 77 | let newObject = realm.create(T.self as! Object.Type, value: object, update: .all) as! T 78 | completion(newObject) 79 | } 80 | } 81 | 82 | func save(object: Storable) throws { 83 | guard let realm = self.realm else { 84 | throw NSError() 85 | } 86 | 87 | try self.safeWrite { 88 | realm.add(object as! Object, update: .all) 89 | } 90 | } 91 | 92 | func update(block: @escaping () -> Void) throws { 93 | try self.safeWrite { 94 | block() 95 | } 96 | } 97 | 98 | func delete(object: Storable) throws { 99 | guard let realm = self.realm else { 100 | throw NSError() 101 | } 102 | 103 | try self.safeWrite { 104 | realm.delete(object as! Object) 105 | } 106 | } 107 | 108 | func deleteAll(_ model: T.Type) throws { 109 | guard let realm = self.realm else { 110 | throw NSError() 111 | } 112 | 113 | try self.safeWrite { 114 | let objects = realm.objects(model as! Object.Type) 115 | 116 | for object in objects { 117 | realm.delete(object) 118 | } 119 | } 120 | } 121 | 122 | func reset() throws { 123 | guard let realm = self.realm else { 124 | throw NSError() 125 | } 126 | 127 | try self.safeWrite { 128 | realm.deleteAll() 129 | } 130 | } 131 | 132 | func fetch(_ model: T.Type, predicate: NSPredicate? = nil, sorted: Sorted? = nil, completion: (([T]) -> Void)) { 133 | var objects = self.realm?.objects(model as! Object.Type) 134 | 135 | if let predicate = predicate { 136 | objects = objects?.filter(predicate) 137 | } 138 | 139 | if let sorted = sorted { 140 | objects = objects?.sorted(byKeyPath: sorted.key, ascending: sorted.ascending) 141 | } 142 | 143 | var accumulate: [T] = [T]() 144 | for object in objects! { 145 | accumulate.append(object as! T) 146 | } 147 | 148 | completion(accumulate) 149 | } 150 | 151 | // MARK: Utils 152 | 153 | static fileprivate func getPath(_ identifier: String) -> URL { 154 | let realmPath = NSURL.fileURL(withPath: Realm.Configuration().fileURL!.absoluteString) 155 | .deletingLastPathComponent() 156 | .appendingPathComponent(identifier) 157 | .appendingPathExtension("realm") 158 | return realmPath 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /TryNetworkLayer/Storage/Storable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storable.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 02/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | 12 | /// Storable object 13 | public protocol Storable { 14 | } 15 | 16 | extension Object: Storable { 17 | } 18 | -------------------------------------------------------------------------------- /TryNetworkLayer/Storage/StorageContext.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StorageContext.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 02/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct Sorted { 12 | var key: String 13 | var ascending: Bool = true 14 | } 15 | 16 | /// Operations available on storage context 17 | protocol StorageContext { 18 | 19 | /// Create a new object with default values 20 | /// 21 | /// - Parameters: 22 | /// - object: The object to create on Realm 23 | /// - completion: The completion handler 24 | /// - Returns: An object that is conformed to the `Storable` protocol 25 | func create(object: T, completion: @escaping ((T) -> Void)) throws 26 | 27 | /// Save an object 28 | /// 29 | /// - Parameter object: The object to save 30 | func save(object: Storable) throws 31 | 32 | /// Update an object 33 | /// 34 | /// - Parameter block: The update block 35 | func update(block: @escaping () -> Void) throws 36 | 37 | /// Delete an object 38 | /// 39 | /// - Parameter object: The object to delete 40 | func delete(object: Storable) throws 41 | 42 | /// Delete all objects 43 | /// 44 | /// - Parameter model: The model type of the objects to delete 45 | func deleteAll(_ model: T.Type) throws 46 | 47 | /// A list of object to fetch from the storage 48 | /// 49 | /// - Parameters: 50 | /// - model: The model type 51 | /// - predicate: An optional predicate 52 | /// - sorted: An optional sorting key 53 | /// - completion: The completion handler 54 | func fetch(_ model: T.Type, predicate: NSPredicate?, sorted: Sorted?, completion: (([T]) -> Void)) 55 | } 56 | -------------------------------------------------------------------------------- /TryNetworkLayer/Storyboards/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 | 32 | 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 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /TryNetworkLayer/Storyboards/Storyboards.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Storyboards.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 13/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /// Storyboard enum. Require storyboardId equal to class name 12 | /// Example: let vc = Storyboard.login.instantiate(LoginViewController.self) 13 | enum Storyboard: String { 14 | case main = "Main" 15 | 16 | func instantiate(_ viewController: VC.Type) -> VC { 17 | guard let vc = UIStoryboard(name: self.rawValue, bundle: nil).instantiateViewController(withIdentifier: VC.storyboardIdentifier) as? VC 18 | else { fatalError("Couldn't instantiate \(VC.storyboardIdentifier) from \(self.rawValue)") } 19 | return vc 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Controllers/UserDetail/UserDetailViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailViewModelTests.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TryNetworkLayer 11 | 12 | class UserDetailViewModelTests: XCTestCase { 13 | 14 | var sut = UserDetailViewModel(userLogin: "andr3a88") 15 | 16 | override func setUp() { 17 | } 18 | 19 | override func tearDown() { 20 | } 21 | 22 | func testExample() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Controllers/Users/UsersViewModelTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersViewModelTests.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TryNetworkLayer 11 | 12 | class UsersViewModelTests: XCTestCase { 13 | 14 | var sut = UsersViewModel() 15 | 16 | override func setUp() { 17 | } 18 | 19 | override func tearDown() { 20 | } 21 | 22 | func testExample() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Mocks/MockGHUser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockGHUser.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MockGHUser { 12 | static let organizationsUrl = "url" 13 | static let score = 1 14 | static let reposUrl = "url" 15 | static let htmlUrl = "url" 16 | static let gravatarId = "id" 17 | static let avatarUrl = "url" 18 | static let type = "user" 19 | static let login = "url" 20 | static let followersUrl = "url" 21 | static let subscriptionsUrl = "url" 22 | static let receivedEventsUrl = "url" 23 | static let url = "url" 24 | 25 | var id = 123456 26 | 27 | init(id: Int) { 28 | self.id = id 29 | } 30 | 31 | func JSON() -> [String: Any] { 32 | return ["organizations_url": MockGHUser.organizationsUrl, 33 | "score": MockGHUser.score, 34 | "repos_url": MockGHUser.reposUrl, 35 | "html_url": MockGHUser.htmlUrl, 36 | "gravatar_id": MockGHUser.gravatarId, 37 | "avatar_url": MockGHUser.avatarUrl, 38 | "type": MockGHUser.type, 39 | "login": MockGHUser.login, 40 | "followers_url": MockGHUser.followersUrl, 41 | "id": id, 42 | "subscriptions_url": MockGHUser.subscriptionsUrl, 43 | "received_events_url": MockGHUser.receivedEventsUrl, 44 | "url": MockGHUser.url] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Mocks/MockSearchResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockSearchResponse.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | struct MockSearchResponse { 12 | static let incompleteResults = false 13 | static let totalCount = 800 14 | static let items = [MockGHUser(id: 1).JSON(), MockGHUser(id: 2).JSON()] 15 | 16 | func JSON() -> [String: Any] { 17 | return ["incomplete_results": MockSearchResponse.incompleteResults, 18 | "total_count": MockSearchResponse.totalCount, 19 | "items": MockSearchResponse.items] 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Mocks/MockUserDetailOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUserDetailOperation.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TryNetworkLayer 11 | 12 | class MockUserDetailOperation: UserDetailOperationType { 13 | 14 | typealias D = Dispatcher 15 | typealias R = GHUserDetail 16 | 17 | // MARK: Request parameters 18 | var username: String 19 | 20 | required init(username: String) { 21 | self.username = username 22 | } 23 | 24 | var request: Request { 25 | return UserRequests.searchUsers(query: self.username, perPage: 50, page: 1) 26 | } 27 | 28 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) { 29 | completion(.success(GHUserDetail())) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Mocks/MockUserOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUserOperation.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TryNetworkLayer 11 | 12 | class MockUserOperation: UsersOperationType { 13 | 14 | typealias D = Dispatcher 15 | typealias R = GHSearchResponse 16 | 17 | // MARK: Request parameters 18 | var query: String 19 | 20 | required init(query: String) { 21 | self.query = query 22 | } 23 | 24 | var request: Request { 25 | return UserRequests.searchUsers(query: self.query, perPage: 50, page: 1) 26 | } 27 | 28 | func execute(in dispatcher: Dispatcher, completion: @escaping (Result) -> Void) { 29 | 30 | let object1 = try! JSONDecoder.decode( MockGHUser(id: 1).JSON(), to: GHUser.self) 31 | let object2 = try! JSONDecoder.decode( MockGHUser(id: 2).JSON(), to: GHUser.self) 32 | let response = GHSearchResponse() 33 | response.items = [object1, object2] 34 | completion(.success(response)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Mocks/MockUsersRepo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MockUsersRepo.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 05/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TryNetworkLayer 11 | 12 | class MockUsersRepo: UsersRepoProtocol { 13 | 14 | var storage: StorageContext = try! RealmStorageContext(configuration: ConfigurationType.inMemory(identifier: "test")) 15 | var dispatcher: Dispatcher = NetworkDispatcher(environment: Environment("Github", host: "mock")) 16 | 17 | var usersOperation: UsersOperationType = MockUserOperation(query: "language:swift") 18 | var userDetailOperation: UserDetailOperationType = MockUserDetailOperation(username: "andr3a88") 19 | 20 | func fetch(fromStorage: Bool = true, query: String, block: @escaping (_ users: [GHUser]) -> Void) { 21 | 22 | if fromStorage { 23 | let sort = Sorted(key: "login", ascending: false) 24 | self.storage.fetch(GHUser.self, predicate: nil, sorted: sort, completion: { (users) in 25 | block(users) 26 | }) 27 | return 28 | } 29 | 30 | usersOperation.query = query 31 | usersOperation.execute(in: dispatcher) { [unowned self] result in 32 | switch result { 33 | case .success(let response): 34 | for user in response.items { 35 | self.create(user: user) 36 | } 37 | block(response.items) 38 | case .failure(let error): 39 | print("\(String(describing: error.localizedDescription))") 40 | } 41 | } 42 | } 43 | 44 | func create(user: GHUser) { 45 | do { 46 | try self.storage.create(object: user, completion: { (user) in 47 | }) 48 | } catch _ as NSError { 49 | } 50 | } 51 | 52 | func update(block: @escaping () -> Void) { 53 | do { 54 | try self.storage.update(block: { 55 | block() 56 | }) 57 | } catch _ as NSError { 58 | } 59 | } 60 | 61 | func delete(user: GHUser) { 62 | do { 63 | try self.storage.delete(object: user) 64 | } catch _ as NSError { 65 | } 66 | } 67 | 68 | func fetchDetail(username: String, block: @escaping (GHUserDetail?) -> Void) { 69 | 70 | } 71 | 72 | func create(userDetail: GHUserDetail) { 73 | 74 | } 75 | 76 | func deleteAll() { 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Network/UserDetailOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserDetailTaskTests.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 03/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TryNetworkLayer 11 | 12 | class UserDetailOperationTests: XCTestCase { 13 | 14 | let dispatcher = NetworkDispatcher(environment: TestUtils.Env.default) 15 | let sut = UserDetailOperation(username: "andr3a88") 16 | 17 | override func setUp() { 18 | super.setUp() 19 | // Put setup code here. This method is called before the invocation of each test method in the class. 20 | } 21 | 22 | override func tearDown() { 23 | // Put teardown code here. This method is called after the invocation of each test method in the class. 24 | super.tearDown() 25 | } 26 | 27 | func testUserDetailTask() { 28 | let expectedResult = expectation(description: "Async real request") 29 | 30 | sut.execute(in: dispatcher) { result in 31 | 32 | switch result { 33 | case .success(let user): 34 | XCTAssertEqual(user.login, "andr3a88") 35 | expectedResult.fulfill() 36 | case .failure(let error): 37 | XCTFail("\(error.localizedDescription)") 38 | } 39 | 40 | } 41 | waitForExpectations(timeout: 10, handler:nil) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Network/UsersOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TryNetworkLayerTests.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea on 30/12/2019. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TryNetworkLayer 11 | 12 | class UsersOperationTests: XCTestCase { 13 | 14 | let dispatcher = NetworkDispatcher(environment: TestUtils.Env.default) 15 | let sut = UsersOperation(query: "language:swift") 16 | 17 | override func setUp() { 18 | super.setUp() 19 | } 20 | 21 | override func tearDown() { 22 | super.tearDown() 23 | } 24 | 25 | func testSearchResponseMapper() { 26 | let object = try! JSONDecoder.decode(MockSearchResponse().JSON(), to: GHSearchResponse.self) 27 | 28 | XCTAssertNotNil(object) 29 | XCTAssertFalse(object.incompleteResults) 30 | XCTAssertEqual(object.totalCount, 800) 31 | XCTAssertEqual(object.items.count, 2) 32 | 33 | } 34 | 35 | func testUserMapper() { 36 | let object = try! JSONDecoder.decode( MockGHUser(id: 1).JSON(), to: GHUser.self) 37 | 38 | XCTAssertNotNil(object) 39 | XCTAssertEqual(object.organizationsUrl!, "url") 40 | XCTAssertEqual(object.score, 1) 41 | XCTAssertEqual(object.reposUrl!, "url") 42 | XCTAssertEqual(object.htmlUrl!, "url") 43 | XCTAssertEqual(object.gravatarId!, "id") 44 | XCTAssertEqual(object.avatarUrl!, "url") 45 | XCTAssertEqual(object.type!, "user") 46 | XCTAssertEqual(object.login, "url") 47 | XCTAssertEqual(object.followersUrl!, "url") 48 | XCTAssertEqual(object.id, 1) 49 | XCTAssertEqual(object.receivedEventsUrl!, "url") 50 | XCTAssertEqual(object.subscriptionsUrl!, "url") 51 | XCTAssertEqual(object.url!, "url") 52 | } 53 | 54 | func testMockSearchUserTask() { 55 | let expectedResult = expectation(description: "Async mocked request") 56 | 57 | sut.execute(in: dispatcher) { result in 58 | switch result { 59 | case .success(let response): 60 | XCTAssertEqual(response.items.count, 50) 61 | expectedResult.fulfill() 62 | case .failure(let error): 63 | XCTFail("\(error.localizedDescription)") 64 | } 65 | } 66 | waitForExpectations(timeout: 10, handler:nil) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/Repository/UsersRepoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UsersRepoTests.swift 3 | // TryNetworkLayer 4 | // 5 | // Created by Andrea Stevanato on 02/09/2017. 6 | // Copyright © 2019 Andrea Stevanato All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import TryNetworkLayer 11 | 12 | class UsersRepoTests: XCTestCase { 13 | 14 | let sut = UsersRepo() 15 | 16 | override func setUp() { 17 | super.setUp() 18 | } 19 | 20 | override func tearDown() { 21 | super.tearDown() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /TryNetworkLayerTests/TestUtils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestUtils.swift 3 | // TryNetworkLayerTests 4 | // 5 | // Created by Andrea Stevanato on 04/03/2020. 6 | // Copyright © 2020 Andrea Stevanato. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import TryNetworkLayer 11 | 12 | struct TestUtils { 13 | struct Env { 14 | static let Local = Environment("Local", host: "http://localhost:3900") 15 | static let GitHub = Environment("Github", host: "https://api.github.com") 16 | 17 | static let `default` = TestUtils.Env.GitHub 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /wiremock/README.md: -------------------------------------------------------------------------------- 1 | ## WIREMOCK 2 | 3 | Local server for testing purpose. 4 | 5 | #### Installation 6 | 7 | Run the mocked server with `start_server.sh` 8 | 9 | #### Stubbing 10 | 11 | http://wiremock.org/docs/stubbing/ 12 | 13 | #### Request matching 14 | 15 | http://wiremock.org/docs/request-matching/ 16 | 17 | Query parameters match: 18 | ``` 19 | "queryParameters" : { 20 | "search_term" : { 21 | "equalTo" : "WireMock" 22 | } 23 | } 24 | ``` 25 | -------------------------------------------------------------------------------- /wiremock/__files/users/detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "login": "andr3a88", 3 | "id": 6755254, 4 | "node_id": "MDQ6VXNlcjY3NTUyNTQ=", 5 | "avatar_url": "https://avatars0.githubusercontent.com/u/6755254?v=4", 6 | "gravatar_id": "", 7 | "url": "https://api.github.com/users/andr3a88", 8 | "html_url": "https://github.com/andr3a88", 9 | "followers_url": "https://api.github.com/users/andr3a88/followers", 10 | "following_url": "https://api.github.com/users/andr3a88/following{/other_user}", 11 | "gists_url": "https://api.github.com/users/andr3a88/gists{/gist_id}", 12 | "starred_url": "https://api.github.com/users/andr3a88/starred{/owner}{/repo}", 13 | "subscriptions_url": "https://api.github.com/users/andr3a88/subscriptions", 14 | "organizations_url": "https://api.github.com/users/andr3a88/orgs", 15 | "repos_url": "https://api.github.com/users/andr3a88/repos", 16 | "events_url": "https://api.github.com/users/andr3a88/events{/privacy}", 17 | "received_events_url": "https://api.github.com/users/andr3a88/received_events", 18 | "type": "User", 19 | "site_admin": false, 20 | "name": "Andrea", 21 | "company": "@DoveConviene (ShopFully International Group)", 22 | "blog": "https://andr3a88.github.io/", 23 | "location": "Venice (Italy)", 24 | "email": null, 25 | "hireable": true, 26 | "bio": "📱iOS Developer. Swift and clean code enthusiast. Also working on backend development with NodeJS, Typescript.", 27 | "public_repos": 47, 28 | "public_gists": 24, 29 | "followers": 14, 30 | "following": 32, 31 | "created_at": "2014-02-22T10:06:28Z", 32 | "updated_at": "2020-03-03T21:24:03Z" 33 | } -------------------------------------------------------------------------------- /wiremock/__files/users/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "total_count": 131810, 3 | "incomplete_results": false, 4 | "items": [ 5 | { 6 | "login": "JohnSundell", 7 | "id": 2466701, 8 | "node_id": "MDQ6VXNlcjI0NjY3MDE=", 9 | "avatar_url": "https://avatars3.githubusercontent.com/u/2466701?v=4", 10 | "gravatar_id": "", 11 | "url": "https://api.github.com/users/JohnSundell", 12 | "html_url": "https://github.com/JohnSundell", 13 | "followers_url": "https://api.github.com/users/JohnSundell/followers", 14 | "following_url": "https://api.github.com/users/JohnSundell/following{/other_user}", 15 | "gists_url": "https://api.github.com/users/JohnSundell/gists{/gist_id}", 16 | "starred_url": "https://api.github.com/users/JohnSundell/starred{/owner}{/repo}", 17 | "subscriptions_url": "https://api.github.com/users/JohnSundell/subscriptions", 18 | "organizations_url": "https://api.github.com/users/JohnSundell/orgs", 19 | "repos_url": "https://api.github.com/users/JohnSundell/repos", 20 | "events_url": "https://api.github.com/users/JohnSundell/events{/privacy}", 21 | "received_events_url": "https://api.github.com/users/JohnSundell/received_events", 22 | "type": "User", 23 | "site_admin": false, 24 | "score": 1.0 25 | }, 26 | { 27 | "login": "RamotionDev", 28 | "id": 8984754, 29 | "node_id": "MDQ6VXNlcjg5ODQ3NTQ=", 30 | "avatar_url": "https://avatars3.githubusercontent.com/u/8984754?v=4", 31 | "gravatar_id": "", 32 | "url": "https://api.github.com/users/RamotionDev", 33 | "html_url": "https://github.com/RamotionDev", 34 | "followers_url": "https://api.github.com/users/RamotionDev/followers", 35 | "following_url": "https://api.github.com/users/RamotionDev/following{/other_user}", 36 | "gists_url": "https://api.github.com/users/RamotionDev/gists{/gist_id}", 37 | "starred_url": "https://api.github.com/users/RamotionDev/starred{/owner}{/repo}", 38 | "subscriptions_url": "https://api.github.com/users/RamotionDev/subscriptions", 39 | "organizations_url": "https://api.github.com/users/RamotionDev/orgs", 40 | "repos_url": "https://api.github.com/users/RamotionDev/repos", 41 | "events_url": "https://api.github.com/users/RamotionDev/events{/privacy}", 42 | "received_events_url": "https://api.github.com/users/RamotionDev/received_events", 43 | "type": "User", 44 | "site_admin": false, 45 | "score": 1.0 46 | }, 47 | { 48 | "login": "kevinzhow", 49 | "id": 1156192, 50 | "node_id": "MDQ6VXNlcjExNTYxOTI=", 51 | "avatar_url": "https://avatars1.githubusercontent.com/u/1156192?v=4", 52 | "gravatar_id": "", 53 | "url": "https://api.github.com/users/kevinzhow", 54 | "html_url": "https://github.com/kevinzhow", 55 | "followers_url": "https://api.github.com/users/kevinzhow/followers", 56 | "following_url": "https://api.github.com/users/kevinzhow/following{/other_user}", 57 | "gists_url": "https://api.github.com/users/kevinzhow/gists{/gist_id}", 58 | "starred_url": "https://api.github.com/users/kevinzhow/starred{/owner}{/repo}", 59 | "subscriptions_url": "https://api.github.com/users/kevinzhow/subscriptions", 60 | "organizations_url": "https://api.github.com/users/kevinzhow/orgs", 61 | "repos_url": "https://api.github.com/users/kevinzhow/repos", 62 | "events_url": "https://api.github.com/users/kevinzhow/events{/privacy}", 63 | "received_events_url": "https://api.github.com/users/kevinzhow/received_events", 64 | "type": "User", 65 | "site_admin": false, 66 | "score": 1.0 67 | }, 68 | { 69 | "login": "ForrestKnight", 70 | "id": 15620553, 71 | "node_id": "MDQ6VXNlcjE1NjIwNTUz", 72 | "avatar_url": "https://avatars0.githubusercontent.com/u/15620553?v=4", 73 | "gravatar_id": "", 74 | "url": "https://api.github.com/users/ForrestKnight", 75 | "html_url": "https://github.com/ForrestKnight", 76 | "followers_url": "https://api.github.com/users/ForrestKnight/followers", 77 | "following_url": "https://api.github.com/users/ForrestKnight/following{/other_user}", 78 | "gists_url": "https://api.github.com/users/ForrestKnight/gists{/gist_id}", 79 | "starred_url": "https://api.github.com/users/ForrestKnight/starred{/owner}{/repo}", 80 | "subscriptions_url": "https://api.github.com/users/ForrestKnight/subscriptions", 81 | "organizations_url": "https://api.github.com/users/ForrestKnight/orgs", 82 | "repos_url": "https://api.github.com/users/ForrestKnight/repos", 83 | "events_url": "https://api.github.com/users/ForrestKnight/events{/privacy}", 84 | "received_events_url": "https://api.github.com/users/ForrestKnight/received_events", 85 | "type": "User", 86 | "site_admin": false, 87 | "score": 1.0 88 | }, 89 | { 90 | "login": "lexrus", 91 | "id": 219689, 92 | "node_id": "MDQ6VXNlcjIxOTY4OQ==", 93 | "avatar_url": "https://avatars0.githubusercontent.com/u/219689?v=4", 94 | "gravatar_id": "", 95 | "url": "https://api.github.com/users/lexrus", 96 | "html_url": "https://github.com/lexrus", 97 | "followers_url": "https://api.github.com/users/lexrus/followers", 98 | "following_url": "https://api.github.com/users/lexrus/following{/other_user}", 99 | "gists_url": "https://api.github.com/users/lexrus/gists{/gist_id}", 100 | "starred_url": "https://api.github.com/users/lexrus/starred{/owner}{/repo}", 101 | "subscriptions_url": "https://api.github.com/users/lexrus/subscriptions", 102 | "organizations_url": "https://api.github.com/users/lexrus/orgs", 103 | "repos_url": "https://api.github.com/users/lexrus/repos", 104 | "events_url": "https://api.github.com/users/lexrus/events{/privacy}", 105 | "received_events_url": "https://api.github.com/users/lexrus/received_events", 106 | "type": "User", 107 | "site_admin": false, 108 | "score": 1.0 109 | }, 110 | { 111 | "login": "KalleHallden", 112 | "id": 35563440, 113 | "node_id": "MDQ6VXNlcjM1NTYzNDQw", 114 | "avatar_url": "https://avatars2.githubusercontent.com/u/35563440?v=4", 115 | "gravatar_id": "", 116 | "url": "https://api.github.com/users/KalleHallden", 117 | "html_url": "https://github.com/KalleHallden", 118 | "followers_url": "https://api.github.com/users/KalleHallden/followers", 119 | "following_url": "https://api.github.com/users/KalleHallden/following{/other_user}", 120 | "gists_url": "https://api.github.com/users/KalleHallden/gists{/gist_id}", 121 | "starred_url": "https://api.github.com/users/KalleHallden/starred{/owner}{/repo}", 122 | "subscriptions_url": "https://api.github.com/users/KalleHallden/subscriptions", 123 | "organizations_url": "https://api.github.com/users/KalleHallden/orgs", 124 | "repos_url": "https://api.github.com/users/KalleHallden/repos", 125 | "events_url": "https://api.github.com/users/KalleHallden/events{/privacy}", 126 | "received_events_url": "https://api.github.com/users/KalleHallden/received_events", 127 | "type": "User", 128 | "site_admin": false, 129 | "score": 1.0 130 | }, 131 | { 132 | "login": "JakeLin", 133 | "id": 573856, 134 | "node_id": "MDQ6VXNlcjU3Mzg1Ng==", 135 | "avatar_url": "https://avatars1.githubusercontent.com/u/573856?v=4", 136 | "gravatar_id": "", 137 | "url": "https://api.github.com/users/JakeLin", 138 | "html_url": "https://github.com/JakeLin", 139 | "followers_url": "https://api.github.com/users/JakeLin/followers", 140 | "following_url": "https://api.github.com/users/JakeLin/following{/other_user}", 141 | "gists_url": "https://api.github.com/users/JakeLin/gists{/gist_id}", 142 | "starred_url": "https://api.github.com/users/JakeLin/starred{/owner}{/repo}", 143 | "subscriptions_url": "https://api.github.com/users/JakeLin/subscriptions", 144 | "organizations_url": "https://api.github.com/users/JakeLin/orgs", 145 | "repos_url": "https://api.github.com/users/JakeLin/repos", 146 | "events_url": "https://api.github.com/users/JakeLin/events{/privacy}", 147 | "received_events_url": "https://api.github.com/users/JakeLin/received_events", 148 | "type": "User", 149 | "site_admin": false, 150 | "score": 1.0 151 | }, 152 | { 153 | "login": "twostraws", 154 | "id": 190200, 155 | "node_id": "MDQ6VXNlcjE5MDIwMA==", 156 | "avatar_url": "https://avatars3.githubusercontent.com/u/190200?v=4", 157 | "gravatar_id": "", 158 | "url": "https://api.github.com/users/twostraws", 159 | "html_url": "https://github.com/twostraws", 160 | "followers_url": "https://api.github.com/users/twostraws/followers", 161 | "following_url": "https://api.github.com/users/twostraws/following{/other_user}", 162 | "gists_url": "https://api.github.com/users/twostraws/gists{/gist_id}", 163 | "starred_url": "https://api.github.com/users/twostraws/starred{/owner}{/repo}", 164 | "subscriptions_url": "https://api.github.com/users/twostraws/subscriptions", 165 | "organizations_url": "https://api.github.com/users/twostraws/orgs", 166 | "repos_url": "https://api.github.com/users/twostraws/repos", 167 | "events_url": "https://api.github.com/users/twostraws/events{/privacy}", 168 | "received_events_url": "https://api.github.com/users/twostraws/received_events", 169 | "type": "User", 170 | "site_admin": false, 171 | "score": 1.0 172 | }, 173 | { 174 | "login": "jessesquires", 175 | "id": 2301114, 176 | "node_id": "MDQ6VXNlcjIzMDExMTQ=", 177 | "avatar_url": "https://avatars2.githubusercontent.com/u/2301114?v=4", 178 | "gravatar_id": "", 179 | "url": "https://api.github.com/users/jessesquires", 180 | "html_url": "https://github.com/jessesquires", 181 | "followers_url": "https://api.github.com/users/jessesquires/followers", 182 | "following_url": "https://api.github.com/users/jessesquires/following{/other_user}", 183 | "gists_url": "https://api.github.com/users/jessesquires/gists{/gist_id}", 184 | "starred_url": "https://api.github.com/users/jessesquires/starred{/owner}{/repo}", 185 | "subscriptions_url": "https://api.github.com/users/jessesquires/subscriptions", 186 | "organizations_url": "https://api.github.com/users/jessesquires/orgs", 187 | "repos_url": "https://api.github.com/users/jessesquires/repos", 188 | "events_url": "https://api.github.com/users/jessesquires/events{/privacy}", 189 | "received_events_url": "https://api.github.com/users/jessesquires/received_events", 190 | "type": "User", 191 | "site_admin": false, 192 | "score": 1.0 193 | }, 194 | { 195 | "login": "hollance", 196 | "id": 346853, 197 | "node_id": "MDQ6VXNlcjM0Njg1Mw==", 198 | "avatar_url": "https://avatars3.githubusercontent.com/u/346853?v=4", 199 | "gravatar_id": "", 200 | "url": "https://api.github.com/users/hollance", 201 | "html_url": "https://github.com/hollance", 202 | "followers_url": "https://api.github.com/users/hollance/followers", 203 | "following_url": "https://api.github.com/users/hollance/following{/other_user}", 204 | "gists_url": "https://api.github.com/users/hollance/gists{/gist_id}", 205 | "starred_url": "https://api.github.com/users/hollance/starred{/owner}{/repo}", 206 | "subscriptions_url": "https://api.github.com/users/hollance/subscriptions", 207 | "organizations_url": "https://api.github.com/users/hollance/orgs", 208 | "repos_url": "https://api.github.com/users/hollance/repos", 209 | "events_url": "https://api.github.com/users/hollance/events{/privacy}", 210 | "received_events_url": "https://api.github.com/users/hollance/received_events", 211 | "type": "User", 212 | "site_admin": false, 213 | "score": 1.0 214 | } 215 | ] 216 | } -------------------------------------------------------------------------------- /wiremock/__files/users/users.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "login": "mojombo", 4 | "id": 1, 5 | "node_id": "MDQ6VXNlcjE=", 6 | "avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4", 7 | "gravatar_id": "", 8 | "url": "https://api.github.com/users/mojombo", 9 | "html_url": "https://github.com/mojombo", 10 | "followers_url": "https://api.github.com/users/mojombo/followers", 11 | "following_url": "https://api.github.com/users/mojombo/following{/other_user}", 12 | "gists_url": "https://api.github.com/users/mojombo/gists{/gist_id}", 13 | "starred_url": "https://api.github.com/users/mojombo/starred{/owner}{/repo}", 14 | "subscriptions_url": "https://api.github.com/users/mojombo/subscriptions", 15 | "organizations_url": "https://api.github.com/users/mojombo/orgs", 16 | "repos_url": "https://api.github.com/users/mojombo/repos", 17 | "events_url": "https://api.github.com/users/mojombo/events{/privacy}", 18 | "received_events_url": "https://api.github.com/users/mojombo/received_events", 19 | "type": "User", 20 | "site_admin": false 21 | }, 22 | { 23 | "login": "defunkt", 24 | "id": 2, 25 | "node_id": "MDQ6VXNlcjI=", 26 | "avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4", 27 | "gravatar_id": "", 28 | "url": "https://api.github.com/users/defunkt", 29 | "html_url": "https://github.com/defunkt", 30 | "followers_url": "https://api.github.com/users/defunkt/followers", 31 | "following_url": "https://api.github.com/users/defunkt/following{/other_user}", 32 | "gists_url": "https://api.github.com/users/defunkt/gists{/gist_id}", 33 | "starred_url": "https://api.github.com/users/defunkt/starred{/owner}{/repo}", 34 | "subscriptions_url": "https://api.github.com/users/defunkt/subscriptions", 35 | "organizations_url": "https://api.github.com/users/defunkt/orgs", 36 | "repos_url": "https://api.github.com/users/defunkt/repos", 37 | "events_url": "https://api.github.com/users/defunkt/events{/privacy}", 38 | "received_events_url": "https://api.github.com/users/defunkt/received_events", 39 | "type": "User", 40 | "site_admin": false 41 | }, 42 | { 43 | "login": "pjhyett", 44 | "id": 3, 45 | "node_id": "MDQ6VXNlcjM=", 46 | "avatar_url": "https://avatars0.githubusercontent.com/u/3?v=4", 47 | "gravatar_id": "", 48 | "url": "https://api.github.com/users/pjhyett", 49 | "html_url": "https://github.com/pjhyett", 50 | "followers_url": "https://api.github.com/users/pjhyett/followers", 51 | "following_url": "https://api.github.com/users/pjhyett/following{/other_user}", 52 | "gists_url": "https://api.github.com/users/pjhyett/gists{/gist_id}", 53 | "starred_url": "https://api.github.com/users/pjhyett/starred{/owner}{/repo}", 54 | "subscriptions_url": "https://api.github.com/users/pjhyett/subscriptions", 55 | "organizations_url": "https://api.github.com/users/pjhyett/orgs", 56 | "repos_url": "https://api.github.com/users/pjhyett/repos", 57 | "events_url": "https://api.github.com/users/pjhyett/events{/privacy}", 58 | "received_events_url": "https://api.github.com/users/pjhyett/received_events", 59 | "type": "User", 60 | "site_admin": false 61 | }, 62 | { 63 | "login": "wycats", 64 | "id": 4, 65 | "node_id": "MDQ6VXNlcjQ=", 66 | "avatar_url": "https://avatars0.githubusercontent.com/u/4?v=4", 67 | "gravatar_id": "", 68 | "url": "https://api.github.com/users/wycats", 69 | "html_url": "https://github.com/wycats", 70 | "followers_url": "https://api.github.com/users/wycats/followers", 71 | "following_url": "https://api.github.com/users/wycats/following{/other_user}", 72 | "gists_url": "https://api.github.com/users/wycats/gists{/gist_id}", 73 | "starred_url": "https://api.github.com/users/wycats/starred{/owner}{/repo}", 74 | "subscriptions_url": "https://api.github.com/users/wycats/subscriptions", 75 | "organizations_url": "https://api.github.com/users/wycats/orgs", 76 | "repos_url": "https://api.github.com/users/wycats/repos", 77 | "events_url": "https://api.github.com/users/wycats/events{/privacy}", 78 | "received_events_url": "https://api.github.com/users/wycats/received_events", 79 | "type": "User", 80 | "site_admin": false 81 | }, 82 | { 83 | "login": "ezmobius", 84 | "id": 5, 85 | "node_id": "MDQ6VXNlcjU=", 86 | "avatar_url": "https://avatars0.githubusercontent.com/u/5?v=4", 87 | "gravatar_id": "", 88 | "url": "https://api.github.com/users/ezmobius", 89 | "html_url": "https://github.com/ezmobius", 90 | "followers_url": "https://api.github.com/users/ezmobius/followers", 91 | "following_url": "https://api.github.com/users/ezmobius/following{/other_user}", 92 | "gists_url": "https://api.github.com/users/ezmobius/gists{/gist_id}", 93 | "starred_url": "https://api.github.com/users/ezmobius/starred{/owner}{/repo}", 94 | "subscriptions_url": "https://api.github.com/users/ezmobius/subscriptions", 95 | "organizations_url": "https://api.github.com/users/ezmobius/orgs", 96 | "repos_url": "https://api.github.com/users/ezmobius/repos", 97 | "events_url": "https://api.github.com/users/ezmobius/events{/privacy}", 98 | "received_events_url": "https://api.github.com/users/ezmobius/received_events", 99 | "type": "User", 100 | "site_admin": false 101 | }, 102 | { 103 | "login": "ivey", 104 | "id": 6, 105 | "node_id": "MDQ6VXNlcjY=", 106 | "avatar_url": "https://avatars0.githubusercontent.com/u/6?v=4", 107 | "gravatar_id": "", 108 | "url": "https://api.github.com/users/ivey", 109 | "html_url": "https://github.com/ivey", 110 | "followers_url": "https://api.github.com/users/ivey/followers", 111 | "following_url": "https://api.github.com/users/ivey/following{/other_user}", 112 | "gists_url": "https://api.github.com/users/ivey/gists{/gist_id}", 113 | "starred_url": "https://api.github.com/users/ivey/starred{/owner}{/repo}", 114 | "subscriptions_url": "https://api.github.com/users/ivey/subscriptions", 115 | "organizations_url": "https://api.github.com/users/ivey/orgs", 116 | "repos_url": "https://api.github.com/users/ivey/repos", 117 | "events_url": "https://api.github.com/users/ivey/events{/privacy}", 118 | "received_events_url": "https://api.github.com/users/ivey/received_events", 119 | "type": "User", 120 | "site_admin": false 121 | }, 122 | { 123 | "login": "evanphx", 124 | "id": 7, 125 | "node_id": "MDQ6VXNlcjc=", 126 | "avatar_url": "https://avatars0.githubusercontent.com/u/7?v=4", 127 | "gravatar_id": "", 128 | "url": "https://api.github.com/users/evanphx", 129 | "html_url": "https://github.com/evanphx", 130 | "followers_url": "https://api.github.com/users/evanphx/followers", 131 | "following_url": "https://api.github.com/users/evanphx/following{/other_user}", 132 | "gists_url": "https://api.github.com/users/evanphx/gists{/gist_id}", 133 | "starred_url": "https://api.github.com/users/evanphx/starred{/owner}{/repo}", 134 | "subscriptions_url": "https://api.github.com/users/evanphx/subscriptions", 135 | "organizations_url": "https://api.github.com/users/evanphx/orgs", 136 | "repos_url": "https://api.github.com/users/evanphx/repos", 137 | "events_url": "https://api.github.com/users/evanphx/events{/privacy}", 138 | "received_events_url": "https://api.github.com/users/evanphx/received_events", 139 | "type": "User", 140 | "site_admin": false 141 | }, 142 | { 143 | "login": "vanpelt", 144 | "id": 17, 145 | "node_id": "MDQ6VXNlcjE3", 146 | "avatar_url": "https://avatars1.githubusercontent.com/u/17?v=4", 147 | "gravatar_id": "", 148 | "url": "https://api.github.com/users/vanpelt", 149 | "html_url": "https://github.com/vanpelt", 150 | "followers_url": "https://api.github.com/users/vanpelt/followers", 151 | "following_url": "https://api.github.com/users/vanpelt/following{/other_user}", 152 | "gists_url": "https://api.github.com/users/vanpelt/gists{/gist_id}", 153 | "starred_url": "https://api.github.com/users/vanpelt/starred{/owner}{/repo}", 154 | "subscriptions_url": "https://api.github.com/users/vanpelt/subscriptions", 155 | "organizations_url": "https://api.github.com/users/vanpelt/orgs", 156 | "repos_url": "https://api.github.com/users/vanpelt/repos", 157 | "events_url": "https://api.github.com/users/vanpelt/events{/privacy}", 158 | "received_events_url": "https://api.github.com/users/vanpelt/received_events", 159 | "type": "User", 160 | "site_admin": false 161 | }, 162 | { 163 | "login": "wayneeseguin", 164 | "id": 18, 165 | "node_id": "MDQ6VXNlcjE4", 166 | "avatar_url": "https://avatars0.githubusercontent.com/u/18?v=4", 167 | "gravatar_id": "", 168 | "url": "https://api.github.com/users/wayneeseguin", 169 | "html_url": "https://github.com/wayneeseguin", 170 | "followers_url": "https://api.github.com/users/wayneeseguin/followers", 171 | "following_url": "https://api.github.com/users/wayneeseguin/following{/other_user}", 172 | "gists_url": "https://api.github.com/users/wayneeseguin/gists{/gist_id}", 173 | "starred_url": "https://api.github.com/users/wayneeseguin/starred{/owner}{/repo}", 174 | "subscriptions_url": "https://api.github.com/users/wayneeseguin/subscriptions", 175 | "organizations_url": "https://api.github.com/users/wayneeseguin/orgs", 176 | "repos_url": "https://api.github.com/users/wayneeseguin/repos", 177 | "events_url": "https://api.github.com/users/wayneeseguin/events{/privacy}", 178 | "received_events_url": "https://api.github.com/users/wayneeseguin/received_events", 179 | "type": "User", 180 | "site_admin": false 181 | }, 182 | { 183 | "login": "brynary", 184 | "id": 19, 185 | "node_id": "MDQ6VXNlcjE5", 186 | "avatar_url": "https://avatars0.githubusercontent.com/u/19?v=4", 187 | "gravatar_id": "", 188 | "url": "https://api.github.com/users/brynary", 189 | "html_url": "https://github.com/brynary", 190 | "followers_url": "https://api.github.com/users/brynary/followers", 191 | "following_url": "https://api.github.com/users/brynary/following{/other_user}", 192 | "gists_url": "https://api.github.com/users/brynary/gists{/gist_id}", 193 | "starred_url": "https://api.github.com/users/brynary/starred{/owner}{/repo}", 194 | "subscriptions_url": "https://api.github.com/users/brynary/subscriptions", 195 | "organizations_url": "https://api.github.com/users/brynary/orgs", 196 | "repos_url": "https://api.github.com/users/brynary/repos", 197 | "events_url": "https://api.github.com/users/brynary/events{/privacy}", 198 | "received_events_url": "https://api.github.com/users/brynary/received_events", 199 | "type": "User", 200 | "site_admin": false 201 | }, 202 | { 203 | "login": "kevinclark", 204 | "id": 20, 205 | "node_id": "MDQ6VXNlcjIw", 206 | "avatar_url": "https://avatars3.githubusercontent.com/u/20?v=4", 207 | "gravatar_id": "", 208 | "url": "https://api.github.com/users/kevinclark", 209 | "html_url": "https://github.com/kevinclark", 210 | "followers_url": "https://api.github.com/users/kevinclark/followers", 211 | "following_url": "https://api.github.com/users/kevinclark/following{/other_user}", 212 | "gists_url": "https://api.github.com/users/kevinclark/gists{/gist_id}", 213 | "starred_url": "https://api.github.com/users/kevinclark/starred{/owner}{/repo}", 214 | "subscriptions_url": "https://api.github.com/users/kevinclark/subscriptions", 215 | "organizations_url": "https://api.github.com/users/kevinclark/orgs", 216 | "repos_url": "https://api.github.com/users/kevinclark/repos", 217 | "events_url": "https://api.github.com/users/kevinclark/events{/privacy}", 218 | "received_events_url": "https://api.github.com/users/kevinclark/received_events", 219 | "type": "User", 220 | "site_admin": false 221 | }, 222 | { 223 | "login": "technoweenie", 224 | "id": 21, 225 | "node_id": "MDQ6VXNlcjIx", 226 | "avatar_url": "https://avatars3.githubusercontent.com/u/21?v=4", 227 | "gravatar_id": "", 228 | "url": "https://api.github.com/users/technoweenie", 229 | "html_url": "https://github.com/technoweenie", 230 | "followers_url": "https://api.github.com/users/technoweenie/followers", 231 | "following_url": "https://api.github.com/users/technoweenie/following{/other_user}", 232 | "gists_url": "https://api.github.com/users/technoweenie/gists{/gist_id}", 233 | "starred_url": "https://api.github.com/users/technoweenie/starred{/owner}{/repo}", 234 | "subscriptions_url": "https://api.github.com/users/technoweenie/subscriptions", 235 | "organizations_url": "https://api.github.com/users/technoweenie/orgs", 236 | "repos_url": "https://api.github.com/users/technoweenie/repos", 237 | "events_url": "https://api.github.com/users/technoweenie/events{/privacy}", 238 | "received_events_url": "https://api.github.com/users/technoweenie/received_events", 239 | "type": "User", 240 | "site_admin": false 241 | }, 242 | { 243 | "login": "macournoyer", 244 | "id": 22, 245 | "node_id": "MDQ6VXNlcjIy", 246 | "avatar_url": "https://avatars3.githubusercontent.com/u/22?v=4", 247 | "gravatar_id": "", 248 | "url": "https://api.github.com/users/macournoyer", 249 | "html_url": "https://github.com/macournoyer", 250 | "followers_url": "https://api.github.com/users/macournoyer/followers", 251 | "following_url": "https://api.github.com/users/macournoyer/following{/other_user}", 252 | "gists_url": "https://api.github.com/users/macournoyer/gists{/gist_id}", 253 | "starred_url": "https://api.github.com/users/macournoyer/starred{/owner}{/repo}", 254 | "subscriptions_url": "https://api.github.com/users/macournoyer/subscriptions", 255 | "organizations_url": "https://api.github.com/users/macournoyer/orgs", 256 | "repos_url": "https://api.github.com/users/macournoyer/repos", 257 | "events_url": "https://api.github.com/users/macournoyer/events{/privacy}", 258 | "received_events_url": "https://api.github.com/users/macournoyer/received_events", 259 | "type": "User", 260 | "site_admin": false 261 | }, 262 | { 263 | "login": "takeo", 264 | "id": 23, 265 | "node_id": "MDQ6VXNlcjIz", 266 | "avatar_url": "https://avatars3.githubusercontent.com/u/23?v=4", 267 | "gravatar_id": "", 268 | "url": "https://api.github.com/users/takeo", 269 | "html_url": "https://github.com/takeo", 270 | "followers_url": "https://api.github.com/users/takeo/followers", 271 | "following_url": "https://api.github.com/users/takeo/following{/other_user}", 272 | "gists_url": "https://api.github.com/users/takeo/gists{/gist_id}", 273 | "starred_url": "https://api.github.com/users/takeo/starred{/owner}{/repo}", 274 | "subscriptions_url": "https://api.github.com/users/takeo/subscriptions", 275 | "organizations_url": "https://api.github.com/users/takeo/orgs", 276 | "repos_url": "https://api.github.com/users/takeo/repos", 277 | "events_url": "https://api.github.com/users/takeo/events{/privacy}", 278 | "received_events_url": "https://api.github.com/users/takeo/received_events", 279 | "type": "User", 280 | "site_admin": false 281 | }, 282 | { 283 | "login": "caged", 284 | "id": 25, 285 | "node_id": "MDQ6VXNlcjI1", 286 | "avatar_url": "https://avatars3.githubusercontent.com/u/25?v=4", 287 | "gravatar_id": "", 288 | "url": "https://api.github.com/users/caged", 289 | "html_url": "https://github.com/caged", 290 | "followers_url": "https://api.github.com/users/caged/followers", 291 | "following_url": "https://api.github.com/users/caged/following{/other_user}", 292 | "gists_url": "https://api.github.com/users/caged/gists{/gist_id}", 293 | "starred_url": "https://api.github.com/users/caged/starred{/owner}{/repo}", 294 | "subscriptions_url": "https://api.github.com/users/caged/subscriptions", 295 | "organizations_url": "https://api.github.com/users/caged/orgs", 296 | "repos_url": "https://api.github.com/users/caged/repos", 297 | "events_url": "https://api.github.com/users/caged/events{/privacy}", 298 | "received_events_url": "https://api.github.com/users/caged/received_events", 299 | "type": "User", 300 | "site_admin": false 301 | }, 302 | { 303 | "login": "topfunky", 304 | "id": 26, 305 | "node_id": "MDQ6VXNlcjI2", 306 | "avatar_url": "https://avatars3.githubusercontent.com/u/26?v=4", 307 | "gravatar_id": "", 308 | "url": "https://api.github.com/users/topfunky", 309 | "html_url": "https://github.com/topfunky", 310 | "followers_url": "https://api.github.com/users/topfunky/followers", 311 | "following_url": "https://api.github.com/users/topfunky/following{/other_user}", 312 | "gists_url": "https://api.github.com/users/topfunky/gists{/gist_id}", 313 | "starred_url": "https://api.github.com/users/topfunky/starred{/owner}{/repo}", 314 | "subscriptions_url": "https://api.github.com/users/topfunky/subscriptions", 315 | "organizations_url": "https://api.github.com/users/topfunky/orgs", 316 | "repos_url": "https://api.github.com/users/topfunky/repos", 317 | "events_url": "https://api.github.com/users/topfunky/events{/privacy}", 318 | "received_events_url": "https://api.github.com/users/topfunky/received_events", 319 | "type": "User", 320 | "site_admin": false 321 | }, 322 | { 323 | "login": "anotherjesse", 324 | "id": 27, 325 | "node_id": "MDQ6VXNlcjI3", 326 | "avatar_url": "https://avatars3.githubusercontent.com/u/27?v=4", 327 | "gravatar_id": "", 328 | "url": "https://api.github.com/users/anotherjesse", 329 | "html_url": "https://github.com/anotherjesse", 330 | "followers_url": "https://api.github.com/users/anotherjesse/followers", 331 | "following_url": "https://api.github.com/users/anotherjesse/following{/other_user}", 332 | "gists_url": "https://api.github.com/users/anotherjesse/gists{/gist_id}", 333 | "starred_url": "https://api.github.com/users/anotherjesse/starred{/owner}{/repo}", 334 | "subscriptions_url": "https://api.github.com/users/anotherjesse/subscriptions", 335 | "organizations_url": "https://api.github.com/users/anotherjesse/orgs", 336 | "repos_url": "https://api.github.com/users/anotherjesse/repos", 337 | "events_url": "https://api.github.com/users/anotherjesse/events{/privacy}", 338 | "received_events_url": "https://api.github.com/users/anotherjesse/received_events", 339 | "type": "User", 340 | "site_admin": false 341 | }, 342 | { 343 | "login": "roland", 344 | "id": 28, 345 | "node_id": "MDQ6VXNlcjI4", 346 | "avatar_url": "https://avatars2.githubusercontent.com/u/28?v=4", 347 | "gravatar_id": "", 348 | "url": "https://api.github.com/users/roland", 349 | "html_url": "https://github.com/roland", 350 | "followers_url": "https://api.github.com/users/roland/followers", 351 | "following_url": "https://api.github.com/users/roland/following{/other_user}", 352 | "gists_url": "https://api.github.com/users/roland/gists{/gist_id}", 353 | "starred_url": "https://api.github.com/users/roland/starred{/owner}{/repo}", 354 | "subscriptions_url": "https://api.github.com/users/roland/subscriptions", 355 | "organizations_url": "https://api.github.com/users/roland/orgs", 356 | "repos_url": "https://api.github.com/users/roland/repos", 357 | "events_url": "https://api.github.com/users/roland/events{/privacy}", 358 | "received_events_url": "https://api.github.com/users/roland/received_events", 359 | "type": "User", 360 | "site_admin": false 361 | }, 362 | { 363 | "login": "lukas", 364 | "id": 29, 365 | "node_id": "MDQ6VXNlcjI5", 366 | "avatar_url": "https://avatars2.githubusercontent.com/u/29?v=4", 367 | "gravatar_id": "", 368 | "url": "https://api.github.com/users/lukas", 369 | "html_url": "https://github.com/lukas", 370 | "followers_url": "https://api.github.com/users/lukas/followers", 371 | "following_url": "https://api.github.com/users/lukas/following{/other_user}", 372 | "gists_url": "https://api.github.com/users/lukas/gists{/gist_id}", 373 | "starred_url": "https://api.github.com/users/lukas/starred{/owner}{/repo}", 374 | "subscriptions_url": "https://api.github.com/users/lukas/subscriptions", 375 | "organizations_url": "https://api.github.com/users/lukas/orgs", 376 | "repos_url": "https://api.github.com/users/lukas/repos", 377 | "events_url": "https://api.github.com/users/lukas/events{/privacy}", 378 | "received_events_url": "https://api.github.com/users/lukas/received_events", 379 | "type": "User", 380 | "site_admin": false 381 | }, 382 | { 383 | "login": "fanvsfan", 384 | "id": 30, 385 | "node_id": "MDQ6VXNlcjMw", 386 | "avatar_url": "https://avatars2.githubusercontent.com/u/30?v=4", 387 | "gravatar_id": "", 388 | "url": "https://api.github.com/users/fanvsfan", 389 | "html_url": "https://github.com/fanvsfan", 390 | "followers_url": "https://api.github.com/users/fanvsfan/followers", 391 | "following_url": "https://api.github.com/users/fanvsfan/following{/other_user}", 392 | "gists_url": "https://api.github.com/users/fanvsfan/gists{/gist_id}", 393 | "starred_url": "https://api.github.com/users/fanvsfan/starred{/owner}{/repo}", 394 | "subscriptions_url": "https://api.github.com/users/fanvsfan/subscriptions", 395 | "organizations_url": "https://api.github.com/users/fanvsfan/orgs", 396 | "repos_url": "https://api.github.com/users/fanvsfan/repos", 397 | "events_url": "https://api.github.com/users/fanvsfan/events{/privacy}", 398 | "received_events_url": "https://api.github.com/users/fanvsfan/received_events", 399 | "type": "User", 400 | "site_admin": false 401 | }, 402 | { 403 | "login": "tomtt", 404 | "id": 31, 405 | "node_id": "MDQ6VXNlcjMx", 406 | "avatar_url": "https://avatars2.githubusercontent.com/u/31?v=4", 407 | "gravatar_id": "", 408 | "url": "https://api.github.com/users/tomtt", 409 | "html_url": "https://github.com/tomtt", 410 | "followers_url": "https://api.github.com/users/tomtt/followers", 411 | "following_url": "https://api.github.com/users/tomtt/following{/other_user}", 412 | "gists_url": "https://api.github.com/users/tomtt/gists{/gist_id}", 413 | "starred_url": "https://api.github.com/users/tomtt/starred{/owner}{/repo}", 414 | "subscriptions_url": "https://api.github.com/users/tomtt/subscriptions", 415 | "organizations_url": "https://api.github.com/users/tomtt/orgs", 416 | "repos_url": "https://api.github.com/users/tomtt/repos", 417 | "events_url": "https://api.github.com/users/tomtt/events{/privacy}", 418 | "received_events_url": "https://api.github.com/users/tomtt/received_events", 419 | "type": "User", 420 | "site_admin": false 421 | }, 422 | { 423 | "login": "railsjitsu", 424 | "id": 32, 425 | "node_id": "MDQ6VXNlcjMy", 426 | "avatar_url": "https://avatars2.githubusercontent.com/u/32?v=4", 427 | "gravatar_id": "", 428 | "url": "https://api.github.com/users/railsjitsu", 429 | "html_url": "https://github.com/railsjitsu", 430 | "followers_url": "https://api.github.com/users/railsjitsu/followers", 431 | "following_url": "https://api.github.com/users/railsjitsu/following{/other_user}", 432 | "gists_url": "https://api.github.com/users/railsjitsu/gists{/gist_id}", 433 | "starred_url": "https://api.github.com/users/railsjitsu/starred{/owner}{/repo}", 434 | "subscriptions_url": "https://api.github.com/users/railsjitsu/subscriptions", 435 | "organizations_url": "https://api.github.com/users/railsjitsu/orgs", 436 | "repos_url": "https://api.github.com/users/railsjitsu/repos", 437 | "events_url": "https://api.github.com/users/railsjitsu/events{/privacy}", 438 | "received_events_url": "https://api.github.com/users/railsjitsu/received_events", 439 | "type": "User", 440 | "site_admin": false 441 | }, 442 | { 443 | "login": "nitay", 444 | "id": 34, 445 | "node_id": "MDQ6VXNlcjM0", 446 | "avatar_url": "https://avatars2.githubusercontent.com/u/34?v=4", 447 | "gravatar_id": "", 448 | "url": "https://api.github.com/users/nitay", 449 | "html_url": "https://github.com/nitay", 450 | "followers_url": "https://api.github.com/users/nitay/followers", 451 | "following_url": "https://api.github.com/users/nitay/following{/other_user}", 452 | "gists_url": "https://api.github.com/users/nitay/gists{/gist_id}", 453 | "starred_url": "https://api.github.com/users/nitay/starred{/owner}{/repo}", 454 | "subscriptions_url": "https://api.github.com/users/nitay/subscriptions", 455 | "organizations_url": "https://api.github.com/users/nitay/orgs", 456 | "repos_url": "https://api.github.com/users/nitay/repos", 457 | "events_url": "https://api.github.com/users/nitay/events{/privacy}", 458 | "received_events_url": "https://api.github.com/users/nitay/received_events", 459 | "type": "User", 460 | "site_admin": false 461 | }, 462 | { 463 | "login": "kevwil", 464 | "id": 35, 465 | "node_id": "MDQ6VXNlcjM1", 466 | "avatar_url": "https://avatars2.githubusercontent.com/u/35?v=4", 467 | "gravatar_id": "", 468 | "url": "https://api.github.com/users/kevwil", 469 | "html_url": "https://github.com/kevwil", 470 | "followers_url": "https://api.github.com/users/kevwil/followers", 471 | "following_url": "https://api.github.com/users/kevwil/following{/other_user}", 472 | "gists_url": "https://api.github.com/users/kevwil/gists{/gist_id}", 473 | "starred_url": "https://api.github.com/users/kevwil/starred{/owner}{/repo}", 474 | "subscriptions_url": "https://api.github.com/users/kevwil/subscriptions", 475 | "organizations_url": "https://api.github.com/users/kevwil/orgs", 476 | "repos_url": "https://api.github.com/users/kevwil/repos", 477 | "events_url": "https://api.github.com/users/kevwil/events{/privacy}", 478 | "received_events_url": "https://api.github.com/users/kevwil/received_events", 479 | "type": "User", 480 | "site_admin": false 481 | }, 482 | { 483 | "login": "KirinDave", 484 | "id": 36, 485 | "node_id": "MDQ6VXNlcjM2", 486 | "avatar_url": "https://avatars2.githubusercontent.com/u/36?v=4", 487 | "gravatar_id": "", 488 | "url": "https://api.github.com/users/KirinDave", 489 | "html_url": "https://github.com/KirinDave", 490 | "followers_url": "https://api.github.com/users/KirinDave/followers", 491 | "following_url": "https://api.github.com/users/KirinDave/following{/other_user}", 492 | "gists_url": "https://api.github.com/users/KirinDave/gists{/gist_id}", 493 | "starred_url": "https://api.github.com/users/KirinDave/starred{/owner}{/repo}", 494 | "subscriptions_url": "https://api.github.com/users/KirinDave/subscriptions", 495 | "organizations_url": "https://api.github.com/users/KirinDave/orgs", 496 | "repos_url": "https://api.github.com/users/KirinDave/repos", 497 | "events_url": "https://api.github.com/users/KirinDave/events{/privacy}", 498 | "received_events_url": "https://api.github.com/users/KirinDave/received_events", 499 | "type": "User", 500 | "site_admin": false 501 | }, 502 | { 503 | "login": "jamesgolick", 504 | "id": 37, 505 | "node_id": "MDQ6VXNlcjM3", 506 | "avatar_url": "https://avatars2.githubusercontent.com/u/37?v=4", 507 | "gravatar_id": "", 508 | "url": "https://api.github.com/users/jamesgolick", 509 | "html_url": "https://github.com/jamesgolick", 510 | "followers_url": "https://api.github.com/users/jamesgolick/followers", 511 | "following_url": "https://api.github.com/users/jamesgolick/following{/other_user}", 512 | "gists_url": "https://api.github.com/users/jamesgolick/gists{/gist_id}", 513 | "starred_url": "https://api.github.com/users/jamesgolick/starred{/owner}{/repo}", 514 | "subscriptions_url": "https://api.github.com/users/jamesgolick/subscriptions", 515 | "organizations_url": "https://api.github.com/users/jamesgolick/orgs", 516 | "repos_url": "https://api.github.com/users/jamesgolick/repos", 517 | "events_url": "https://api.github.com/users/jamesgolick/events{/privacy}", 518 | "received_events_url": "https://api.github.com/users/jamesgolick/received_events", 519 | "type": "User", 520 | "site_admin": false 521 | }, 522 | { 523 | "login": "atmos", 524 | "id": 38, 525 | "node_id": "MDQ6VXNlcjM4", 526 | "avatar_url": "https://avatars3.githubusercontent.com/u/38?v=4", 527 | "gravatar_id": "", 528 | "url": "https://api.github.com/users/atmos", 529 | "html_url": "https://github.com/atmos", 530 | "followers_url": "https://api.github.com/users/atmos/followers", 531 | "following_url": "https://api.github.com/users/atmos/following{/other_user}", 532 | "gists_url": "https://api.github.com/users/atmos/gists{/gist_id}", 533 | "starred_url": "https://api.github.com/users/atmos/starred{/owner}{/repo}", 534 | "subscriptions_url": "https://api.github.com/users/atmos/subscriptions", 535 | "organizations_url": "https://api.github.com/users/atmos/orgs", 536 | "repos_url": "https://api.github.com/users/atmos/repos", 537 | "events_url": "https://api.github.com/users/atmos/events{/privacy}", 538 | "received_events_url": "https://api.github.com/users/atmos/received_events", 539 | "type": "User", 540 | "site_admin": false 541 | }, 542 | { 543 | "login": "errfree", 544 | "id": 44, 545 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjQ0", 546 | "avatar_url": "https://avatars2.githubusercontent.com/u/44?v=4", 547 | "gravatar_id": "", 548 | "url": "https://api.github.com/users/errfree", 549 | "html_url": "https://github.com/errfree", 550 | "followers_url": "https://api.github.com/users/errfree/followers", 551 | "following_url": "https://api.github.com/users/errfree/following{/other_user}", 552 | "gists_url": "https://api.github.com/users/errfree/gists{/gist_id}", 553 | "starred_url": "https://api.github.com/users/errfree/starred{/owner}{/repo}", 554 | "subscriptions_url": "https://api.github.com/users/errfree/subscriptions", 555 | "organizations_url": "https://api.github.com/users/errfree/orgs", 556 | "repos_url": "https://api.github.com/users/errfree/repos", 557 | "events_url": "https://api.github.com/users/errfree/events{/privacy}", 558 | "received_events_url": "https://api.github.com/users/errfree/received_events", 559 | "type": "Organization", 560 | "site_admin": false 561 | }, 562 | { 563 | "login": "mojodna", 564 | "id": 45, 565 | "node_id": "MDQ6VXNlcjQ1", 566 | "avatar_url": "https://avatars2.githubusercontent.com/u/45?v=4", 567 | "gravatar_id": "", 568 | "url": "https://api.github.com/users/mojodna", 569 | "html_url": "https://github.com/mojodna", 570 | "followers_url": "https://api.github.com/users/mojodna/followers", 571 | "following_url": "https://api.github.com/users/mojodna/following{/other_user}", 572 | "gists_url": "https://api.github.com/users/mojodna/gists{/gist_id}", 573 | "starred_url": "https://api.github.com/users/mojodna/starred{/owner}{/repo}", 574 | "subscriptions_url": "https://api.github.com/users/mojodna/subscriptions", 575 | "organizations_url": "https://api.github.com/users/mojodna/orgs", 576 | "repos_url": "https://api.github.com/users/mojodna/repos", 577 | "events_url": "https://api.github.com/users/mojodna/events{/privacy}", 578 | "received_events_url": "https://api.github.com/users/mojodna/received_events", 579 | "type": "User", 580 | "site_admin": false 581 | }, 582 | { 583 | "login": "bmizerany", 584 | "id": 46, 585 | "node_id": "MDQ6VXNlcjQ2", 586 | "avatar_url": "https://avatars2.githubusercontent.com/u/46?v=4", 587 | "gravatar_id": "", 588 | "url": "https://api.github.com/users/bmizerany", 589 | "html_url": "https://github.com/bmizerany", 590 | "followers_url": "https://api.github.com/users/bmizerany/followers", 591 | "following_url": "https://api.github.com/users/bmizerany/following{/other_user}", 592 | "gists_url": "https://api.github.com/users/bmizerany/gists{/gist_id}", 593 | "starred_url": "https://api.github.com/users/bmizerany/starred{/owner}{/repo}", 594 | "subscriptions_url": "https://api.github.com/users/bmizerany/subscriptions", 595 | "organizations_url": "https://api.github.com/users/bmizerany/orgs", 596 | "repos_url": "https://api.github.com/users/bmizerany/repos", 597 | "events_url": "https://api.github.com/users/bmizerany/events{/privacy}", 598 | "received_events_url": "https://api.github.com/users/bmizerany/received_events", 599 | "type": "User", 600 | "site_admin": false 601 | } 602 | ] -------------------------------------------------------------------------------- /wiremock/mappings/users/detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/users/andr3a88", 5 | "headers": { 6 | "Content-Type": { 7 | "equalTo": "application/json" 8 | } 9 | } 10 | }, 11 | "response": { 12 | "status": 200, 13 | "bodyFileName": "users/detail.json", 14 | "headers": { 15 | "Content-Type": "application/json" 16 | } 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /wiremock/mappings/users/search.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "urlPath": "/search/users", 5 | "headers": { 6 | "Content-Type": { 7 | "equalTo": "application/json" 8 | } 9 | }, 10 | "queryParameters": { 11 | "q": { 12 | "matches": "[a-zA-Z0-9_]+" 13 | }, 14 | "per_page": { 15 | "matches": "[a-zA-Z0-9_.]+" 16 | }, 17 | "page": { 18 | "matches": "[a-zA-Z0-9_.]+" 19 | } 20 | } 21 | }, 22 | "response": { 23 | "status": 200, 24 | "bodyFileName": "users/search.json", 25 | "headers": { 26 | "Content-Type": "application/json" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /wiremock/mappings/users/users.json: -------------------------------------------------------------------------------- 1 | { 2 | "request": { 3 | "method": "GET", 4 | "url": "/users", 5 | "headers": { 6 | "Content-Type": { 7 | "equalTo": "application/json" 8 | } 9 | } 10 | }, 11 | "response": { 12 | "status": 200, 13 | "bodyFileName": "users/users.json", 14 | "headers": { 15 | "Content-Type": "application/json" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /wiremock/start_server.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | VERSION=2.22.0 4 | PORT=3900 5 | 6 | if ! [ -f "wiremock-standalone-$VERSION.jar" ]; 7 | then 8 | curl -O http://repo1.maven.org/maven2/com/github/tomakehurst/wiremock-standalone/$VERSION/wiremock-standalone-$VERSION.jar 9 | fi 10 | 11 | java -jar wiremock-standalone-$VERSION.jar --port $PORT --verbose 12 | -------------------------------------------------------------------------------- /wiremock/wiremock-standalone-2.22.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andr3a88/TryNetworkLayer/0bc204de70af8eaeee9d8dd661ef14d694123712/wiremock/wiremock-standalone-2.22.0.jar --------------------------------------------------------------------------------