From 4d243885bbd5a050b0d27dbe04852ac91bdcbc45 Mon Sep 17 00:00:00 2001 From: Paul Cantrell Date: Mon, 14 Nov 2016 20:57:22 -0600 Subject: [PATCH] Doc, logging, other minor refinements in example project --- .../GithubBrowser.xcodeproj/project.pbxproj | 11 ++-- Examples/GithubBrowser/README.md | 16 ++--- .../GithubBrowser/Source/API/GithubAPI.swift | 65 ++++++++++++++----- Source/Siesta/Support/Logging.swift | 3 +- 4 files changed, 64 insertions(+), 31 deletions(-) diff --git a/Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj b/Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj index 01807ab1..78f18b15 100644 --- a/Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj +++ b/Examples/GithubBrowser/GithubBrowser.xcodeproj/project.pbxproj @@ -17,7 +17,7 @@ DA7462471B4C768B00406D67 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA7462451B4C768B00406D67 /* LaunchScreen.storyboard */; }; DAAC4E461D3AAD1B00FB3CE2 /* RepositoryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAC4E451D3AAD1B00FB3CE2 /* RepositoryViewController.swift */; }; DAE27A801D3F1EF400D757F9 /* Optional+GithubBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE27A7F1D3F1EF400D757F9 /* Optional+GithubBrowser.swift */; }; - DAE2F84A1B94F10500D2AD96 /* GithubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */; }; + DAE2F84A1B94F10500D2AD96 /* GitHubAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */; }; DAE2F84C1B94F10500D2AD96 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8471B94F10500D2AD96 /* AppDelegate.swift */; }; DAE2F84D1B94F10500D2AD96 /* RepositoryListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8481B94F10500D2AD96 /* RepositoryListViewController.swift */; }; DAE2F84E1B94F10500D2AD96 /* UserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE2F8491B94F10500D2AD96 /* UserViewController.swift */; }; @@ -38,7 +38,7 @@ DA7462481B4C768B00406D67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DAAC4E451D3AAD1B00FB3CE2 /* RepositoryViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryViewController.swift; sourceTree = ""; }; DAE27A7F1D3F1EF400D757F9 /* Optional+GithubBrowser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Optional+GithubBrowser.swift"; sourceTree = ""; }; - DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GithubAPI.swift; sourceTree = ""; }; + DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GitHubAPI.swift; sourceTree = ""; }; DAE2F8471B94F10500D2AD96 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; DAE2F8481B94F10500D2AD96 /* RepositoryListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RepositoryListViewController.swift; sourceTree = ""; }; DAE2F8491B94F10500D2AD96 /* UserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserViewController.swift; sourceTree = ""; }; @@ -134,7 +134,7 @@ DAE2F8431B94F10500D2AD96 /* API */ = { isa = PBXGroup; children = ( - DAE2F8441B94F10500D2AD96 /* GithubAPI.swift */, + DAE2F8441B94F10500D2AD96 /* GitHubAPI.swift */, ); path = API; sourceTree = ""; @@ -282,7 +282,7 @@ DA2B556A1C7EDE7B00EB4D67 /* LoginViewController.swift in Sources */, DA2B55621C7D6F8700EB4D67 /* User.swift in Sources */, DAE2F84C1B94F10500D2AD96 /* AppDelegate.swift in Sources */, - DAE2F84A1B94F10500D2AD96 /* GithubAPI.swift in Sources */, + DAE2F84A1B94F10500D2AD96 /* GitHubAPI.swift in Sources */, DAE27A801D3F1EF400D757F9 /* Optional+GithubBrowser.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -350,6 +350,7 @@ IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_SWIFT_FLAGS = "-D DEBUG"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; @@ -389,6 +390,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; + OTHER_SWIFT_FLAGS = "-D DEBUG"; SDKROOT = iphoneos; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; VALIDATE_PRODUCT = YES; @@ -402,7 +404,6 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; INFOPLIST_FILE = Source/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - "OTHER_SWIFT_FLAGS[arch=*]" = "-DDEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.bustoutsolutions.GithubBrowser; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 3.0; diff --git a/Examples/GithubBrowser/README.md b/Examples/GithubBrowser/README.md index eac7265f..e88eb5f2 100644 --- a/Examples/GithubBrowser/README.md +++ b/Examples/GithubBrowser/README.md @@ -1,8 +1,8 @@ # Siesta Example Project -This app allows you to type a Github username and see the user’s name, avatar, and repos. +This app allows you to search and view GitHub repositories. -This is a simple app, and intentionally minimizes things outside of Siesta’s purview: minimal models, minimal functionality, and bare bones UI. (Well, there is the gratuitous use of the Siesta color scheme!) +It is a simple app, and intentionally minimizes things outside of Siesta’s purview: minimal models, minimal functionality, and bare bones UI. (Well, there is the gratuitous use of the Siesta color scheme!) ## What’s interesting about it? @@ -19,24 +19,24 @@ Siesta solves all these problems transparently, with minimal code. ## Files of note -- `Source/API/GithubAPI.swift` shows how to: - +- `Source/API/GitHubAPI.swift` shows how to: + - set up a Siesta service, - send an authentication header, and - add a custom response transformers that: - wrap all JSON responses with SwiftyJSON, - map endpoints to models, and - - replace Siesta’s default error messages with Github-provided messages when present. + - replace Siesta’s default error messages with GitHub-provided messages when present. - `Source/UI/UserViewController.swift` shows how to: - + - use Siesta to propagate changes from a Resource to a UI, - retarget a view controller at different Resources while it is visible, - use `ResourceStatusOverlay` to show a spinner and default error message, and - use Siesta’s caching, throttling, and delayed cancellation to manage a rapid series of requests triggered by keystrokes. - `Source/UI/RepositoryListViewController.swift` shows how to: - + - create a view controller which displays a Siesta resource determined by a parent VC and - populate a table view with Siesta. @@ -48,6 +48,6 @@ Siesta solves all these problems transparently, with minimal code. ## Rate limit errors? -If you hit the Github API’s rate limit while running the demo, press the “Log In” button. If you’re experimenting with the demo a lot, you can set `GITHUB_USER` and `GITHUB_PASS` environment variables in the “Run” build scheme to make the app automatically log you in on launch. +If you hit the GitHub API’s rate limit while running the demo, press the “Log In” button. If you’re experimenting with the demo a lot, you can set `GITHUB_USER` and `GITHUB_PASS` environment variables in the “Run” build scheme to make the app automatically log you in on launch. You can use a [personal access token](https://github.com/settings/tokens) in place of your password. You don’t need to grant any permissions to your token for this app; just the public access will do. diff --git a/Examples/GithubBrowser/Source/API/GithubAPI.swift b/Examples/GithubBrowser/Source/API/GithubAPI.swift index 8ceffa17..b58b6452 100644 --- a/Examples/GithubBrowser/Source/API/GithubAPI.swift +++ b/Examples/GithubBrowser/Source/API/GithubAPI.swift @@ -14,17 +14,22 @@ class _GithubAPI { fileprivate init() { #if DEBUG - LogCategory.enabled = [.network, .staleness] - #endif + // Bare-bones logging of which network calls Siesta makes: + LogCategory.enabled = [.network] - // Configuration + // For more info about how Siesta decides whether to make a network call, + // and when it broadcasts state updates to the app: + //LogCategory.enabled = LogCategory.common - service.configure("**") { - // The basicAuthHeader property’s didSet causes this config to be reapplied whenever auth changes. + // For the gory details of what Siesta’s up to: + //LogCategory.enabled = LogCategory.detailed + #endif - $0.headers["Authorization"] = self.basicAuthHeader + // Global configuration - // By default, Siesta parses JSON using Foundation JSONSerialization. This transformer wraps that with SwiftyJSON. + service.configure { + // By default, Siesta parses JSON using Foundation JSONSerialization. + // This transformer wraps that with SwiftyJSON. $0.pipeline[.parsing].add(SwiftyJSONTransformer, contentTypes: ["*/json"]) @@ -34,14 +39,27 @@ class _GithubAPI { $0.pipeline[.cleanup].add(GithubErrorMessageExtractor()) } + // Resource-specific configuration + service.configure("/search/**") { $0.expirationTime = 10 // Refresh search results after 10 seconds (Siesta default is 30) } + // Auth configuration + + // Note the "**" pattern, which makes this config apply only to subpaths of baseURL. + // This prevents accidental credential leakage to untrusted servers. + + service.configure("**") { + // This header configuration gets reapplied whenever the user logs in or out. + // How? See the basicAuthHeader property’s didSet. + + $0.headers["Authorization"] = self.basicAuthHeader + } + // Mapping from specific paths to models service.configureTransformer("/users/*") { - // Swift 3 TODO: see if bare $0 bug is finally fixed, or consider passing struct that still supports $0.content try User(json: $0.content) // Input type inferred because User.init takes JSON } @@ -52,7 +70,8 @@ class _GithubAPI { } service.configureTransformer("/search/repositories") { - try ($0.content as JSON)["items"].arrayValue + try ($0.content as JSON)["items"] + .arrayValue .map(Repository.init) } @@ -61,7 +80,7 @@ class _GithubAPI { } service.configure("/user/starred/*/*") { // Github gives 202 for “starred” and 404 for “not starred.” - $0.pipeline[.model].add( // This custom transformer turns that curious convention into + $0.pipeline[.model].add( // This custom transformer turns that curious convention into TrueIfResourceFoundTransformer()) // a resource whose content is a simple boolean. } @@ -99,8 +118,8 @@ class _GithubAPI { service.wipeResources() // Scrub all unauthenticated data // Note that wipeResources() broadcasts a “no data” event to all observers of all resources. - // Therefore, if your UI diligently observes all the resources it uses, this call prevents sensitive data - // from lingering in the UI after logout. + // Therefore, if your UI diligently observes all the resources it displays, this call prevents sensitive + // data from lingering in the UI after logout. } } @@ -134,7 +153,9 @@ class _GithubAPI { } func repository(_ repositoryModel: Repository) -> Resource { - return repository(ownedBy: repositoryModel.owner.login, named: repositoryModel.name) + return repository( + ownedBy: repositoryModel.owner.login, + named: repositoryModel.name) } func currentUserStarred(_ repositoryModel: Repository) -> Resource { @@ -149,16 +170,26 @@ class _GithubAPI { return starredResource .request(isStarred ? .put : .delete) .onSuccess { _ in + // Update succeeded. Directly update the locally cached “starred / not starred” state. + starredResource.overrideLocalContent(with: isStarred) - self.repository(repositoryModel).load() // To update star count + + // Ask server for an updated star count. Note that we only need to trigger the load here, not handle + // the response! Any UI that is displaying the star count will be observing this resource, and thus + // will pick up the change. The code that knows _when_ to trigger the load is decoupled from the code + // that knows _what_ to do with the updated data. This is the magic of Siesta. + + self.repository(repositoryModel).load() } } } +/// Wraps all response entity content with SwiftyJSON private let SwiftyJSONTransformer = - ResponseContentTransformer + ResponseContentTransformer(transformErrors: true) { JSON($0.content as AnyObject) } +/// If the response is JSON and has a "message" value, use it as the user-visible error message. private struct GithubErrorMessageExtractor: ResponseTransformer { func process(_ response: Response) -> Response { switch response { @@ -166,12 +197,14 @@ private struct GithubErrorMessageExtractor: ResponseTransformer { return response case .failure(var error): - error.userMessage = error.jsonDict["message"] as? String ?? error.userMessage + let json = error.typedContent(ifNone: JSON.null) + error.userMessage = json["message"].string ?? error.userMessage return .failure(error) } } } +/// Special handling for detecting whether repo is starred; see "/user/starred/*/*" config above private struct TrueIfResourceFoundTransformer: ResponseTransformer { func process(_ response: Response) -> Response { switch response { diff --git a/Source/Siesta/Support/Logging.swift b/Source/Siesta/Support/Logging.swift index f0228c93..56f5450c 100644 --- a/Source/Siesta/Support/Logging.swift +++ b/Source/Siesta/Support/Logging.swift @@ -27,8 +27,7 @@ public enum LogCategory /// `ResourceEvent` broadcast by resources. case stateChanges - /// Detailed information about which events are sent to which observers, when they are added, and when they are - /// removed. + /// Detailed information about when observers are added, when they are removed, and which events they receive. case observers /// Information about how `Resource.loadIfNeeded()` decides whether to initiate a request.