Feature: Swiftlint

This commit is contained in:
Flávio Caetano 2017-10-24 17:25:34 -02:00
parent c3517c32b1
commit 33fa4efc19
23 changed files with 456 additions and 328 deletions

47
.swiftlint.yml Normal file
View File

@ -0,0 +1,47 @@
excluded:
- Carthage
- Example/Pods
disabled_rules:
- nesting
- opening_brace
- statement_position
- weak_delegate
opt_in_rules:
# - array_init # is causing a false positive on `.map { !$0 }`
- closure_end_indentation
- closure_spacing
- contains_over_first_not_nil
- empty_count
- explicit_init
- explicit_top_level_acl
- extension_access_modifier
- fatal_error_message
- file_header
- first_where
- force_unwrapping
- implicit_return
- let_var_whitespace
- literal_expression_end_indentation
- multiline_arguments
- multiple_closures_with_trailing_closure
- operator_usage_whitespace
- redundant_nil_coalescing
- single_test_class
- sorted_imports
# - trailing_closure # causing a false positive on `.subscribe(onNext:)`
- unneeded_parentheses_in_closure_argument
vertical_whitespace:
max_empty_lines: 2
file_header:
required_pattern: |
\/\/
\/\/ .*?\.swift
\/\/ ReCaptcha
\/\/
\/\/ Created by .*? on \d{1,2}\/\d{1,2}\/\d{2}\.
\/\/ Copyright © \d{4} ReCaptcha\. All rights reserved\.
\/\/

View File

@ -6,6 +6,7 @@ before_install:
- pod --version
- pod repo update --silent
- brew outdated carthage || brew upgrade carthage
- brew outdated swiftlint || brew upgrade swiftlint
install:
- pushd Example; pod install; popd;
@ -17,6 +18,9 @@ env:
- DESTINATION="platform=iOS Simulator,OS=9.0,name=iPhone 6"
- DESTINATION="platform=iOS Simulator,OS=8.3,name=iPhone 4S"
before_script:
- swiftlint --strict --path "$TRAVIS_BUILD_DIR"
script:
- xcodebuild -workspace Example/ReCaptcha.xcworkspace -scheme ReCaptcha-Example -destination "$DESTINATION" build | xcpretty
- xcodebuild -workspace Example/ReCaptcha.xcworkspace -scheme ReCaptcha-Example -destination "$DESTINATION" test | xcpretty

View File

@ -8,6 +8,7 @@ inhibit_all_warnings!
target 'ReCaptcha_Example' do
pod 'ReCaptcha/RxSwift', :path => '../'
pod 'RxCocoa', '~> 3.0'
pod 'SwiftLint', '~> 0.23'
target 'ReCaptcha_Tests' do
inherit! :search_paths

View File

@ -1,19 +1,21 @@
PODS:
- AppSwizzle (1.1.2)
- ReCaptcha/Core (0.2.0):
- ReCaptcha/Core (0.3.0):
- Result (~> 3.0)
- ReCaptcha/RxSwift (0.2.0):
- ReCaptcha/RxSwift (0.3.0):
- ReCaptcha/Core
- RxSwift (~> 3.0)
- Result (3.2.4)
- RxCocoa (3.6.1):
- RxSwift (~> 3.6)
- RxSwift (3.6.1)
- SwiftLint (0.23.1)
DEPENDENCIES:
- AppSwizzle (~> 1.1)
- ReCaptcha/RxSwift (from `../`)
- RxCocoa (~> 3.0)
- SwiftLint (~> 0.23)
EXTERNAL SOURCES:
ReCaptcha:
@ -21,11 +23,12 @@ EXTERNAL SOURCES:
SPEC CHECKSUMS:
AppSwizzle: 45d1a9d71821e7e1bb3a2a503c2979ca6fe4e46d
ReCaptcha: d39a6bac7d877a2541d5e676e9f8c194a7d6c928
ReCaptcha: 0ecf1105cae5040557d8b470909dda5196853c15
Result: d2d07204ce72856f1fd9130bbe42c35a7b0fea10
RxCocoa: 84a08739ab186248c7f31ce4ee92d6f8a947d690
RxSwift: f9de85ea20cd2f7716ee5409fc13523dc638e4e4
SwiftLint: 1b670ce79284c76520f84060e87d645078fd32fa
PODFILE CHECKSUM: 8feb0da0c7db4d53374bb19369e025e179015e8b
PODFILE CHECKSUM: 02f550962dfe5b54072b9091c41d29819d67c422
COCOAPODS: 1.3.1

View File

