From df595dbe28fc4b4f198c3590c5549e2bff57f178 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:07:27 -0400 Subject: [PATCH 1/8] move matching logic over to new class URLMatcher --- Sources/URLMatcher.swift | 186 +++++++++++++++++++++++++ Sources/URLNavigator.swift | 150 +------------------- URLNavigator.xcodeproj/project.pbxproj | 4 + 3 files changed, 194 insertions(+), 146 deletions(-) create mode 100644 Sources/URLMatcher.swift diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift new file mode 100644 index 0000000..64a1c82 --- /dev/null +++ b/Sources/URLMatcher.swift @@ -0,0 +1,186 @@ +// +// URLMatcher.swift +// URLNavigator +// +// Created by Sklar, Josh on 9/2/16. +// Copyright © 2016 Suyeol Jeon. All rights reserved. +// +// The MIT License (MIT) +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import Foundation + +/// URLMatcher provides a way to match URLs against a list of specified patterns. +/// +/// URLMather extracts the pattrn and the values from the URL if possible. +public class URLMatcher { + + // MARK: Initialization + + public init() { + // 🔄 I'm a URLMatcher! + } + + // MARK: Singleton + + public static func defaultMatcher() -> URLMatcher { + struct Shared { + static let defaultMatcher = URLMatcher() + } + return Shared.defaultMatcher + } + + // MARK: Matching + + /// Returns a matching URL pattern and placeholder values from specified URL and URL patterns. Returns `nil` if the + /// URL is not contained in URL patterns. + /// + /// For example: + /// + /// let (URLPattern, values) = URLNavigator.matchURL("myapp://user/123", from: ["myapp://user/"]) + /// + /// The value of the `URLPattern` from an example above is `"myapp://user/"` and the value of the `values` + /// is `["id": 123]`. + /// + /// - Parameter URL: The placeholder-filled URL. + /// - Parameter from: The array of URL patterns. + /// + /// - Returns: A tuple of URL pattern string and a dictionary of URL placeholder values. + public func matchURL(URL: URLConvertible, scheme: String? = nil, + from URLPatterns: [String]) -> (String, [String: AnyObject])? { + let normalizedURLString = self.normalizedURL(URL, scheme: scheme).URLStringValue + let URLPathComponents = normalizedURLString.componentsSeparatedByString("/") // e.g. ["myapp:", "user", "123"] + + outer: for URLPattern in URLPatterns { + // e.g. ["myapp:", "user", ""] + let URLPatternPathComponents = URLPattern.componentsSeparatedByString("/") + let containsPathPlaceholder = URLPatternPathComponents.contains({ $0.hasPrefix(""] + for (i, component) in URLPatternPathComponents.enumerate() { + guard i < URLPathComponents.count else { + continue outer + } + let info = self.placeholderKeyValueFromURLPatternPathComponent(component, + URLPathComponents: URLPathComponents, + atIndex: i + ) + if let key = info?.0, value = info?.1 { + values[key] = value // e.g. ["id": 123] + if component.hasPrefix(" + } + } else if component != URLPathComponents[i] { + continue outer + } + } + + return (URLPattern, values) + } + return nil + } + + // MARK: Utils + + /// Returns an scheme-appended `URLConvertible` if given `URL` doesn't have its scheme. + func URLWithScheme(scheme: String?, _ URL: URLConvertible) -> URLConvertible { + let URLString = URL.URLStringValue + if let scheme = scheme where !URLString.containsString("://") { + #if DEBUG + if !URLPatternString.hasPrefix("/") { + NSLog("[Warning] URL pattern doesn't have leading slash(/): '\(URL)'") + } + #endif + return scheme + ":/" + URLString + } else if scheme == nil && !URLString.containsString("://") { + assertionFailure("Either navigator or URL should have scheme: '\(URL)'") // assert only in debug build + } + return URLString + } + + /// Returns the URL by + /// + /// - Removing redundant trailing slash(/) on scheme + /// - Removing redundant double-slashes(//) + /// - Removing trailing slash(/) + /// + /// - Parameter URL: The dirty URL to be normalized. + /// + /// - Returns: The normalized URL. Returns `nil` if the pecified URL is invalid. + func normalizedURL(dirtyURL: URLConvertible, scheme: String? = nil) -> URLConvertible { + guard dirtyURL.URLValue != nil else { + return dirtyURL + } + var URLString = self.URLWithScheme(scheme, dirtyURL).URLStringValue + URLString = URLString.componentsSeparatedByString("?")[0].componentsSeparatedByString("#")[0] + URLString = self.replaceRegex(":/{3,}", "://", URLString) + URLString = self.replaceRegex("(? (String, AnyObject)? { + guard component.hasPrefix("<") && component.hasSuffix(">") else { + return nil + } + + let start = component.startIndex.advancedBy(1) + let end = component.endIndex.advancedBy(-1) + let placeholder = component[start.." -> "int:id" + + let typeAndKey = placeholder.componentsSeparatedByString(":") // e.g. ["int", "id"] + if typeAndKey.count == 0 { // e.g. component is "<>" + return nil + } + if typeAndKey.count == 1 { // untyped placeholder + return (placeholder, URLPathComponents[index]) + } + + let (type, key) = (typeAndKey[0], typeAndKey[1]) // e.g. ("int", "id") + let value: AnyObject? + switch type { + case "int": value = Int(URLPathComponents[index]) // e.g. 123 + case "float": value = Float(URLPathComponents[index]) // e.g. 123.0 + case "path": value = URLPathComponents[index.. String { + guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { + return string + } + let mutableString = NSMutableString(string: string) + let range = NSMakeRange(0, string.characters.count) + regex.replaceMatchesInString(mutableString, options: [], range: range, withTemplate: repl) + return mutableString as String + } +} diff --git a/Sources/URLNavigator.swift b/Sources/URLNavigator.swift index 68d37e3..249d71e 100644 --- a/Sources/URLNavigator.swift +++ b/Sources/URLNavigator.swift @@ -109,85 +109,28 @@ public class URLNavigator { /// Map an `URLNavigable` to an URL pattern. public func map(URLPattern: URLConvertible, _ navigable: URLNavigable.Type) { - let URLString = URLNavigator.normalizedURL(URLPattern, scheme: self.scheme).URLStringValue + let URLString = URLMatcher.defaultMatcher().normalizedURL(URLPattern, scheme: self.scheme).URLStringValue self.URLMap[URLString] = navigable } /// Map an `URLOpenHandler` to an URL pattern. public func map(URLPattern: URLConvertible, _ handler: URLOpenHandler) { - let URLString = URLNavigator.normalizedURL(URLPattern, scheme: self.scheme).URLStringValue + let URLString = URLMatcher.defaultMatcher().normalizedURL(URLPattern, scheme: self.scheme).URLStringValue self.URLOpenHandlers[URLString] = handler } - - // MARK: Matching URLs - - /// Returns a matching URL pattern and placeholder values from specified URL and URL patterns. Returns `nil` if the - /// URL is not contained in URL patterns. - /// - /// For example: - /// - /// let (URLPattern, values) = URLNavigator.matchURL("myapp://user/123", from: ["myapp://user/"]) - /// - /// The value of the `URLPattern` from an example above is `"myapp://user/"` and the value of the `values` - /// is `["id": 123]`. - /// - /// - Parameter URL: The placeholder-filled URL. - /// - Parameter from: The array of URL patterns. - /// - /// - Returns: A tuple of URL pattern string and a dictionary of URL placeholder values. - static func matchURL(URL: URLConvertible, scheme: String? = nil, - from URLPatterns: [String]) -> (String, [String: AnyObject])? { - let normalizedURLString = URLNavigator.normalizedURL(URL, scheme: scheme).URLStringValue - let URLPathComponents = normalizedURLString.componentsSeparatedByString("/") // e.g. ["myapp:", "user", "123"] - - outer: for URLPattern in URLPatterns { - // e.g. ["myapp:", "user", ""] - let URLPatternPathComponents = URLPattern.componentsSeparatedByString("/") - let containsPathPlaceholder = URLPatternPathComponents.contains({ $0.hasPrefix(""] - for (i, component) in URLPatternPathComponents.enumerate() { - guard i < URLPathComponents.count else { - continue outer - } - let info = self.placeholderKeyValueFromURLPatternPathComponent(component, - URLPathComponents: URLPathComponents, - atIndex: i - ) - if let key = info?.0, value = info?.1 { - values[key] = value // e.g. ["id": 123] - if component.hasPrefix(" - } - } else if component != URLPathComponents[i] { - continue outer - } - } - - return (URLPattern, values) - } - return nil - } - /// Returns a matched view controller from a specified URL. /// /// - Parameter URL: The URL to find view controllers. /// - Returns: A match view controller or `nil` if not matched. public func viewControllerForURL(URL: URLConvertible) -> UIViewController? { - if let (URLPattern, values) = URLNavigator.matchURL(URL, scheme: self.scheme, from: Array(self.URLMap.keys)) { + if let (URLPattern, values) = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: Array(self.URLMap.keys)) { let navigable = self.URLMap[URLPattern] return navigable?.init(URL: URL, values: values) as? UIViewController } return nil } - // MARK: Pushing View Controllers with URL /// Pushes a view controller using `UINavigationController.pushViewController()`. @@ -308,7 +251,7 @@ public class URLNavigator { /// - Returns: The return value of the matching `URLOpenHandler`. Returns `false` if there's no match. public func openURL(URL: URLConvertible) -> Bool { let URLOpenHandlersKeys = Array(self.URLOpenHandlers.keys) - if let (URLPattern, values) = URLNavigator.matchURL(URL, scheme: self.scheme, from: URLOpenHandlersKeys) { + if let (URLPattern, values) = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: URLOpenHandlersKeys) { let handler = self.URLOpenHandlers[URLPattern] if handler?(URL: URL, values: values) == true { return true @@ -316,91 +259,6 @@ public class URLNavigator { } return false } - - - // MARK: Utils - - /// Returns an scheme-appended `URLConvertible` if given `URL` doesn't have its scheme. - static func URLWithScheme(scheme: String?, _ URL: URLConvertible) -> URLConvertible { - let URLString = URL.URLStringValue - if let scheme = scheme where !URLString.containsString("://") { - #if DEBUG - if !URLPatternString.hasPrefix("/") { - NSLog("[Warning] URL pattern doesn't have leading slash(/): '\(URL)'") - } - #endif - return scheme + ":/" + URLString - } else if scheme == nil && !URLString.containsString("://") { - assertionFailure("Either navigator or URL should have scheme: '\(URL)'") // assert only in debug build - } - return URLString - } - - /// Returns the URL by - /// - /// - Removing redundant trailing slash(/) on scheme - /// - Removing redundant double-slashes(//) - /// - Removing trailing slash(/) - /// - /// - Parameter URL: The dirty URL to be normalized. - /// - /// - Returns: The normalized URL. Returns `nil` if the pecified URL is invalid. - static func normalizedURL(dirtyURL: URLConvertible, scheme: String? = nil) -> URLConvertible { - guard dirtyURL.URLValue != nil else { - return dirtyURL - } - var URLString = URLNavigator.URLWithScheme(scheme, dirtyURL).URLStringValue - URLString = URLString.componentsSeparatedByString("?")[0].componentsSeparatedByString("#")[0] - URLString = self.replaceRegex(":/{3,}", "://", URLString) - URLString = self.replaceRegex("(? (String, AnyObject)? { - guard component.hasPrefix("<") && component.hasSuffix(">") else { - return nil - } - - let start = component.startIndex.advancedBy(1) - let end = component.endIndex.advancedBy(-1) - let placeholder = component[start.." -> "int:id" - - let typeAndKey = placeholder.componentsSeparatedByString(":") // e.g. ["int", "id"] - if typeAndKey.count == 0 { // e.g. component is "<>" - return nil - } - if typeAndKey.count == 1 { // untyped placeholder - return (placeholder, URLPathComponents[index]) - } - - let (type, key) = (typeAndKey[0], typeAndKey[1]) // e.g. ("int", "id") - let value: AnyObject? - switch type { - case "int": value = Int(URLPathComponents[index]) // e.g. 123 - case "float": value = Float(URLPathComponents[index]) // e.g. 123.0 - case "path": value = URLPathComponents[index.. String { - guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { - return string - } - let mutableString = NSMutableString(string: string) - let range = NSMakeRange(0, string.characters.count) - regex.replaceMatchesInString(mutableString, options: [], range: range, withTemplate: repl) - return mutableString as String - } - } diff --git a/URLNavigator.xcodeproj/project.pbxproj b/URLNavigator.xcodeproj/project.pbxproj index 285b956..0dc607d 100644 --- a/URLNavigator.xcodeproj/project.pbxproj +++ b/URLNavigator.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 02E016081D79E45E00BB7E25 /* URLMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E016071D79E45E00BB7E25 /* URLMatcher.swift */; }; 03032BB81CCBA94B00F7EBFD /* URLConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03032BB61CCBA93400F7EBFD /* URLConvertibleTests.swift */; }; 03107EDD1C6364E700DF2F14 /* URLNavigator.h in Headers */ = {isa = PBXBuildFile; fileRef = 03107EDA1C6364E700DF2F14 /* URLNavigator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03107EE01C63653900DF2F14 /* URLNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03107EDF1C63653900DF2F14 /* URLNavigator.swift */; }; @@ -65,6 +66,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 02E016071D79E45E00BB7E25 /* URLMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLMatcher.swift; sourceTree = ""; }; 03032BB61CCBA93400F7EBFD /* URLConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLConvertibleTests.swift; sourceTree = ""; }; 03107ECE1C6362E800DF2F14 /* URLNavigator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = URLNavigator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 03107EDA1C6364E700DF2F14 /* URLNavigator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = URLNavigator.h; sourceTree = ""; }; @@ -144,6 +146,7 @@ isa = PBXGroup; children = ( 03107EDA1C6364E700DF2F14 /* URLNavigator.h */, + 02E016071D79E45E00BB7E25 /* URLMatcher.swift */, 03107EDF1C63653900DF2F14 /* URLNavigator.swift */, 03107EE11C6365BF00DF2F14 /* URLNavigable.swift */, 03107EE31C6365E500DF2F14 /* URLConvertible.swift */, @@ -398,6 +401,7 @@ files = ( 03107EE61C6366F500DF2F14 /* UIViewController+TopMostViewController.swift in Sources */, 03107EE01C63653900DF2F14 /* URLNavigator.swift in Sources */, + 02E016081D79E45E00BB7E25 /* URLMatcher.swift in Sources */, 03107EE41C6365E500DF2F14 /* URLConvertible.swift in Sources */, 03107EE21C6365BF00DF2F14 /* URLNavigable.swift in Sources */, ); From fbd3d06a20306365333fa07507d5983c537e2bbe Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:22:41 -0400 Subject: [PATCH 2/8] move tests out of URLNavigator tests --- Tests/URLMatcherInternalTests.swift | 155 ++++++++++++++ Tests/URLMatcherPublicTests.swift | 158 +++++++++++++++ Tests/URLNavigatorInternalTests.swift | 267 ------------------------- URLNavigator.xcodeproj/project.pbxproj | 12 +- 4 files changed, 321 insertions(+), 271 deletions(-) create mode 100644 Tests/URLMatcherInternalTests.swift create mode 100644 Tests/URLMatcherPublicTests.swift delete mode 100644 Tests/URLNavigatorInternalTests.swift diff --git a/Tests/URLMatcherInternalTests.swift b/Tests/URLMatcherInternalTests.swift new file mode 100644 index 0000000..b8240bd --- /dev/null +++ b/Tests/URLMatcherInternalTests.swift @@ -0,0 +1,155 @@ +// +// URLMatcherInternalTests.swift +// URLNavigator +// +// +// Created by Sklar, Josh on 9/2/16. +// Copyright (c) 2016 Suyeol Jeon (xoul.kr) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + +import XCTest +@testable import URLNavigator + +class URLMatcherInternalTests: XCTestCase { + + var matcher: URLMatcher! + + override func setUp() { + super.setUp() + self.matcher = URLMatcher() + } + + func testURLWithScheme() { + XCTAssertEqual(matcher.URLWithScheme(nil, "myapp://user/1").URLStringValue, "myapp://user/1") + XCTAssertEqual(matcher.URLWithScheme("myapp", "/user/1").URLStringValue, "myapp://user/1") + XCTAssertEqual(matcher.URLWithScheme("", "/user/1").URLStringValue, "://user/1") // idiot + } + + func testNormalizedURL() { + XCTAssertEqual(matcher.normalizedURL("myapp://user//hello").URLStringValue, "myapp://user//hello") + XCTAssertEqual(matcher.normalizedURL("myapp:///////user/////hello/??/#abc=/def").URLStringValue, + "myapp://user//hello") + XCTAssertEqual(matcher.normalizedURL("https://").URLStringValue, "https://") + } + + func testPlaceholderValueFromURLPathComponents() { + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["123", "456"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "id") + XCTAssertEqual(placeholder?.1 as? String, "123") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["123", "456"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "id") + XCTAssertEqual(placeholder?.1 as? Int, 123) + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["abc", "456"], + atIndex: 0 + ) + XCTAssertNil(placeholder) + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["180", "456"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "height") + XCTAssertEqual(placeholder?.1 as? Float, 180) + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["abc", "456"], + atIndex: 0 + ) + XCTAssertNil(placeholder) + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["xoul.kr"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["xoul.kr", "resume"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["xoul.kr"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["xoul.kr", "resume"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "xoul.kr/resume") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["google.com", "search?q=test"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "google.com/search?q=test") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["google.com", "search", "?q=test"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "url") + XCTAssertEqual(placeholder?.1 as? String, "google.com/search/?q=test") + }(); + } + + func testReplaceRegex() { + XCTAssertEqual(matcher.replaceRegex("a", "0", "abc"), "0bc") + XCTAssertEqual(matcher.replaceRegex("\\d", "A", "1234567abc098"), "AAAAAAAabcAAA") + } +} diff --git a/Tests/URLMatcherPublicTests.swift b/Tests/URLMatcherPublicTests.swift new file mode 100644 index 0000000..1b10035 --- /dev/null +++ b/Tests/URLMatcherPublicTests.swift @@ -0,0 +1,158 @@ +// +// URLMatcherPublicTests.swift +// URLNavigator +// +// Created by Sklar, Josh on 9/2/16. +// Copyright (c) 2016 Suyeol Jeon (xoul.kr) +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import XCTest +@testable import URLNavigator + +class URLMatcherPublicTests: XCTestCase { + + var matcher: URLMatcher! + + override func setUp() { + super.setUp() + self.matcher = URLMatcher() + } + + func testMatchURL() { + { + XCTAssertNil(matcher.matchURL("myapp://user/1", from: [])) + }(); + { + XCTAssertNil(matcher.matchURL("myapp://user/1", from: ["myapp://comment/"])) + }(); + { + XCTAssertNil(matcher.matchURL("myapp://user/1", from: ["myapp://user//hello"])) + }(); + { + XCTAssertNil(matcher.matchURL("/user/1", scheme: "myapp", from: [])) + }(); + { + XCTAssertNil(matcher.matchURL("/user/1", scheme: "myapp", from: ["myapp://comment/"])) + }(); + { + XCTAssertNil(matcher.matchURL("/user/1", scheme: "myapp", from: ["myapp://user//hello"])) + }(); + { + XCTAssertNil(matcher.matchURL("myapp://user/1", scheme: "myapp", from: [])) + }(); + { + XCTAssertNil(matcher.matchURL("myapp://user/1", scheme: "myapp", from: ["myapp://comment/"])) + }(); + { + XCTAssertNil(matcher.matchURL("myapp://user/1", scheme: "myapp", from: ["myapp://user//hello"])) + }(); + { + let from = ["myapp://hello"] + let (URLPattern, values) = matcher.matchURL("myapp://hello", from: from)! + XCTAssertEqual(URLPattern, "myapp://hello") + XCTAssertEqual(values.count, 0) + + let scheme = matcher.matchURL("/hello", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["myapp://user/"] + let (URLPattern, values) = matcher.matchURL("myapp://user/1", from: from)! + XCTAssertEqual(URLPattern, "myapp://user/") + XCTAssertEqual(values as! [String: String], ["id": "1"]) + + let scheme = matcher.matchURL("/user/1", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["myapp://user/", "myapp://user//hello"] + let (URLPattern, values) = matcher.matchURL("myapp://user/1", from: from)! + XCTAssertEqual(URLPattern, "myapp://user/") + XCTAssertEqual(values as! [String: String], ["id": "1"]) + + let scheme = matcher.matchURL("/user/1", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["myapp://user/", "myapp://user//"] + let (URLPattern, values) = matcher.matchURL("myapp://user/1/posts", from: from)! + XCTAssertEqual(URLPattern, "myapp://user//") + XCTAssertEqual(values as! [String: String], ["id": "1", "object": "posts"]) + + let scheme = matcher.matchURL("/user/1/posts", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["myapp://alert"] + let (URLPattern, values) = matcher.matchURL("myapp://alert?title=hello&message=world", from: from)! + XCTAssertEqual(URLPattern, "myapp://alert") + XCTAssertEqual(values.count, 0) + + let scheme = matcher.matchURL("/alert?title=hello&message=world", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["http://"] + let (URLPattern, values) = matcher.matchURL("http://xoul.kr", from: from)! + XCTAssertEqual(URLPattern, "http://") + XCTAssertEqual(values as! [String: String], ["url": "xoul.kr"]) + + let scheme = matcher.matchURL("http://xoul.kr", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["http://"] + let (URLPattern, values) = matcher.matchURL("http://xoul.kr/resume", from: from)! + XCTAssertEqual(URLPattern, "http://") + XCTAssertEqual(values as! [String: String], ["url": "xoul.kr/resume"]) + + let scheme = matcher.matchURL("http://xoul.kr/resume", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["http://"] + let (URLPattern, values) = matcher.matchURL("http://google.com/search?q=URLNavigator", from: from)! + XCTAssertEqual(URLPattern, "http://") + XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) + + let scheme = matcher.matchURL("http://google.com/search?q=URLNavigator", scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + { + let from = ["http://"] + let (URLPattern, values) = matcher.matchURL("http://google.com/search/?q=URLNavigator", from: from)! + XCTAssertEqual(URLPattern, "http://") + XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) + + let scheme = matcher.matchURL("http://google.com/search/?q=URLNavigator", + scheme: "myapp", from: from)! + XCTAssertEqual(URLPattern, scheme.0) + XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + }(); + } +} diff --git a/Tests/URLNavigatorInternalTests.swift b/Tests/URLNavigatorInternalTests.swift deleted file mode 100644 index ab741b8..0000000 --- a/Tests/URLNavigatorInternalTests.swift +++ /dev/null @@ -1,267 +0,0 @@ -// The MIT License (MIT) -// -// Copyright (c) 2016 Suyeol Jeon (xoul.kr) -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import XCTest -@testable import URLNavigator - -class URLNavigatorInternalTests: XCTestCase { - - func testMatchURL() { - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", from: [])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", from: ["myapp://comment/"])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", from: ["myapp://user//hello"])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("/user/1", scheme: "myapp", from: [])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("/user/1", scheme: "myapp", from: ["myapp://comment/"])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("/user/1", scheme: "myapp", from: ["myapp://user//hello"])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", scheme: "myapp", from: [])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", scheme: "myapp", from: ["myapp://comment/"])) - }(); - { - XCTAssertNil(URLNavigator.matchURL("myapp://user/1", scheme: "myapp", from: ["myapp://user//hello"])) - }(); - { - let from = ["myapp://hello"] - let (URLPattern, values) = URLNavigator.matchURL("myapp://hello", from: from)! - XCTAssertEqual(URLPattern, "myapp://hello") - XCTAssertEqual(values.count, 0) - - let scheme = URLNavigator.matchURL("/hello", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["myapp://user/"] - let (URLPattern, values) = URLNavigator.matchURL("myapp://user/1", from: from)! - XCTAssertEqual(URLPattern, "myapp://user/") - XCTAssertEqual(values as! [String: String], ["id": "1"]) - - let scheme = URLNavigator.matchURL("/user/1", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["myapp://user/", "myapp://user//hello"] - let (URLPattern, values) = URLNavigator.matchURL("myapp://user/1", from: from)! - XCTAssertEqual(URLPattern, "myapp://user/") - XCTAssertEqual(values as! [String: String], ["id": "1"]) - - let scheme = URLNavigator.matchURL("/user/1", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["myapp://user/", "myapp://user//"] - let (URLPattern, values) = URLNavigator.matchURL("myapp://user/1/posts", from: from)! - XCTAssertEqual(URLPattern, "myapp://user//") - XCTAssertEqual(values as! [String: String], ["id": "1", "object": "posts"]) - - let scheme = URLNavigator.matchURL("/user/1/posts", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["myapp://alert"] - let (URLPattern, values) = URLNavigator.matchURL("myapp://alert?title=hello&message=world", from: from)! - XCTAssertEqual(URLPattern, "myapp://alert") - XCTAssertEqual(values.count, 0) - - let scheme = URLNavigator.matchURL("/alert?title=hello&message=world", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["http://"] - let (URLPattern, values) = URLNavigator.matchURL("http://xoul.kr", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "xoul.kr"]) - - let scheme = URLNavigator.matchURL("http://xoul.kr", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["http://"] - let (URLPattern, values) = URLNavigator.matchURL("http://xoul.kr/resume", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "xoul.kr/resume"]) - - let scheme = URLNavigator.matchURL("http://xoul.kr/resume", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["http://"] - let (URLPattern, values) = URLNavigator.matchURL("http://google.com/search?q=URLNavigator", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) - - let scheme = URLNavigator.matchURL("http://google.com/search?q=URLNavigator", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - { - let from = ["http://"] - let (URLPattern, values) = URLNavigator.matchURL("http://google.com/search/?q=URLNavigator", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) - - let scheme = URLNavigator.matchURL("http://google.com/search/?q=URLNavigator", - scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) - }(); - } - - func testURLWithScheme() { - XCTAssertEqual(URLNavigator.URLWithScheme(nil, "myapp://user/1").URLStringValue, "myapp://user/1") - XCTAssertEqual(URLNavigator.URLWithScheme("myapp", "/user/1").URLStringValue, "myapp://user/1") - XCTAssertEqual(URLNavigator.URLWithScheme("", "/user/1").URLStringValue, "://user/1") // idiot - } - - func testNormalizedURL() { - XCTAssertEqual(URLNavigator.normalizedURL("myapp://user//hello").URLStringValue, "myapp://user//hello") - XCTAssertEqual(URLNavigator.normalizedURL("myapp:///////user/////hello/??/#abc=/def").URLStringValue, - "myapp://user//hello") - XCTAssertEqual(URLNavigator.normalizedURL("https://").URLStringValue, "https://") - } - - func testPlaceholderValueFromURLPathComponents() { - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["123", "456"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "id") - XCTAssertEqual(placeholder?.1 as? String, "123") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["123", "456"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "id") - XCTAssertEqual(placeholder?.1 as? Int, 123) - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["abc", "456"], - atIndex: 0 - ) - XCTAssertNil(placeholder) - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["180", "456"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "height") - XCTAssertEqual(placeholder?.1 as? Float, 180) - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["abc", "456"], - atIndex: 0 - ) - XCTAssertNil(placeholder) - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["xoul.kr"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["xoul.kr", "resume"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["xoul.kr"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "xoul.kr") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["xoul.kr", "resume"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "xoul.kr/resume") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["google.com", "search?q=test"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "google.com/search?q=test") - }(); - { - let placeholder = URLNavigator.placeholderKeyValueFromURLPatternPathComponent( - "", - URLPathComponents: ["google.com", "search", "?q=test"], - atIndex: 0 - ) - XCTAssertEqual(placeholder?.0, "url") - XCTAssertEqual(placeholder?.1 as? String, "google.com/search/?q=test") - }(); - } - - func testReplaceRegex() { - XCTAssertEqual(URLNavigator.replaceRegex("a", "0", "abc"), "0bc") - XCTAssertEqual(URLNavigator.replaceRegex("\\d", "A", "1234567abc098"), "AAAAAAAabcAAA") - } - -} diff --git a/URLNavigator.xcodeproj/project.pbxproj b/URLNavigator.xcodeproj/project.pbxproj index 0dc607d..d392f46 100644 --- a/URLNavigator.xcodeproj/project.pbxproj +++ b/URLNavigator.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0245A7291D79E9CB004698A3 /* URLMatcherInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0245A7251D79E8CD004698A3 /* URLMatcherInternalTests.swift */; }; + 0245A72A1D79EB0F004698A3 /* URLMatcherPublicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0245A7271D79E8F5004698A3 /* URLMatcherPublicTests.swift */; }; 02E016081D79E45E00BB7E25 /* URLMatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02E016071D79E45E00BB7E25 /* URLMatcher.swift */; }; 03032BB81CCBA94B00F7EBFD /* URLConvertibleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03032BB61CCBA93400F7EBFD /* URLConvertibleTests.swift */; }; 03107EDD1C6364E700DF2F14 /* URLNavigator.h in Headers */ = {isa = PBXBuildFile; fileRef = 03107EDA1C6364E700DF2F14 /* URLNavigator.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -15,7 +17,6 @@ 03107EE41C6365E500DF2F14 /* URLConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03107EE31C6365E500DF2F14 /* URLConvertible.swift */; }; 03107EE61C6366F500DF2F14 /* UIViewController+TopMostViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03107EE51C6366F500DF2F14 /* UIViewController+TopMostViewController.swift */; }; 03107EF01C636A3D00DF2F14 /* URLNavigator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03107ECE1C6362E800DF2F14 /* URLNavigator.framework */; }; - 03107EF91C636A7500DF2F14 /* URLNavigatorInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03107EF71C636A5500DF2F14 /* URLNavigatorInternalTests.swift */; }; 03107EFC1C67289100DF2F14 /* URLNavigatorPublicTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03107EFA1C67288400DF2F14 /* URLNavigatorPublicTests.swift */; }; 03D35E781D35289200452731 /* URLNavigator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03107ECE1C6362E800DF2F14 /* URLNavigator.framework */; }; 03D35E791D35289200452731 /* URLNavigator.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 03107ECE1C6362E800DF2F14 /* URLNavigator.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -66,6 +67,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0245A7251D79E8CD004698A3 /* URLMatcherInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLMatcherInternalTests.swift; sourceTree = ""; }; + 0245A7271D79E8F5004698A3 /* URLMatcherPublicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLMatcherPublicTests.swift; sourceTree = ""; }; 02E016071D79E45E00BB7E25 /* URLMatcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLMatcher.swift; sourceTree = ""; }; 03032BB61CCBA93400F7EBFD /* URLConvertibleTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLConvertibleTests.swift; sourceTree = ""; }; 03107ECE1C6362E800DF2F14 /* URLNavigator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = URLNavigator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -76,7 +79,6 @@ 03107EE31C6365E500DF2F14 /* URLConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLConvertible.swift; sourceTree = ""; }; 03107EE51C6366F500DF2F14 /* UIViewController+TopMostViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIViewController+TopMostViewController.swift"; sourceTree = ""; }; 03107EEB1C636A3D00DF2F14 /* URLNavigatorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = URLNavigatorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 03107EF71C636A5500DF2F14 /* URLNavigatorInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLNavigatorInternalTests.swift; sourceTree = ""; }; 03107EFA1C67288400DF2F14 /* URLNavigatorPublicTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLNavigatorPublicTests.swift; sourceTree = ""; }; 03D35E661D35209600452731 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 03D35E7F1D3528F800452731 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -166,7 +168,8 @@ 03107EF61C636A5500DF2F14 /* Tests */ = { isa = PBXGroup; children = ( - 03107EF71C636A5500DF2F14 /* URLNavigatorInternalTests.swift */, + 0245A7251D79E8CD004698A3 /* URLMatcherInternalTests.swift */, + 0245A7271D79E8F5004698A3 /* URLMatcherPublicTests.swift */, 03107EFA1C67288400DF2F14 /* URLNavigatorPublicTests.swift */, 03032BB61CCBA93400F7EBFD /* URLConvertibleTests.swift */, ); @@ -411,9 +414,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 03107EF91C636A7500DF2F14 /* URLNavigatorInternalTests.swift in Sources */, 03107EFC1C67289100DF2F14 /* URLNavigatorPublicTests.swift in Sources */, 03032BB81CCBA94B00F7EBFD /* URLConvertibleTests.swift in Sources */, + 0245A7291D79E9CB004698A3 /* URLMatcherInternalTests.swift in Sources */, + 0245A72A1D79EB0F004698A3 /* URLMatcherPublicTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; From 0681473e1875741fcc60c393daff82d4eac01240 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:25:16 -0400 Subject: [PATCH 3/8] parse query items from URL when matching --- Sources/URLMatcher.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 64a1c82..6a4621f 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -77,6 +77,13 @@ public class URLMatcher { var values = [String: AnyObject]() + // Query String + let urlComponents = NSURLComponents(string: URL.URLStringValue) + + for queryItem in urlComponents?.queryItems ?? [] { + values[queryItem.name] = queryItem.value + } + // e.g. ["user", ""] for (i, component) in URLPatternPathComponents.enumerate() { guard i < URLPathComponents.count else { From 26fe9b3091586c6131681bc39f06087a45b69e50 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:41:19 -0400 Subject: [PATCH 4/8] return a typed struct instead of a tuple This also adds tests for query item parsing. --- Sources/URLMatcher.swift | 22 +++++-- Sources/URLNavigator.swift | 12 ++-- Tests/URLMatcherPublicTests.swift | 96 ++++++++++++++++--------------- 3 files changed, 74 insertions(+), 56 deletions(-) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 6a4621f..47c765c 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -26,6 +26,17 @@ import Foundation +/// URLMatchComponents encapsulates data about a URL match. +/// It contains the following attributes: +/// - pattern: The URL pattern that was matched. +/// - values: The values extracted from the URL. +/// - queryItems: The query items of the URL. +public struct URLMatchComponents { + let pattern: String + let values: [String : AnyObject] + let queryItems: [String : AnyObject] +} + /// URLMatcher provides a way to match URLs against a list of specified patterns. /// /// URLMather extracts the pattrn and the values from the URL if possible. @@ -53,7 +64,7 @@ public class URLMatcher { /// /// For example: /// - /// let (URLPattern, values) = URLNavigator.matchURL("myapp://user/123", from: ["myapp://user/"]) + /// let urlMatchComponents = URLNavigator.matchURL("myapp://user/123", from: ["myapp://user/"]) /// /// The value of the `URLPattern` from an example above is `"myapp://user/"` and the value of the `values` /// is `["id": 123]`. @@ -61,9 +72,9 @@ public class URLMatcher { /// - Parameter URL: The placeholder-filled URL. /// - Parameter from: The array of URL patterns. /// - /// - Returns: A tuple of URL pattern string and a dictionary of URL placeholder values. + /// - Returns: A `URLMatchComponents` struct that holds the URL pattern string, a dictionary of URL placeholder values, and any query items. public func matchURL(URL: URLConvertible, scheme: String? = nil, - from URLPatterns: [String]) -> (String, [String: AnyObject])? { + from URLPatterns: [String]) -> URLMatchComponents? { let normalizedURLString = self.normalizedURL(URL, scheme: scheme).URLStringValue let URLPathComponents = normalizedURLString.componentsSeparatedByString("/") // e.g. ["myapp:", "user", "123"] @@ -76,12 +87,13 @@ public class URLMatcher { } var values = [String: AnyObject]() + var queryItems = [String: AnyObject]() // Query String let urlComponents = NSURLComponents(string: URL.URLStringValue) for queryItem in urlComponents?.queryItems ?? [] { - values[queryItem.name] = queryItem.value + queryItems[queryItem.name] = queryItem.value } // e.g. ["user", ""] @@ -103,7 +115,7 @@ public class URLMatcher { } } - return (URLPattern, values) + return URLMatchComponents(pattern: URLPattern, values: values, queryItems: queryItems) } return nil } diff --git a/Sources/URLNavigator.swift b/Sources/URLNavigator.swift index 249d71e..3b976f7 100644 --- a/Sources/URLNavigator.swift +++ b/Sources/URLNavigator.swift @@ -124,9 +124,9 @@ public class URLNavigator { /// - Parameter URL: The URL to find view controllers. /// - Returns: A match view controller or `nil` if not matched. public func viewControllerForURL(URL: URLConvertible) -> UIViewController? { - if let (URLPattern, values) = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: Array(self.URLMap.keys)) { - let navigable = self.URLMap[URLPattern] - return navigable?.init(URL: URL, values: values) as? UIViewController + if let urlMatchComponents = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: Array(self.URLMap.keys)) { + let navigable = self.URLMap[urlMatchComponents.pattern] + return navigable?.init(URL: URL, values: urlMatchComponents.values) as? UIViewController } return nil } @@ -251,9 +251,9 @@ public class URLNavigator { /// - Returns: The return value of the matching `URLOpenHandler`. Returns `false` if there's no match. public func openURL(URL: URLConvertible) -> Bool { let URLOpenHandlersKeys = Array(self.URLOpenHandlers.keys) - if let (URLPattern, values) = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: URLOpenHandlersKeys) { - let handler = self.URLOpenHandlers[URLPattern] - if handler?(URL: URL, values: values) == true { + if let urlMatchComponents = URLMatcher.defaultMatcher().matchURL(URL, scheme: self.scheme, from: URLOpenHandlersKeys) { + let handler = self.URLOpenHandlers[urlMatchComponents.pattern] + if handler?(URL: URL, values: urlMatchComponents.values) == true { return true } } diff --git a/Tests/URLMatcherPublicTests.swift b/Tests/URLMatcherPublicTests.swift index 1b10035..1301c6f 100644 --- a/Tests/URLMatcherPublicTests.swift +++ b/Tests/URLMatcherPublicTests.swift @@ -65,94 +65,100 @@ class URLMatcherPublicTests: XCTestCase { }(); { let from = ["myapp://hello"] - let (URLPattern, values) = matcher.matchURL("myapp://hello", from: from)! - XCTAssertEqual(URLPattern, "myapp://hello") - XCTAssertEqual(values.count, 0) + let urlMatchComponents = matcher.matchURL("myapp://hello", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "myapp://hello") + XCTAssertEqual(urlMatchComponents.values.count, 0) let scheme = matcher.matchURL("/hello", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["myapp://user/"] - let (URLPattern, values) = matcher.matchURL("myapp://user/1", from: from)! - XCTAssertEqual(URLPattern, "myapp://user/") - XCTAssertEqual(values as! [String: String], ["id": "1"]) + let urlMatchComponents = matcher.matchURL("myapp://user/1", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "myapp://user/") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["id": "1"]) let scheme = matcher.matchURL("/user/1", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["myapp://user/", "myapp://user//hello"] - let (URLPattern, values) = matcher.matchURL("myapp://user/1", from: from)! - XCTAssertEqual(URLPattern, "myapp://user/") - XCTAssertEqual(values as! [String: String], ["id": "1"]) + let urlMatchComponents = matcher.matchURL("myapp://user/1", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "myapp://user/") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["id": "1"]) let scheme = matcher.matchURL("/user/1", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["myapp://user/", "myapp://user//"] - let (URLPattern, values) = matcher.matchURL("myapp://user/1/posts", from: from)! - XCTAssertEqual(URLPattern, "myapp://user//") - XCTAssertEqual(values as! [String: String], ["id": "1", "object": "posts"]) + let urlMatchComponents = matcher.matchURL("myapp://user/1/posts", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "myapp://user//") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["id": "1", "object": "posts"]) let scheme = matcher.matchURL("/user/1/posts", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["myapp://alert"] - let (URLPattern, values) = matcher.matchURL("myapp://alert?title=hello&message=world", from: from)! - XCTAssertEqual(URLPattern, "myapp://alert") - XCTAssertEqual(values.count, 0) + let urlMatchComponents = matcher.matchURL("myapp://alert?title=hello&message=world", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "myapp://alert") + XCTAssertEqual(urlMatchComponents.values.count, 0) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], ["title": "hello", "message": "world"]) let scheme = matcher.matchURL("/alert?title=hello&message=world", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], ["title": "hello", "message": "world"]) }(); { let from = ["http://"] - let (URLPattern, values) = matcher.matchURL("http://xoul.kr", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "xoul.kr"]) + let urlMatchComponents = matcher.matchURL("http://xoul.kr", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "http://") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["url": "xoul.kr"]) let scheme = matcher.matchURL("http://xoul.kr", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["http://"] - let (URLPattern, values) = matcher.matchURL("http://xoul.kr/resume", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "xoul.kr/resume"]) + let urlMatchComponents = matcher.matchURL("http://xoul.kr/resume", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "http://") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["url": "xoul.kr/resume"]) let scheme = matcher.matchURL("http://xoul.kr/resume", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) }(); { let from = ["http://"] - let (URLPattern, values) = matcher.matchURL("http://google.com/search?q=URLNavigator", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) + let urlMatchComponents = matcher.matchURL("http://google.com/search?q=URLNavigator", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "http://") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["url": "google.com/search"]) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], ["q": "URLNavigator"]) let scheme = matcher.matchURL("http://google.com/search?q=URLNavigator", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], scheme.queryItems as! [String: String]) }(); { let from = ["http://"] - let (URLPattern, values) = matcher.matchURL("http://google.com/search/?q=URLNavigator", from: from)! - XCTAssertEqual(URLPattern, "http://") - XCTAssertEqual(values as! [String: String], ["url": "google.com/search"]) + let urlMatchComponents = matcher.matchURL("http://google.com/search/?q=URLNavigator", from: from)! + XCTAssertEqual(urlMatchComponents.pattern, "http://") + XCTAssertEqual(urlMatchComponents.values as! [String: String], ["url": "google.com/search"]) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], ["q": "URLNavigator"]) let scheme = matcher.matchURL("http://google.com/search/?q=URLNavigator", scheme: "myapp", from: from)! - XCTAssertEqual(URLPattern, scheme.0) - XCTAssertEqual(values as! [String: String], scheme.1 as! [String: String]) + XCTAssertEqual(urlMatchComponents.pattern, scheme.pattern) + XCTAssertEqual(urlMatchComponents.values as! [String: String], scheme.values as! [String: String]) + XCTAssertEqual(urlMatchComponents.queryItems as! [String: String], scheme.queryItems as! [String: String]) }(); } } From a57191b2592889c84b3bf124914e4987cb341459 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:45:09 -0400 Subject: [PATCH 5/8] add UUID and string parsing when matching URLs --- Sources/URLMatcher.swift | 2 ++ Tests/URLMatcherInternalTests.swift | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 47c765c..5a8a2e3 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -181,6 +181,8 @@ public class URLMatcher { let (type, key) = (typeAndKey[0], typeAndKey[1]) // e.g. ("int", "id") let value: AnyObject? switch type { + case "UUID": value = NSUUID(UUIDString: URLPathComponents[index]) // e.g. 123e4567-e89b-12d3-a456-426655440000 + case "string": value = String(URLPathComponents[index]) // e.g. "123" case "int": value = Int(URLPathComponents[index]) // e.g. 123 case "float": value = Float(URLPathComponents[index]) // e.g. 123.0 case "path": value = URLPathComponents[index..", + URLPathComponents: ["123", "456"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "id") + XCTAssertEqual(placeholder?.1 as? String, "123") + }(); + { + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["123e4567-e89b-12d3-a456-426655440000"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "uuid") + XCTAssertEqual(placeholder?.1 as? NSUUID, NSUUID(UUIDString: "123e4567-e89b-12d3-a456-426655440000")) + }(); { let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( "", From 97fd6462fd027e99baf6f14e45af3a0b3777ddd2 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:45:59 -0400 Subject: [PATCH 6/8] update typo --- Sources/URLMatcher.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 5a8a2e3..155aa39 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -39,7 +39,7 @@ public struct URLMatchComponents { /// URLMatcher provides a way to match URLs against a list of specified patterns. /// -/// URLMather extracts the pattrn and the values from the URL if possible. +/// URLMatcher extracts the pattrn and the values from the URL if possible. public class URLMatcher { // MARK: Initialization From 90b08b26b6a7d69a2450dc74a32dd400ed7f6789 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:54:28 -0400 Subject: [PATCH 7/8] add custom URL value type matching capability --- Sources/URLMatcher.swift | 32 ++++++++++++++++++++++++++++- Tests/URLMatcherInternalTests.swift | 21 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 155aa39..7fa055f 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -42,6 +42,12 @@ public struct URLMatchComponents { /// URLMatcher extracts the pattrn and the values from the URL if possible. public class URLMatcher { + /// A closure type which matches a URL value string to a typed value. + public typealias URLValueMatcherHandler = (String) -> AnyObject? + + /// A dictionary to store URL value matchers by value type. + private var customURLValueMatcherHandlers = [String : URLValueMatcherHandler]() + // MARK: Initialization public init() { @@ -121,6 +127,24 @@ public class URLMatcher { } // MARK: Utils + + /// Adds a new handler for matching any custom URL value type. + /// If the custom URL type already has a custom handler, this overwrites its handler. + /// + /// For example: + /// + /// URLMatcher.defaultMatcher().addURLValueMatcherHandler("ssn") { (ssnString) -> AnyObject? in + /// return SSN(string: ssnString) + /// } + /// + /// The value type that this would match against is "ssn" (i.e. Social Security Number), and the + /// handler to be used for that type returns a newly created `SSN` object from the ssn string. + /// + /// - Parameter valueType: The value type (string) to match against. + /// - Parameter handler: The handler to use when matching against that value type. + public func addURLValueMatcherHandler(valueType: String, handler: URLValueMatcherHandler) { + self.customURLValueMatcherHandlers[valueType] = handler + } /// Returns an scheme-appended `URLConvertible` if given `URL` doesn't have its scheme. func URLWithScheme(scheme: String?, _ URL: URLConvertible) -> URLConvertible { @@ -186,7 +210,13 @@ public class URLMatcher { case "int": value = Int(URLPathComponents[index]) // e.g. 123 case "float": value = Float(URLPathComponents[index]) // e.g. 123.0 case "path": value = URLPathComponents[index.. AnyObject? in + return SSN(ssnString: ssnString) + }) + + let placeholder = matcher.placeholderKeyValueFromURLPatternPathComponent( + "", + URLPathComponents: ["123-45-6789"], + atIndex: 0 + ) + XCTAssertEqual(placeholder?.0, "ssn") + XCTAssertEqual((placeholder?.1 as? SSN)?.ssnString, "123-45-6789") + }(); } func testReplaceRegex() { From 44d958143d1fa75d36bb99a4bfd6df339a7ed5d7 Mon Sep 17 00:00:00 2001 From: Josh Sklar Date: Fri, 2 Sep 2016 13:54:57 -0400 Subject: [PATCH 8/8] update comments and error messaging to use matcher --- Sources/URLMatcher.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/URLMatcher.swift b/Sources/URLMatcher.swift index 7fa055f..d60f13d 100644 --- a/Sources/URLMatcher.swift +++ b/Sources/URLMatcher.swift @@ -70,7 +70,7 @@ public class URLMatcher { /// /// For example: /// - /// let urlMatchComponents = URLNavigator.matchURL("myapp://user/123", from: ["myapp://user/"]) + /// let urlMatchComponents = matcher.matchURL("myapp://user/123", from: ["myapp://user/"]) /// /// The value of the `URLPattern` from an example above is `"myapp://user/"` and the value of the `values` /// is `["id": 123]`. @@ -133,7 +133,7 @@ public class URLMatcher { /// /// For example: /// - /// URLMatcher.defaultMatcher().addURLValueMatcherHandler("ssn") { (ssnString) -> AnyObject? in + /// matcher.addURLValueMatcherHandler("SSN") { (ssnString) -> AnyObject? in /// return SSN(string: ssnString) /// } /// @@ -157,7 +157,7 @@ public class URLMatcher { #endif return scheme + ":/" + URLString } else if scheme == nil && !URLString.containsString("://") { - assertionFailure("Either navigator or URL should have scheme: '\(URL)'") // assert only in debug build + assertionFailure("Either matcher or URL should have scheme: '\(URL)'") // assert only in debug build } return URLString }