Skip to content

Commit

Permalink
Added the MapRemoteFloret (#220)
Browse files Browse the repository at this point in the history
* Added a MapRemoteFloret that can map a url onto another url

* Added a UI to enable and disable mappings

* Added persistence of the configuration and documentation

* Updates after swiftlint warnings

* Updated the documentation

* Minor documentation changes after code review
  • Loading branch information
brototyp authored Mar 15, 2021
1 parent 74bdc8b commit 62626f4
Show file tree
Hide file tree
Showing 24 changed files with 1,029 additions and 906 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## Unreleased
* **feature** Added a `MapRemoteFloret` that can change urls of requests before they are sent. [#216](https://github.com/cauliframework/cauli/issues/216) by @brototyp
* **improvement** Added an optional description to florets [#176](https://github.com/cauliframework/cauli/issues/176) by @Shukuyen
* **bugfix** Fixed an issue where cauli didn’t pass the redirection information up to the application. [#196](https://github.com/cauliframework/cauli/issues/196)

Expand Down
5 changes: 5 additions & 0 deletions Cauli/CauliViewController/CauliViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ internal class CauliViewController: UITableViewController {
tableView.register(UINib(nibName: SwitchTableViewCell.nibName, bundle: bundle), forCellReuseIdentifier: SwitchTableViewCell.reuseIdentifier)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
tableView.reloadData()
}

override func numberOfSections(in tableView: UITableView) -> Int {
2
}
Expand Down
100 changes: 100 additions & 0 deletions Cauli/Florets/MapRemote/MapRemoteFloret.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//
// Copyright (c) 2018 cauli.works
//
// 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 UIKit

/// The `MapRemoteFloret` can modify the url before the request is performed.
/// This is esp. helpful when using a staging or testing server.
///
/// The `MapRemoteFloret` can only modify the url of a request. If you need to update headers please use the `FindReplaceFloret`.
///
/// Example configuration. For more examples check the `Mapping` documentation.
/// ```swift
/// let httpsifyMapping = Mapping(name: "https-ify", sourceLocation: MappingLocation(scheme: "http"), destinationLocation: MappingLocation(scheme: "https"))
/// let mapLocal = Mapping(name: "map local", sourceLocation: MappingLocation(), destinationLocation: MappingLocation(host: "localhost")
/// let floret = MapRemoteFloret(mappings: [httpsifyMapping, mapLocal])
/// Cauli([floret])
/// ```
public class MapRemoteFloret: InterceptingFloret {

internal var userDefaults = UserDefaults()
public var enabled: Bool {
get {
userDefaults.bool(forKey: "Cauli.MapRemoteFloret.enabled")
}
set {
userDefaults.setValue(newValue, forKey: "Cauli.MapRemoteFloret.enabled")
}
}

public var description: String? {
"The MapRemoteFloret can modify the url of a request before performed. \n Currently \(enabledMappings.count) mappings are enabled."
}

private let mappings: [Mapping]
private var enabledMappings: Set<String> {
get {
guard let mappings = userDefaults.array(forKey: "Cauli.MapRemoteFloret.enabledMappings") as? [String] else { return [] }
return Set(mappings)
}
set {
userDefaults.setValue(Array(newValue), forKey: "Cauli.MapRemoteFloret.enabledMappings")
}
}

/// Instantiates a new `MapRemoteFloret` instance with an array of mappings.
/// - Parameter mappings: An array of mappings. Mappings will be evaluated in the order of this array.
public init(mappings: [Mapping]) {
self.mappings = mappings
}

func isMappingEnabled(_ mapping: Mapping) -> Bool {
enabledMappings.contains(mapping.name)
}

func setMapping(_ mapping: Mapping, enabled: Bool) {
if enabled {
enabledMappings.insert(mapping.name)
} else {
enabledMappings.remove(mapping.name)
}
}

public func willRequest(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) {
let mappedRecord = mappings.filter { enabledMappings.contains($0.name) }.reduce(record) { record, mapping -> Record in
guard let requestUrl = record.designatedRequest.url, mapping.sourceLocation.matches(url: requestUrl) else { return record }
var record = record
let updatedUrl = mapping.destinationLocation.updating(url: requestUrl)
record.designatedRequest.url = updatedUrl
return record
}
completionHandler(mappedRecord)
}

public func didRespond(_ record: Record, modificationCompletionHandler completionHandler: @escaping (Record) -> Void) {
completionHandler(record)
}

public func viewController(_ cauli: Cauli) -> UIViewController {
MappingsListViewController(mapRemoteFloret: self, mappings: mappings)
}
}
129 changes: 129 additions & 0 deletions Cauli/Florets/MapRemote/Mapping.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//
// Copyright (c) 2018 cauli.works
//
// 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

/// A `Mapping` is a declaration of a single mapping. All urls that
/// match the description of the `sourceLocation` will be changed
/// according to the description in the `destinationLocation`.
/// All `nil` values are ignored in this case.
///
/// ## Examples
///
/// This `Mapping` will update all http requests to https requests.
/// ```swift
/// Mapping(name: "https-ify", sourceLocation: MappingLocation(scheme: "http"), destinationLocation: MappingLocation(scheme: "https"))
/// ```
///
/// This mapping will redirect all requests to localhost.
/// ```swift
/// Mapping(name: "map local", sourceLocation: MappingLocation(), destinationLocation: MappingLocation(host: "localhost")
/// ```
public struct Mapping {
let name: String
let sourceLocation: MappingLocation
let destinationLocation: MappingLocation

/// Initializes a new `Mapping`.
/// - Parameters:
/// - name: The name of the `Mapping`. This is used to uniquely identify this mapping.
/// - sourceLocation: This defines all urls this `Mapping` should apply to.
/// - destinationLocation: This defines the changes for the url.
public init(name: String, sourceLocation: MappingLocation, destinationLocation: MappingLocation) {
self.name = name
self.sourceLocation = sourceLocation
self.destinationLocation = destinationLocation
}
}

/// A mapping location describes a given part of a url.
/// The mapping location can be used to either filter urls, or to apply changes on the url.
public struct MappingLocation {
let `protocol`: Protocol?
let host: String?
let port: Int?
let path: String?
let query: String?

/// Instantiates a new `MappingLocation`.
/// - Parameters:
/// - protocol: The protocol, http or https, of the url.
/// - host: The host of the url.
/// - port: The port of the url.
/// - path: The path of the url. Should begin with a `/`.
/// - query: The query of the url. Should contain the `?`.
public init(`protocol`: Protocol? = nil, host: String? = nil, port: Int? = nil, path: String? = nil, query: String? = nil) {
self.`protocol` = `protocol`
self.host = host
self.port = port
self.path = path
self.query = query
}
}

internal extension MappingLocation {
func matches(url: URL) -> Bool {
if let `protocol` = `protocol`, url.scheme != `protocol`.rawValue {
return false
}
if let host = host, url.host != host {
return false
}
if let port = port, url.port != port {
return false
}
if let path = path, url.path != path {
return false
}
if let query = query, url.query != query {
return false
}
return true
}

func updating(url oldUrl: URL) -> URL {
guard var components = URLComponents(url: oldUrl, resolvingAgainstBaseURL: false) else { return oldUrl }
if let `protocol` = `protocol` {
components.scheme = `protocol`.rawValue
}
if let host = host {
components.host = host
}
if let port = port {
components.port = port
}
if let path = path {
components.path = path
}
if let query = query {
components.query = query
}
return components.url ?? oldUrl
}
}

public extension MappingLocation {
enum `Protocol`: String {
case http
case https
}
}
55 changes: 55 additions & 0 deletions Cauli/Florets/MapRemote/MappingsListDataSource.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//
// Copyright (c) 2018 cauli.works
//
// 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 UIKit

internal class MappingsListDataSource: NSObject, UITableViewDataSource {

private let mapRemoteFloret: MapRemoteFloret
private let mappings: [Mapping]

init(mapRemoteFloret: MapRemoteFloret, mappings: [Mapping]) {
self.mapRemoteFloret = mapRemoteFloret
self.mappings = mappings
}

func setup(_ tableView: UITableView) {
let bundle = Bundle(for: SwitchTableViewCell.self)
tableView.register(UINib(nibName: SwitchTableViewCell.nibName, bundle: bundle), forCellReuseIdentifier: SwitchTableViewCell.reuseIdentifier)
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
mappings.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: SwitchTableViewCell.reuseIdentifier, for: indexPath) as? SwitchTableViewCell else {
fatalError("Unable to dequeue a cell")
}
let mapping = mappings[indexPath.row]
cell.set(title: mapping.name, switchValue: mapRemoteFloret.isMappingEnabled(mapping), description: nil)
cell.switchValueChanged = { [weak mapRemoteFloret] newValue in
mapRemoteFloret?.setMapping(mapping, enabled: newValue)
}
return cell
}
}
41 changes: 41 additions & 0 deletions Cauli/Florets/MapRemote/MappingsListViewController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// Copyright (c) 2018 cauli.works
//
// 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 UIKit

internal class MappingsListViewController: UITableViewController {

private let dataSource: MappingsListDataSource

init(mapRemoteFloret: MapRemoteFloret, mappings: [Mapping]) {
self.dataSource = MappingsListDataSource(mapRemoteFloret: mapRemoteFloret, mappings: mappings)
super.init(nibName: nil, bundle: nil)
dataSource.setup(tableView)
tableView.dataSource = dataSource
}

@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

}
Loading

0 comments on commit 62626f4

Please sign in to comment.