URLMatcher.swift 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. import Foundation
  2. /// URLMatcher provides a way to match URLs against a list of specified patterns.
  3. ///
  4. /// URLMatcher extracts the pattern and the values from the URL if possible.
  5. open class URLMatcher {
  6. public typealias URLPattern = String
  7. public typealias URLValueConverter = (_ pathComponents: [String], _ index: Int) -> Any?
  8. static let defaultURLValueConverters: [String: URLValueConverter] = [
  9. "string": { pathComponents, index in
  10. return pathComponents[index]
  11. },
  12. "int": { pathComponents, index in
  13. return Int(pathComponents[index])
  14. },
  15. "float": { pathComponents, index in
  16. return Float(pathComponents[index])
  17. },
  18. "uuid": { pathComponents, index in
  19. return UUID(uuidString: pathComponents[index])
  20. },
  21. "path": { pathComponents, index in
  22. return pathComponents[index..<pathComponents.count].joined(separator: "/")
  23. }
  24. ]
  25. open var valueConverters: [String: URLValueConverter] = URLMatcher.defaultURLValueConverters
  26. public init() {
  27. // 🔄 I'm an URLMatcher!
  28. }
  29. /// Returns a matching URL pattern and placeholder values from the specified URL and patterns.
  30. /// It returns `nil` if the given URL is not contained in the URL patterns.
  31. ///
  32. /// For example:
  33. ///
  34. /// let result = matcher.match("myapp://user/123", from: ["myapp://user/<int:id>"])
  35. ///
  36. /// The value of the `URLPattern` from an example above is `"myapp://user/<int:id>"` and the
  37. /// value of the `values` is `["id": 123]`.
  38. ///
  39. /// - parameter url: The placeholder-filled URL.
  40. /// - parameter from: The array of URL patterns.
  41. ///
  42. /// - returns: A `URLMatchComponents` struct that holds the URL pattern string, a dictionary of
  43. /// the URL placeholder values.
  44. open func match(_ url: URLConvertible, from candidates: [URLPattern]) -> URLMatchResult? {
  45. let url = self.normalizeURL(url)
  46. let scheme = url.urlValue?.scheme
  47. let stringPathComponents = self.stringPathComponents(from :url)
  48. for candidate in candidates {
  49. guard scheme == candidate.urlValue?.scheme else { continue }
  50. if let result = self.match(stringPathComponents, with: candidate) {
  51. return result
  52. }
  53. }
  54. return nil
  55. }
  56. func match(_ stringPathComponents: [String], with candidate: URLPattern) -> URLMatchResult? {
  57. let normalizedCandidate = self.normalizeURL(candidate).urlStringValue
  58. let candidatePathComponents = self.pathComponents(from: normalizedCandidate)
  59. guard self.ensurePathComponentsCount(stringPathComponents, candidatePathComponents) else {
  60. return nil
  61. }
  62. var urlValues: [String: Any] = [:]
  63. let pairCount = min(stringPathComponents.count, candidatePathComponents.count)
  64. for index in 0..<pairCount {
  65. let result = self.matchStringPathComponent(
  66. at: index,
  67. from: stringPathComponents,
  68. with: candidatePathComponents
  69. )
  70. switch result {
  71. case let .matches(placeholderValue):
  72. if let (key, value) = placeholderValue {
  73. urlValues[key] = value
  74. }
  75. case .notMatches:
  76. return nil
  77. }
  78. }
  79. return URLMatchResult(pattern: candidate, values: urlValues)
  80. }
  81. func normalizeURL(_ dirtyURL: URLConvertible) -> URLConvertible {
  82. guard dirtyURL.urlValue != nil else { return dirtyURL }
  83. var urlString = dirtyURL.urlStringValue
  84. urlString = urlString.components(separatedBy: "?")[0].components(separatedBy: "#")[0]
  85. urlString = self.replaceRegex(":/{3,}", "://", urlString)
  86. urlString = self.replaceRegex("(?<!:)/{2,}", "/", urlString)
  87. urlString = self.replaceRegex("(?<!:|:/)/+$", "", urlString)
  88. return urlString
  89. }
  90. func replaceRegex(_ pattern: String, _ repl: String, _ string: String) -> String {
  91. guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return string }
  92. let range = NSMakeRange(0, string.count)
  93. return regex.stringByReplacingMatches(in: string, options: [], range: range, withTemplate: repl)
  94. }
  95. func ensurePathComponentsCount(
  96. _ stringPathComponents: [String],
  97. _ candidatePathComponents: [URLPathComponent]
  98. ) -> Bool {
  99. let hasSameNumberOfComponents = (stringPathComponents.count == candidatePathComponents.count)
  100. let containsPathPlaceholderComponent = candidatePathComponents.contains {
  101. if case let .placeholder(type, _) = $0, type == "path" {
  102. return true
  103. } else {
  104. return false
  105. }
  106. }
  107. return hasSameNumberOfComponents || (containsPathPlaceholderComponent && stringPathComponents.count > candidatePathComponents.count)
  108. }
  109. func stringPathComponents(from url: URLConvertible) -> [String] {
  110. return url.urlStringValue.components(separatedBy: "/").lazy
  111. .filter { !$0.isEmpty }
  112. .filter { !$0.hasSuffix(":") }
  113. }
  114. func pathComponents(from url: URLPattern) -> [URLPathComponent] {
  115. return self.stringPathComponents(from: url).map(URLPathComponent.init)
  116. }
  117. func matchStringPathComponent(
  118. at index: Int,
  119. from stringPathComponents: [String],
  120. with candidatePathComponents: [URLPathComponent]
  121. ) -> URLPathComponentMatchResult {
  122. let stringPathComponent = stringPathComponents[index]
  123. let urlPathComponent = candidatePathComponents[index]
  124. switch urlPathComponent {
  125. case let .plain(value):
  126. guard stringPathComponent == value else { return .notMatches }
  127. return .matches(nil)
  128. case let .placeholder(type, key):
  129. guard let type = type, let converter = self.valueConverters[type] else {
  130. return .matches((key, stringPathComponent))
  131. }
  132. if let value = converter(stringPathComponents, index) {
  133. return .matches((key, value))
  134. } else {
  135. return .notMatches
  136. }
  137. }
  138. }
  139. }