@ -57,6 +57,7 @@
F206BAD41F8D3FEB00A25807 /* Cartfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cartfile; path = ../Cartfile; sourceTree = "<group>"; };
F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = CHANGELOG.md; path = ../CHANGELOG.md; sourceTree = "<group>"; };
F288E9441F9537760018688D /* ReCaptchaError+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReCaptchaError+Equatable.swift"; sourceTree = "<group>"; };
F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text; name = .swiftlint.yml; path = ../.swiftlint.yml; sourceTree = "<group>"; };
F2E2685D1F7AEE3400CD876D /* ReCaptcha__Tests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReCaptcha__Tests.swift; sourceTree = "<group>"; };
F2ECCF761E9FC47B0097B199 /* ReCaptcha_Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ReCaptcha_Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
F2ECCF7A1E9FC47B0097B199 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -143,6 +144,7 @@
F21901D91F98D62F00D8E2C9 /* CHANGELOG.md */,
F2ECCF871E9FCE930097B199 /* .travis.yml */,
F2ECCF8F1EA008C20097B199 /* codecov.yml */,
F2C0FD7F1F9F8111009B7A6F /* .swiftlint.yml */,
);
name = "Podspec Metadata";
sourceTree = "<group>";
@ -220,6 +222,7 @@
607FACCE1AFB9204008FA782 /* Resources */,
8F03FFB3F5C55E873C23C682 /* [CP] Embed Pods Frameworks */,
ED1C0E07490C9C4B4A401061 /* [CP] Copy Pods Resources */,
F2C0FD7E1F9F8061009B7A6F /* SwiftLint */,
);
buildRules = (
);
@ -425,6 +428,20 @@
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ReCaptcha_Example/Pods-ReCaptcha_Example-resources.sh\"\n";
showEnvVarsInLog = 0;
};
F2C0FD7E1F9F8061009B7A6F /* SwiftLint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = SwiftLint;
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n \"${PODS_ROOT}/SwiftLint/swiftlint\" --path \"${PROJECT_DIR}/..\"\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */

View File

@ -0,0 +1,2 @@
disabled_rules:
- explicit_top_level_acl

View File

@ -2,8 +2,8 @@
// AppDelegate.swift
// ReCaptcha
//
// Created by Flávio Caetano on 03/22/2017.
// Copyright (c) 2017 ReCaptcha. All rights reserved.
// Created by Flávio Caetano on 03/22/17.
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import UIKit
@ -14,33 +14,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?
) -> Bool {
// Override point for customization after application launch.
return true
}
func applicationWillResignActive(_ application: UIApplication) {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
func applicationDidEnterBackground(_ application: UIApplication) {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
func applicationDidBecomeActive(_ application: UIApplication) {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
func applicationWillTerminate(_ application: UIApplication) {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}
}

View File

@ -2,44 +2,45 @@
// ViewController.swift
// ReCaptcha
//
// Created by Flávio Caetano on 03/22/2017.
// Copyright (c) 2017 ReCaptcha. All rights reserved.
// Created by Flávio Caetano on 03/22/17.
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import UIKit
import ReCaptcha
import Result
import RxSwift
import RxCocoa
import RxSwift
import UIKit
class ViewController: UIViewController {
fileprivate static let webViewTag = 123
// swiftlint:disable force_try
fileprivate let recaptcha = try! ReCaptcha()
fileprivate var disposeBag = DisposeBag()
@IBOutlet weak var label: UILabel!
@IBOutlet weak var spinner: UIActivityIndicatorView!
override func viewDidLoad() {
super.viewDidLoad()
recaptcha.configureWebView { [weak self] webview in
webview.frame = self?.view.bounds ?? CGRect.zero
webview.tag = ViewController.webViewTag
}
}
@IBAction func didPressButton(button: UIButton) {
disposeBag = DisposeBag()
let validate = recaptcha.rx.validate(on: view)
.debug("validate")
.share()
let isLoading = validate
.map { _ in false }
.startWith(true)
@ -53,7 +54,7 @@ class ViewController: UIViewController {
.catchErrorJustReturn(false)
.bind(to: button.rx.isEnabled)
.disposed(by: disposeBag)
validate
.map { [weak self] _ in
self?.view.viewWithTag(ViewController.webViewTag)
@ -62,11 +63,10 @@ class ViewController: UIViewController {
subview?.removeFromSuperview()
})
.disposed(by: disposeBag)
validate
.map { try $0.dematerialize() }
.bind(to: label.rx.text)
.disposed(by: disposeBag)
}
}

View File

@ -0,0 +1,5 @@
disabled_rules:
- type_name
- nesting
- force_unwrapping
- explicit_top_level_acl

View File

@ -8,145 +8,145 @@
@testable import ReCaptcha
import XCTest
import WebKit
import XCTest
class ReCaptchaDecoder__Tests: XCTestCase {
fileprivate typealias Result = ReCaptchaDecoder.Result
fileprivate var assertResult: ((Result) -> Void)?
fileprivate var decoder: ReCaptchaDecoder!
override func setUp() {
super.setUp()
decoder = ReCaptchaDecoder { [weak self] result in
self?.assertResult?(result)
}
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func test__Send_Error() {
let exp = expectation(description: "send message")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let err = ReCaptchaError.random()
decoder.send(error: err)
waitForExpectations(timeout: 1)
// Check
XCTAssertNotNil(result)
XCTAssertEqual(result?.error, err)
XCTAssertNil(result?.token)
XCTAssertFalse(result!.showReCaptcha)
}
func test__Decode__Wrong_Format() {
let exp = expectation(description: "send unsupported message")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let message = MockMessage(message: "foobar")
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertEqual(result?.error, .wrongMessageFormat)
XCTAssertNil(result?.token)
XCTAssertFalse(result!.showReCaptcha)
}
func test__Decode__Undefined() {
let exp = expectation(description: "send message with undefined body")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let message = MockMessage(message: ["foo": "bar"])
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertEqual(result?.error, .wrongMessageFormat)
XCTAssertNil(result?.token)
XCTAssertFalse(result!.showReCaptcha)
}
func test__Decode__ShowReCaptcha() {
let exp = expectation(description: "send message with undefined body")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let message = MockMessage(message: ["action": "showReCaptcha"])
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertNil(result?.error)
XCTAssertNil(result?.token)
XCTAssertTrue(result!.showReCaptcha)
}
func test__Decode__Token() {
let exp = expectation(description: "send message with undefined body")
var result: Result?
assertResult = { res in
result = res
exp.fulfill()
}
// Send
let token = String(arc4random())
let message = MockMessage(message: ["token": token])
decoder.send(message: message)
waitForExpectations(timeout: 1)
// Check
XCTAssertNil(result?.error)
XCTAssertEqual(result?.token, token)
@ -159,9 +159,9 @@ class MockMessage: WKScriptMessage {
override var body: Any {
return storedBody
}
fileprivate let storedBody: Any
init(message: Any) {
storedBody = message
}
@ -182,12 +182,12 @@ extension ReCaptchaDecoder.Result {
guard case .token(let token) = self else { return nil }
return token
}
var showReCaptcha: Bool {
guard case .showReCaptcha = self else { return false }
return true
}
var error: ReCaptchaError? {
guard case .error(let error) = self else { return nil }
return error

View File

@ -8,133 +8,133 @@
@testable import ReCaptcha
import XCTest
import Result
import WebKit
import XCTest
class ReCaptchaWebViewManager__Tests: XCTestCase {
fileprivate var apiKey: String!
fileprivate var presenterView: UIView!
override func setUp() {
super.setUp()
presenterView = UIApplication.shared.keyWindow!
apiKey = String(arc4random())
}
override func tearDown() {
presenterView = nil
apiKey = nil
super.tearDown()
}
// MARK: Validate
func test__Validate__Token() {
let exp1 = expectation(description: "load token")
var result1: ReCaptchaWebViewManager.Response?
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
manager.validate(on: presenterView) { response in
result1 = response
exp1.fulfill()
}
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result1)
XCTAssertNil(result1?.error)
XCTAssertEqual(result1?.value, apiKey)
// Validate again
let exp2 = expectation(description: "reload token")
var result2: ReCaptchaWebViewManager.Response?
// Validate
manager.validate(on: presenterView) { response in
result2 = response
exp2.fulfill()
}
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result2)
XCTAssertNil(result2?.error)
XCTAssertEqual(result2?.value, apiKey)
}
func test__Validate__Show_ReCaptcha() {
let exp = expectation(description: "show recaptcha")
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.configureWebView { _ in
exp.fulfill()
}
manager.validate(on: presenterView) { response in
manager.validate(on: presenterView) { _ in
XCTFail("should not call completion")
}
waitForExpectations(timeout: 3)
}
func test__Validate__Message_Error() {
var result: ReCaptchaWebViewManager.Response?
let exp = expectation(description: "message error")
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "\"foobar\"")
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
manager.validate(on: presenterView) { response in
result = response
exp.fulfill()
}
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result)
XCTAssertEqual(result?.error, .wrongMessageFormat)
XCTAssertNil(result?.value)
}
func test__Validate__JS_Error() {
var result: ReCaptchaWebViewManager.Response?
let exp = expectation(description: "js error")
// Validate
let manager = ReCaptchaWebViewManager(messageBody: "foobar")
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
manager.validate(on: presenterView) { response in
result = response
exp.fulfill()
}
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result)
XCTAssertNotNil(result?.error)
@ -147,62 +147,62 @@ class ReCaptchaWebViewManager__Tests: XCTestCase {
XCTFail("Unexpected error received")
}
}
// MARK: Configure WebView
func test__Configure_Web_View__Empty() {
let exp = expectation(description: "configure webview")
// Configure WebView
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.validate(on: presenterView) { response in
manager.validate(on: presenterView) { _ in
XCTFail("should not call completion")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
exp.fulfill()
}
waitForExpectations(timeout: 3)
}
func test__Configure_Web_View() {
let exp = expectation(description: "configure webview")
// Configure WebView
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.configureWebView { [unowned self] webView in
XCTAssertEqual(webView.superview, self.presenterView)
exp.fulfill()
}
manager.validate(on: presenterView) { response in
manager.validate(on: presenterView) { _ in
XCTFail("should not call completion")
}
waitForExpectations(timeout: 3)
}
// MARK: Stop
func test__Stop() {
let exp = expectation(description: "stop loading")
// Stop
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.stop()
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
manager.validate(on: presenterView) { _ in
XCTFail("should not validate")
}
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
exp.fulfill()
}
waitForExpectations(timeout: 3)
}

View File

@ -6,11 +6,10 @@
// Copyright © 2017 ReCaptcha. All rights reserved.
//
@testable import ReCaptcha
import XCTest
import AppSwizzle
@testable import ReCaptcha
import RxSwift
import XCTest
class ReCaptcha__Tests: XCTestCase {
@ -79,12 +78,22 @@ class ReCaptcha__Tests: XCTestCase {
// Ensures plist key if nil key
let plistKey = "bar"
let config1 = try? ReCaptcha.Config(apiKey: nil, infoPlistKey: plistKey, baseURL: URL(string: "foo"), infoPlistURL: nil)
let config1 = try? ReCaptcha.Config(
apiKey: nil,
infoPlistKey: plistKey,
baseURL: URL(string: "foo"),
infoPlistURL: nil
)
XCTAssertEqual(config1?.apiKey, plistKey)
// Ensures preference of given key over plist entry
let key = "foo"
let config2 = try? ReCaptcha.Config(apiKey: key, infoPlistKey: plistKey, baseURL: URL(string: "foo"), infoPlistURL: nil)
let config2 = try? ReCaptcha.Config(
apiKey: key,
infoPlistKey: plistKey,
baseURL: URL(string: "foo"),
infoPlistURL: nil
)
XCTAssertEqual(config2?.apiKey, key)
}
}

View File

@ -6,17 +6,16 @@
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
@testable import ReCaptcha
import Foundation
extension ReCaptchaError: Equatable {
public static func ==(lhs: ReCaptchaError, rhs: ReCaptchaError) -> Bool {
public static func == (lhs: ReCaptchaError, rhs: ReCaptchaError) -> Bool {
switch (lhs, rhs) {
case (.htmlLoadError, .htmlLoadError),
(.apiKeyNotFound, .apiKeyNotFound),
(.baseURLNotFound, .baseURLNotFound),
(.wrongMessageFormat, .wrongMessageFormat):
case (.htmlLoadError, .htmlLoadError),
(.apiKeyNotFound, .apiKeyNotFound),
(.baseURLNotFound, .baseURLNotFound),
(.wrongMessageFormat, .wrongMessageFormat):
return true
case (.unexpected(let lhe as NSError), .unexpected(let rhe as NSError)):
return lhe == rhe

View File

@ -6,9 +6,8 @@
// Copyright © 2017 ReCaptcha. All rights reserved.
//
@testable import ReCaptcha
import Foundation
@testable import ReCaptcha
extension ReCaptchaWebViewManager {
@ -18,7 +17,7 @@ extension ReCaptchaWebViewManager {
.path(forResource: "mock", ofType: "html")
.flatMap { try? String(contentsOfFile: $0) }
.map { String(format: $0, arguments: ["message": messageBody]) }
self.init(
html: html!,
apiKey: apiKey ?? String(arc4random()),

View File

@ -14,7 +14,7 @@ extension Result {
guard case .success(let value) = self else { return nil }
return value
}
var error: Error? {
guard case .failure(let error) = self else { return nil }
return error

View File

@ -8,153 +8,153 @@
@testable import ReCaptcha
import XCTest
import RxSwift
import XCTest
class ReCaptcha_Rx__Tests: XCTestCase {
fileprivate var disposeBag: DisposeBag!
fileprivate var apiKey: String!
fileprivate var presenterView: UIView!
override func setUp() {
super.setUp()
disposeBag = DisposeBag()
presenterView = UIApplication.shared.keyWindow!
apiKey = String(arc4random())
}
override func tearDown() {
disposeBag = nil
presenterView = nil
apiKey = nil
super.tearDown()
}
func test__Validate__Token() {
let manager = ReCaptchaWebViewManager(messageBody: "{token: key}", apiKey: apiKey)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
var result: ReCaptchaWebViewManager.Response?
let exp = expectation(description: "validate token")
// Validate
manager.rx.validate(on: presenterView)
.subscribe { event in
switch event {
case .next(let value):
result = value
case .error(let error):
XCTFail(error.localizedDescription)
case .completed:
exp.fulfill()
}
}
.disposed(by: disposeBag)
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result)
XCTAssertEqual(result?.value, apiKey)
XCTAssertNil(result?.error)
}
func test__Validate__Show_ReCaptcha() {
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}", apiKey: apiKey)
let exp = expectation(description: "show recaptcha")
manager.configureWebView { _ in
exp.fulfill()
}
// Validate
manager.rx.validate(on: presenterView)
.timeout(2, scheduler: MainScheduler.instance)
.subscribe { event in
switch event {
case .next(_):
case .next:
XCTFail("should not have validated")
case .error(let error):
XCTAssertEqual(String(describing: error), RxError.timeout.debugDescription)
case .completed:
XCTFail("should not have completed")
}
}
.disposed(by: disposeBag)
waitForExpectations(timeout: 3)
}
func test__Validate__Error() {
let manager = ReCaptchaWebViewManager(messageBody: "\"foobar\"", apiKey: apiKey)
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
var result: ReCaptchaWebViewManager.Response?
let exp = expectation(description: "validate token")
// Validate
manager.rx.validate(on: presenterView)
.subscribe { event in
switch event {
case .next(let value):
result = value
case .error(let error):
XCTFail(error.localizedDescription)
case .completed:
exp.fulfill()
}
}
.disposed(by: disposeBag)
waitForExpectations(timeout: 3)
// Verify
XCTAssertNotNil(result)
XCTAssertNil(result?.value)
XCTAssertNotNil(result?.error)
XCTAssertEqual(result?.error, .wrongMessageFormat)
}
// MARK: Dispose
func test__Dispose() {
let exp = expectation(description: "stop loading")
// Stop
let manager = ReCaptchaWebViewManager(messageBody: "{action: \"showReCaptcha\"}")
manager.configureWebView { _ in
XCTFail("should not ask to configure the webview")
}
let disposable = manager.rx.validate(on: presenterView)
.subscribe { _ in
XCTFail("should not validate")
}
disposable.dispose()
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
exp.fulfill()
}
waitForExpectations(timeout: 3)
}
}

View File

@ -1,4 +1,4 @@
Copyright (c) 2017 Flávio Caetano <flavio@vieiracaetano.com>
Copyright © 2017 Flávio Caetano <flavio@vieiracaetano.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@ -3,7 +3,7 @@
// ReCaptcha
//
// Created by Flávio Caetano on 22/03/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
@ -22,7 +22,9 @@ open class ReCaptcha: ReCaptchaWebViewManager {
/// The JS API endpoint to be loaded onto the HTML file.
public enum Endpoint {
/// Google's default endpoint. Points to https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit
/** Google's default endpoint. Points to
https://www.google.com/recaptcha/api.js?onload=onloadCallback&render=explicit
*/
case `default`
/// Alternate endpoint. Points to https://www.recaptcha.net/recaptcha/api.js
@ -37,17 +39,18 @@ open class ReCaptcha: ReCaptchaWebViewManager {
}
/** Internal data model for DI in unit tests
*/
*/
struct Config {
/// The raw unformated HTML file content
let html: String
// The API key that will be sent to the ReCaptcha API
/// The API key that will be sent to the ReCaptcha API
let apiKey: String
// The base url to be used to resolve relative URLs in the webview
/// The base url to be used to resolve relative URLs in the webview
let baseURL: URL
/// The Bundle that holds ReCaptcha's assets
private static let bundle: Bundle = {
let bundle = Bundle(for: ReCaptcha.self)
guard let cocoapodsBundle = bundle
@ -60,17 +63,19 @@ open class ReCaptcha: ReCaptchaWebViewManager {
}()
/**
- parameter apiKey: The API key sent to the ReCaptcha init
- parameter infoPlistKey: The API key retrived from the application's Info.plist
- parameter baseURL: The base URL sent to the ReCaptcha init
- parameter infoPlistURL: The base URL retrieved from the application's Info.plist
- parameters:
- apiKey: The API key sent to the ReCaptcha init
- infoPlistKey: The API key retrived from the application's Info.plist
- baseURL: The base URL sent to the ReCaptcha init
- infoPlistURL: The base URL retrieved from the application's Info.plist
- Throws:
- `ReCaptchaError.htmlLoadError` if is unable to load the HTML embedded in the bundle.
- `ReCaptchaError.apiKeyNotFound` if an `apiKey` is not provided and can't find one in the project's Info.plist.
- `ReCaptchaError.baseURLNotFound` if a `baseURL` is not provided and can't find one in the project's Info.plist.
- Rethrows any exceptions thrown by `String(contentsOfFile:)`
*/
- Throws: `ReCaptchaError.htmlLoadError`: if is unable to load the HTML embedded in the bundle.
- Throws: `ReCaptchaError.apiKeyNotFound`: if an `apiKey` is not provided and can't find one in the project's
Info.plist.
- Throws: `ReCaptchaError.baseURLNotFound`: if a `baseURL` is not provided and can't find one in the project's
Info.plist.
- Throws: Rethrows any exceptions thrown by `String(contentsOfFile:)`
*/
public init(apiKey: String?, infoPlistKey: String?, baseURL: URL?, infoPlistURL: URL?) throws {
guard let filePath = Config.bundle.path(forResource: "recaptcha", ofType: "html") else {
throw ReCaptchaError.htmlLoadError
@ -85,30 +90,34 @@ open class ReCaptcha: ReCaptchaWebViewManager {
}
let rawHTML = try String(contentsOfFile: filePath)
self.html = rawHTML
self.apiKey = apiKey
self.baseURL = domain
}
}
/** Initializes a ReCaptcha object
/**
- parameters:
- apiKey: The API key sent to the ReCaptcha init
- infoPlistKey: The API key retrived from the application's Info.plist
- baseURL: The base URL sent to the ReCaptcha init
- infoPlistURL: The base URL retrieved from the application's Info.plist
Both `apiKey` and `baseURL` may be nil, in which case the lib will look for entries of `ReCaptchaKey` and
Initializes a ReCaptcha object
Both `apiKey` and `baseURL` may be nil, in which case the lib will look for entries of `ReCaptchaKey` and
`ReCaptchaDomain`, respectively, in the project's Info.plist
A key may be aquired here: https://www.google.com/recaptcha/admin#list
- parameter apiKey: The API key to be provided to Google's ReCaptcha. Overrides the Info.plist entry. Defaults to `nil`.
- parameter baseURL: A url domain to be load onto the webview. Overrides the Info.plist entry. Defaults to `nil`.
- parameter endpoint: The JS API endpoint to be loaded onto the HTML file. Defaults to `.default`.
- Throws:
- `ReCaptchaError.htmlLoadError` if is unable to load the HTML embedded in the bundle.
- `ReCaptchaError.apiKeyNotFound` if an `apiKey` is not provided and can't find one in the project's Info.plist.
- `ReCaptchaError.baseURLNotFound` if a `baseURL` is not provided and can't find one in the project's Info.plist.
- Rethrows any exceptions thrown by `String(contentsOfFile:)`
*/
- Throws: `ReCaptchaError.htmlLoadError`: if is unable to load the HTML embedded in the bundle.
- Throws: `ReCaptchaError.apiKeyNotFound`: if an `apiKey` is not provided and can't find one in the project's
Info.plist.
- Throws: `ReCaptchaError.baseURLNotFound`: if a `baseURL` is not provided and can't find one in the project's
Info.plist.
- Throws: Rethrows any exceptions thrown by `String(contentsOfFile:)`
*/
public init(apiKey: String? = nil, baseURL: URL? = nil, endpoint: Endpoint = .default) throws {
let infoDict = Bundle.main.infoDictionary

View File

@ -3,7 +3,7 @@
// ReCaptcha
//
// Created by Flávio Caetano on 22/03/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
@ -11,40 +11,41 @@ import WebKit
/** The Decoder of javascript messages from the webview
*/
class ReCaptchaDecoder: NSObject {
*/
internal class ReCaptchaDecoder: NSObject {
/** The decoder result.
- token(String): A result token, if any
- showReCaptcha: Indicates that the webview containing the challenge should be displayed.
- error(NSError): Any errors
*/
*/
enum Result {
/// A result token, if any
case token(String)
/// Indicates that the webview containing the challenge should be displayed.
case showReCaptcha
/// Any errors
case error(ReCaptchaError)
}
/// The closure that receives messages
fileprivate let sendMessage: ((Result) -> Void)
/** Initializes a decoder with a completion closure.
/**
- parameter didReceiveMessage: A closure that receives a ReCaptchaDecoder.Result
Initializes a decoder with a completion closure.
*/
init(didReceiveMessage: @escaping (Result) -> Void) {
sendMessage = didReceiveMessage
super.init()
}
/** Sends an error to the completion closure
/**
- parameter error: The error to be sent.
*/
Sends an error to the completion closure
*/
func send(error: ReCaptchaError) {
sendMessage(.error(error))
}
@ -60,7 +61,7 @@ extension ReCaptchaDecoder: WKScriptMessageHandler {
guard let dict = message.body as? [String: Any] else {
return sendMessage(.error(.wrongMessageFormat))
}
sendMessage(Result.from(response: dict))
}
}
@ -71,20 +72,22 @@ extension ReCaptchaDecoder: WKScriptMessageHandler {
/** Private methods on `ReCaptchaDecoder.Result`
*/
fileprivate extension ReCaptchaDecoder.Result {
/** Parses a dict received from the webview onto a `ReCaptchaDecoder.Result`
/**
- parameter response: A dictionary containing the message to be parsed
- returns: A decoded ReCaptchaDecoder.Result
Parses a dict received from the webview onto a `ReCaptchaDecoder.Result`
*/
static func from(response: [String: Any]) -> ReCaptchaDecoder.Result {
if let token = response["token"] as? String {
return .token(token)
}
if let action = response["action"] as? String, action == "showReCaptcha" {
return .showReCaptcha
}
return .error(.wrongMessageFormat)
}
}

View File

@ -3,7 +3,7 @@
// ReCaptcha
//
// Created by Flávio Caetano on 22/03/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation

View File

@ -3,31 +3,36 @@
// ReCaptcha
//
// Created by Flávio Caetano on 22/03/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
import WebKit
import Result
import WebKit
/** Handles comunications with the webview containing the ReCaptcha challenge.
*/
*/
open class ReCaptchaWebViewManager {
public typealias Response = Result<String, ReCaptchaError>
/** The webview delegate object that performs execution uppon script loading
/** The `webView` delegate object that performs execution uppon script loading
*/
fileprivate class WebViewDelegate: NSObject, WKNavigationDelegate {
/// The parent manager
private weak var manager: ReCaptchaWebViewManager?
/// - parameter manager: The parent manager
init(manager: ReCaptchaWebViewManager) {
self.manager = manager
}
/** Called when the navigation is complete.
- parameter webView: The web view invoking the delegate method.
- parameter navigation: The navigation object that finished.
/**
- parameters:
- webView: The web view invoking the delegate method.
- navigation: The navigation object that finished.
Called when the navigation is complete.
*/
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
manager?.didFinishLoading = true
@ -40,35 +45,50 @@ open class ReCaptchaWebViewManager {
}
}
}
fileprivate struct Constants {
static let ExecuteJSCommand = "execute();"
}
/// Sends the result message
fileprivate var completion: ((Response) -> Void)?
/// Configures the webview for display when required
fileprivate var configureWebView: ((WKWebView) -> Void)?
/// The JS message recoder
fileprivate var decoder: ReCaptchaDecoder!
/// Indicates if the script has already been loaded by the `webView`
fileprivate var didFinishLoading = false // webView.isLoading does not work in this case
/// The observer for `.UIWindowDidBecomeVisible`
fileprivate var observer: NSObjectProtocol?
/// The `webView` delegate implementation
fileprivate lazy var webviewDelegate: WebViewDelegate = {
return WebViewDelegate(manager: self)
WebViewDelegate(manager: self)
}()
/// The webview that executes JS code
fileprivate lazy var webView: WKWebView = {
let webview = WKWebView(frame: CGRect(x: 0, y: 0, width: 1, height: 1), configuration: self.buildConfiguration())
let webview = WKWebView(
frame: CGRect(x: 0, y: 0, width: 1, height: 1),
configuration: self.buildConfiguration()
)
webview.navigationDelegate = self.webviewDelegate
webview.isHidden = true
return webview
}()
/** Initializes the manager
/**
- parameters:
- html: The HTML string to be loaded onto the webview
- apiKey: The Google's ReCaptcha API Key
- baseURL: The URL configured with the API Key
- endpoint: The JS API endpoint to be loaded onto the HTML file.
*/
- html: The HTML string to be loaded onto the webview
- apiKey: The Google's ReCaptcha API Key
- baseURL: The URL configured with the API Key
- endpoint: The JS API endpoint to be loaded onto the HTML file.
*/
init(html: String, apiKey: String, baseURL: URL, endpoint: String) {
decoder = ReCaptchaDecoder { [weak self] result in
self?.handle(result: result)
@ -80,20 +100,25 @@ open class ReCaptchaWebViewManager {
setupWebview(on: window, html: formattedHTML, url: baseURL)
}
else {
NotificationCenter.default.addObserver(forName: .UIWindowDidBecomeVisible, object: nil, queue: nil)
{ [weak self] notification in
observer = NotificationCenter.default.addObserver(
forName: .UIWindowDidBecomeVisible,
object: nil,
queue: nil
) { [weak self] notification in
guard let window = notification.object as? UIWindow else { return }
self?.setupWebview(on: window, html: formattedHTML, url: baseURL)
}
}
}
/** Starts the challenge validation
/**
- parameters:
- view: The view that presents the webview.
- completion: A closure that receives a Result<String, NSError> which may contain a valid result token.
*/
- view: The view that should present the webview.
- completion: A closure that receives a Result<String, NSError> which may contain a valid result token.
Starts the challenge validation
*/
open func validate(on view: UIView, completion: @escaping (Response) -> Void) {
self.completion = completion
@ -103,21 +128,22 @@ open class ReCaptchaWebViewManager {
execute()
}
/// Stops the execution of the webview
open func stop() {
webView.stopLoading()
}
/** Provides a closure to configure the webview for presentation if necessary.
If presentation is required, the webview will already be a subview of `presenterView` if one is provided. Otherwise
it might need to be added in a view currently visible.
- parameter configure: A closure that receives an instance of `WKWebView` for configuration.
*/
/**
- parameter configure: A closure that receives an instance of `WKWebView` for configuration.
Provides a closure to configure the webview for presentation if necessary.
If presentation is required, the webview will already be a subview of `presenterView` if one is provided. Otherwise
it might need to be added in a view currently visible.
*/
open func configureWebView(_ configure: @escaping (WKWebView) -> Void) {
self.configureWebView = configure
}
@ -128,7 +154,7 @@ open class ReCaptchaWebViewManager {
/** Private methods for ReCaptchaWebViewManager
*/
fileprivate extension ReCaptchaWebViewManager {
/** Executes the JS command that loads the ReCaptcha challenge.
This method has no effect if the webview hasn't finished loading.
*/
@ -137,52 +163,61 @@ fileprivate extension ReCaptchaWebViewManager {
// Hasn't finished loading the HTML yet
return
}
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] result, error in
webView.evaluateJavaScript(Constants.ExecuteJSCommand) { [weak self] _, error in
if let error = error {
self?.decoder.send(error: .unexpected(error))
}
}
}
/** Creates a `WKWebViewConfiguration` to be added to the `WKWebView` instance.
/**
- returns: An instance of `WKWebViewConfiguration`
Creates a `WKWebViewConfiguration` to be added to the `WKWebView` instance.
*/
func buildConfiguration() -> WKWebViewConfiguration {
let controller = WKUserContentController()
controller.add(decoder, name: "recaptcha")
let conf = WKWebViewConfiguration()
conf.userContentController = controller
return conf
}
/** Handles the decoder results received from the webview
- Parameter result: A `ReCaptchaDecoder.Result` with the decoded message.
/**
- parameter result: A `ReCaptchaDecoder.Result` with the decoded message.
Handles the decoder results received from the webview
*/
func handle(result: ReCaptchaDecoder.Result) {
switch result {
case .token(let token):
completion?(.success(token))
case .error(let error):
completion?(.failure(error))
case .showReCaptcha:
configureWebView?(webView)
}
}
/** Adds the webview to a valid UIView and loads the initial HTML file
- parameter window: The window in which to add the webview
- parameter html: The embedded HTML file
- parameter url: The base URL given to the webview
*/
/**
- parameters:
- window: The window in which to add the webview
- html: The embedded HTML file
- url: The base URL given to the webview
Adds the webview to a valid UIView and loads the initial HTML file
*/
func setupWebview(on window: UIWindow, html: String, url: URL) {
window.addSubview(webView)
webView.loadHTMLString(html, baseURL: url)
NotificationCenter.default.removeObserver(self)
if let observer = observer {
NotificationCenter.default.removeObserver(observer)
}
}
}

View File

@ -3,34 +3,37 @@
// ReCaptcha
//
// Created by Flávio Caetano on 11/04/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import UIKit
import RxSwift
import UIKit
/// Makes ReCaptchaWebViewManager compatible with RxSwift extensions
extension ReCaptchaWebViewManager: ReactiveCompatible {}
/// Provides a public extension on ReCaptchaWebViewManager that makes it reactive.
public extension Reactive where Base: ReCaptchaWebViewManager {
/** Starts the challenge validation uppon subscription.
/**
- parameter view: The view that should present the webview.
Starts the challenge validation uppon subscription.
The stream's element is a `Result<String, ReCaptchaError>` that may contain a valid token.
Sends `stop()` uppon disposal.
- See:
[ReCaptchaWebViewManager.validate(on:completion:)](../Classes/ReCaptchaWebViewManager.html#/s:9ReCaptcha0aB14WebViewManagerC8validateySo6UIViewC2on_y6ResultAHOySSAA0aB5ErrorOGc10completiontF)
*/
public func validate(on view: UIView) -> Observable<Base.Response> {
- See: `ReCaptchaWebViewManager.validate(on:completion:)`
- See: `ReCaptchaWebViewManager.stop()`
*/
func validate(on view: UIView) -> Observable<Base.Response> {
return Observable<Base.Response>.create { [weak base] (observer: AnyObserver<Base.Response>) in
base?.validate(on: view) { response in
observer.onNext(response)
observer.onCompleted()
}
return Disposables.create { [weak base] in
base?.stop()
}

View File

@ -3,13 +3,27 @@
// ReCaptcha
//
// Created by Flávio Caetano on 10/10/17.
//
// Copyright © 2017 ReCaptcha. All rights reserved.
//
import Foundation
extension String {
/**
- parameters:
- format: The string to be formatted.
- arguments: A dictionary containing the which keys should be replaced by which values.
- returns: A formatted string
Parses a format string using a dictionary of arguments
Replaces occurrences of `"${key}"` with their respective values.
```
String(format: "Hello, ${user}", ["user": "Flavio"]) // Hello, Flavio
```
*/
init(format: String, arguments: [String: CustomStringConvertible]) {
self.init(describing: arguments.reduce(format)
{ (format: String, args: (key: String, value: CustomStringConvertible)) -> String in