From 8273610687324236dabde99db87acd57d776c4fa Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 14 May 2021 07:50:26 +1000 Subject: [PATCH 01/26] initial refactor --- DDMockiOS.xcodeproj/project.pbxproj | 72 +++++- DDMockiOS/DDMock.swift | 107 ++++++--- DDMockiOS/DDMockProtocol.swift | 59 ++--- DDMockiOS/DDMockSettingsBundleHelper.swift | 58 ++--- DDMockiOS/DDMockiOS.h | 4 +- DDMockiOS/MockEntry.swift | 53 +++-- DDMockiOS/py/ddmock.py | 248 +++++++++++++++++++++ DDMockiOS/py/src/plist.py | 16 ++ DDMockiOS/py/src/swagger_to_plist.py | 4 + DDMockiOS/resources/general.json | 56 +++++ DDMockiOS/resources/general.plist | 89 ++++++++ 11 files changed, 649 insertions(+), 117 deletions(-) create mode 100755 DDMockiOS/py/ddmock.py create mode 100644 DDMockiOS/py/src/plist.py create mode 100644 DDMockiOS/py/src/swagger_to_plist.py create mode 100644 DDMockiOS/resources/general.json create mode 100644 DDMockiOS/resources/general.plist diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 5b202da..8d48c0c 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -14,6 +14,9 @@ 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMock.swift */; }; 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */; }; + C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; + C9334FC2264D1CB200190EB7 /* plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC0264D1CB200190EB7 /* plist.py */; }; + C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -42,6 +45,11 @@ 9118D8BD223F1B4C00195DC1 /* DDMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMock.swift; sourceTree = ""; }; 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEntry.swift; sourceTree = ""; }; 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockSettingsBundleHelper.swift; sourceTree = ""; }; + C9334FAE264CF8A800190EB7 /* ddmock.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ddmock.py; sourceTree = ""; }; + C9334FB3264CF90400190EB7 /* general.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = general.plist; sourceTree = ""; }; + C9334FBC264D1AB500190EB7 /* general.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = general.json; sourceTree = ""; }; + C9334FC0264D1CB200190EB7 /* plist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = plist.py; sourceTree = ""; }; + C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = swagger_to_plist.py; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -120,10 +128,47 @@ 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, 9118D8B5223F1AE300195DC1 /* Info.plist */, + C9334F98264CC11100190EB7 /* py */, + C9334FBF264D1C9200190EB7 /* resources */, ); path = DDMockiOS; sourceTree = ""; }; + C9334F98264CC11100190EB7 /* py */ = { + isa = PBXGroup; + children = ( + C9334FAE264CF8A800190EB7 /* ddmock.py */, + C9334FAF264CF8A800190EB7 /* src */, + C9334FAD264CF8A800190EB7 /* tests */, + ); + path = py; + sourceTree = ""; + }; + C9334FAD264CF8A800190EB7 /* tests */ = { + isa = PBXGroup; + children = ( + ); + path = tests; + sourceTree = ""; + }; + C9334FAF264CF8A800190EB7 /* src */ = { + isa = PBXGroup; + children = ( + C9334FC0264D1CB200190EB7 /* plist.py */, + C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */, + ); + path = src; + sourceTree = ""; + }; + C9334FBF264D1C9200190EB7 /* resources */ = { + isa = PBXGroup; + children = ( + C9334FBC264D1AB500190EB7 /* general.json */, + C9334FB3264CF90400190EB7 /* general.plist */, + ); + path = resources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -164,6 +209,7 @@ 9118D8AD223F1AE300195DC1 /* Sources */, 9118D8AE223F1AE300195DC1 /* Frameworks */, 9118D8AF223F1AE300195DC1 /* Resources */, + C9334F9D264CCBBA00190EB7 /* DDMOCK */, ); buildRules = ( ); @@ -182,7 +228,7 @@ attributes = { LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1240; - ORGANIZATIONNAME = "Bunduwongse, Natalie (AU - Sydney)"; + ORGANIZATIONNAME = "Deloitte Digital"; TargetAttributes = { 52C27F7C2609C1A600D04B74 = { CreatedOnToolsVersion = 12.4; @@ -223,11 +269,35 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C9334FBD264D1AB500190EB7 /* general.json in Resources */, + C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */, + C9334FC2264D1CB200190EB7 /* plist.py in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + C9334F9D264CCBBA00190EB7 /* DDMOCK */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = DDMOCK; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 ${SRCROOT}/DDMockiOS/py/ddmock.py IOOF/Resources/mockfiles\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 52C27F792609C1A600D04B74 /* Sources */ = { isa = PBXSourcesBuildPhase; diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index 6bf8804..41a8287 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -1,44 +1,68 @@ import Foundation +/* + DDMock + + + + */ + +// public class DDMock { + + // path under resources directory + private let mockDirectory = "/mockfiles" - private let jsonExtension = "json" - - private var mockEntries = [String: MockEntry]() - private(set) var strict: Bool = false // Enforces mocks only and no API fall-through - public private(set) var matchedPaths = [String]() // chronological order of paths + private var mockEntries: [String: MockEntry] = [:] + + // Enforces mocks only and no API fall-through + private(set) var strict: Bool = false + + // chronological order of paths + public private(set) var matchedPaths: [String] = [] + public var onMissingMock: (_ path: String?) -> Void = {path in fatalError("missing stub for path: \(path ?? "")") } - + + // todo: no singletons in libraries public static let shared = DDMock() + // initialise DDMock library + // todo: kinda not great maybe public func initialise(strict: Bool = false) { + self.strict = strict let docsPath = Bundle.main.resourcePath! + mockDirectory let fileManager = FileManager.default - - fileManager.enumerator(atPath: docsPath)?.forEach({ (e) in - if let e = e as? String, let url = URL(string: e) { - if (url.pathExtension == jsonExtension) { + + fileManager + .enumerator(atPath: docsPath)? + .forEach{ + if + let e = $0 as? String, + let url = URL(string: e), + url.pathExtension == "json" { + createMockEntry(url: url) } } - }) } - + public func clearHistory() { matchedPaths.removeAll() } - + private func createMockEntry(url: URL) { + let fileName = "/" + url.lastPathComponent let key = url.path.replacingOccurrences(of: fileName, with: "") if var entry = mockEntries[key] { entry.files.append(url.path) mockEntries[key] = entry - } else { + } + else { mockEntries[key] = MockEntry(path: key, files: [url.path]) } } @@ -72,8 +96,28 @@ public class DDMock { return getMockEntry(path: path, method: method, isTest: false) } + // + func mockPath(request: URLRequest) -> String? { + if let url = request.url, + let method = request.httpMethod { + return mockPath(path: url.path, method: method) + } else { + return nil + } + } + + // + func mockPath(path: String, method: String) -> String? { + return path.replacingRegexMatches( + pattern: "^/", + replaceWith: "") + "/" + method.lowercased() + } + private func getMockEntry(path: String, method: String, isTest: Bool) -> MockEntry? { - guard let path = mockPath(path: path, method: method) else { return nil} + guard + let path = mockPath(path: path, method: method) else { + return nil + } return mockEntry(for: path, isTest: isTest) } @@ -81,7 +125,7 @@ public class DDMock { guard let path = request.url?.path, let method = request.httpMethod else { return .notFound } return hasMockEntry(path: path, method: method) } - + func getMockEntry(request: URLRequest) -> MockEntry? { guard let path = request.url?.path, let method = request.httpMethod else { return nil } return getMockEntry(path: path, method: method, isTest: false) @@ -104,7 +148,7 @@ public class DDMock { } return matches.first } - + func getData(_ entry: MockEntry) -> Data? { var data: Data? = nil let f = entry.files[entry.getSelectedFile()] @@ -118,35 +162,26 @@ public class DDMock { } } +// todo: extension on string idk extension String { + func matches(_ regex: String) -> Bool { - return self.range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil + return range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil } - - func replacingRegexMatches(pattern: String, replaceWith: String = "") -> String { + + func replacingRegexMatches( + pattern: String, + replaceWith: String = "") -> String { + var newString = "" do { let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive) let range = NSMakeRange(0, self.count) newString = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) - } catch { + } + catch { debugPrint("Error \(error)") } return newString } } - -extension DDMock { - func mockPath(request: URLRequest) -> String? { - if let url = request.url, - let method = request.httpMethod { - return mockPath(path: url.path, method: method) - } else { - return nil - } - } - - func mockPath(path: String, method: String) -> String? { - return path.replacingRegexMatches(pattern: "^/", replaceWith: "") + "/" + method.lowercased() - } -} diff --git a/DDMockiOS/DDMockProtocol.swift b/DDMockiOS/DDMockProtocol.swift index e8b829d..410dcb0 100644 --- a/DDMockiOS/DDMockProtocol.swift +++ b/DDMockiOS/DDMockProtocol.swift @@ -1,40 +1,47 @@ import Foundation +// ? enum EntrySetting { case notFound case mocked case useRealAPI } +// implements URLProtocol for some reason public class DDMockProtocol: URLProtocol { - - public static func initialise(config: URLSessionConfiguration) { - var protocolClasses = config.protocolClasses - if protocolClasses == nil { - protocolClasses = [AnyClass]() - } - protocolClasses!.insert(DDMockProtocol.self, at: 0) + + + // initiailise: note this is mutating config and not returning + public static func initialise(config: inout URLSessionConfiguration) { + var protocolClasses = config.protocolClasses ?? [] + protocolClasses.insert(DDMockProtocol.self, at: 0) config.protocolClasses = protocolClasses } - + override public class func canInit(with request: URLRequest) -> Bool { switch DDMock.shared.hasMockEntry(request: request) { - case .mocked: - return true - case .notFound, .useRealAPI: - return false + case .mocked: return true + case .notFound, .useRealAPI: return false } } - + + // canonical request does nothing atm override public class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - + override public func startLoading() { + // fetch item - if let path = self.request.url?.path, - let method = self.request.httpMethod { - if let entry = DDMock.shared.getMockEntry(path: path, method: method) { + if + let path = request.url?.path, + let method = request.httpMethod { + + // todo: singleton + if let entry = DDMock.shared.getMockEntry( + path: path, + method: method) { + // create mock response let data: Data? = DDMock.shared.getData(entry) var headers = [String: String]() @@ -43,25 +50,25 @@ public class DDMockProtocol: URLProtocol { headers["Content-Length"] = "\(data.count)" } let response = HTTPURLResponse(url: self.request.url!, statusCode: entry.getStatusCode(), httpVersion: "HTTP/1.1", headerFields: headers)! - + // Simulate response time Thread.sleep(forTimeInterval: TimeInterval(entry.getResponseTime() / 1000)) - + // send response - self.client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - + client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + // send response data if available if let data = data { - self.client!.urlProtocol(self, didLoad: data) + client!.urlProtocol(self, didLoad: data) } - + // finish up - self.client!.urlProtocolDidFinishLoading(self) + client!.urlProtocolDidFinishLoading(self) } } } - + override public func stopLoading() { - //do nothing + // do nothing } } diff --git a/DDMockiOS/DDMockSettingsBundleHelper.swift b/DDMockiOS/DDMockSettingsBundleHelper.swift index 7a96082..712aa9e 100644 --- a/DDMockiOS/DDMockSettingsBundleHelper.swift +++ b/DDMockiOS/DDMockSettingsBundleHelper.swift @@ -1,45 +1,27 @@ import Foundation -class DDMockSettingsBundleHelper { - private static let statusCode = "_status_code" - private static let responseTime = "_response_time" - private static let endpoint = "_endpoint" - private static let mockFile = "_mock_file" - private static let useRealApi = "_use_real_api" - private static let globalUseRealApis = "use_real_apis" - - static func getSelectedMockFile(key: String) -> Int { - return UserDefaults.standard.integer(forKey: getSettingsBundleKey(key: key) + mockFile) - } - - static func getStatusCode(key: String) -> Int { - let userDefaultKey = getSettingsBundleKey(key: key) + statusCode - if (UserDefaults.standard.object(forKey: userDefaultKey) == nil) { - return MockEntry.defaultStatusCode - } else { - return UserDefaults.standard.integer(forKey: userDefaultKey) - } - } - - static func getResponseTime(key: String) -> Int { - let userDefaultKey = getSettingsBundleKey(key: key) + responseTime - if (UserDefaults.standard.object(forKey: userDefaultKey) == nil) { - return MockEntry.defaultResponseTime - } else { - return UserDefaults.standard.integer(forKey: userDefaultKey) - } - } - - static func useRealAPI(key: String) -> Bool { - let userDefaultKey = getSettingsBundleKey(key: key) + useRealApi - return UserDefaults.standard.object(forKey: userDefaultKey) as? Bool ?? false +class UserDefaultsHelper { + enum SettingsKey: String { + case statusCode = "_status_code" + case responseTime = "_response_time" + case endpoint = "_endpoint" + case mockFile = "_mock_file" + case useRealApi = "_use_real_api" + case globalUseRealApis = "use_real_apis" } - static func globalUseRealAPIs() -> Bool { - return UserDefaults.standard.object(forKey: globalUseRealApis) as? Bool ?? false + static func getInteger(key: String, item: SettingsKey) -> Int { + let key = getSettingsBundleKey(key: key) + item.rawValue + return UserDefaults.standard.integer(forKey: key) } - - private static func getSettingsBundleKey(key: String) -> String { - return key.replacingOccurrences(of: "/", with: ".") + + static func getObject(key: String, item: SettingsKey) -> T? { + let key = getSettingsBundleKey(key: key) + item.rawValue + return UserDefaults.standard.object(forKey: key) as? T } } + +// replaces / with . for some reason +private func getSettingsBundleKey(key: String) -> String { + return key.replacingOccurrences(of: "/", with: ".") +} diff --git a/DDMockiOS/DDMockiOS.h b/DDMockiOS/DDMockiOS.h index 69ce424..23a7855 100644 --- a/DDMockiOS/DDMockiOS.h +++ b/DDMockiOS/DDMockiOS.h @@ -3,6 +3,7 @@ // DDMockiOS // // Created by Bunduwongse, Natalie (AU - Sydney) on 18/3/19. +// todo: update license // Copyright © 2019 Bunduwongse, Natalie (AU - Sydney). All rights reserved. // @@ -14,6 +15,5 @@ FOUNDATION_EXPORT double DDMockiOSVersionNumber; //! Project version string for DDMockiOS. FOUNDATION_EXPORT const unsigned char DDMockiOSVersionString[]; +// todo: helpful - but to be removed // In this header, you should import all the public headers of your framework using statements like #import - - diff --git a/DDMockiOS/MockEntry.swift b/DDMockiOS/MockEntry.swift index 38ddfa5..a683dd5 100644 --- a/DDMockiOS/MockEntry.swift +++ b/DDMockiOS/MockEntry.swift @@ -1,34 +1,59 @@ import Foundation struct MockEntry: Codable { - internal static let defaultResponseTime = 400 - internal static let defaultStatusCode = 200 - + // internal? why not private + private static let defaultResponseTime = 400 + private static let defaultStatusCode = 200 + + // ok let path: String - var files = [String]() + + var files: [String] = [] var selectedFile = 0 + + // private var statusCode = defaultStatusCode var responseTime = defaultResponseTime - + + init(path: String, files: [String]) { self.path = path self.files = files } - + + // this is where all the "settings bundle helper" is used + // why is this an int? func getSelectedFile() -> Int { - return DDMockSettingsBundleHelper.getSelectedMockFile(key: path) + return UserDefaultsHelper.getInteger(key: path, item: .mockFile) } - + + // get status code for an entry func getStatusCode() -> Int { - return DDMockSettingsBundleHelper.getStatusCode(key: path) + return UserDefaultsHelper.getObject( + key: path, + item: .statusCode) ?? MockEntry.defaultStatusCode } - + + // get use real api func useRealAPI() -> Bool { - return DDMockSettingsBundleHelper.useRealAPI(key: path) || - DDMockSettingsBundleHelper.globalUseRealAPIs() + return Self.getGlobalUseRealAPIs() + || getEndpointUseRealAPI(key: path) } - + + // get response time func getResponseTime() -> Int { - return DDMockSettingsBundleHelper.getResponseTime(key: path) + return UserDefaultsHelper.getObject( + key: path, + item: .responseTime) ?? MockEntry.defaultResponseTime + + } + + private func getEndpointUseRealAPI(key: String) -> Bool { + return UserDefaultsHelper.getObject(key: key, item: .useRealApi) ?? false + } + + // read global from user defaults + static func getGlobalUseRealAPIs() -> Bool { + return UserDefaultsHelper.getObject(key: "", item: .globalUseRealApis) ?? false } } diff --git a/DDMockiOS/py/ddmock.py b/DDMockiOS/py/ddmock.py new file mode 100755 index 0000000..52950e3 --- /dev/null +++ b/DDMockiOS/py/ddmock.py @@ -0,0 +1,248 @@ +import os +import shutil +import sys +import plistlib +import logging +import json + +# todo: document args + +# todo: is this not passed straight to main +# path to mockfiles passed as argument +# e.g. IOOF/Resources/mockfiles +mock_files_location = sys.argv[1] + +# todo: dynamic / configuration +# is this from cwd or root of project? +# todo: move to correct location +settings_location = "DDMockiOS/Settings.bundle/" + +# create the map of endpoints from mockfiles +def generate_map(): + map = {} + + ## walks all the mockfiles and for each creates just the leading path? + + # recursive directory traversal + # todo: what is subdir + # damn dynamic languages + for subdir, dirs, files in os.walk(mock_files_location): + print(subdir) + + # iterate through mockfiles + for file in files: + + # create path + filepath = subdir + os.sep + file + + # only the json files + if filepath.endswith(".json"): + endpointPath = subdir.replace(mock_files_location, "") + + # strip the leading slash if present + # todo: presumably this is always present? wt + if endpointPath.startswith("/"): + endpointPath = endpointPath.replace("/", "", 1) + + # map is accessed here (therefore make this a function duh) + if endpointPath in map: + files = map[endpointPath] + files.append(file) + else: + map[endpointPath] = [file] + + return map + + +def main(): + map = generate_map() + + # todo: can we log as part of build process? is that what this does? + # todo: yes it is + print("Creating map of endpoint paths and mock files...") + + # start creating settings bundle + print("Creating Settings.bundle...") + + # create file if it doesn't exist + if not os.path.exists(settings_location): + os.makedirs(settings_location) + + + # todo: update to use the plist interpreter module and a model plist + + # Root plist file + root = '' + root = root + '\n' + root = root + '\n' + root = root + "\n" + root = root + "\n\tStringsTable" + root = root + "\n\tRoot" + root = root + "\n\tPreferenceSpecifiers" + root = root + "\n\t" + root = root + "\n\t\t" + root = root + "\n\t\t\tType" + root = root + "\n\t\t\t\tPSToggleSwitchSpecifier" + root = root + "\n\t\t\t\tTitle" + root = root + "\n\t\t\t\tUse real APIs" + root = root + "\n\t\t\t\tKey" + root = root + "\n\t\t\t\tuse_real_apis" + root = root + "\n\t\t\t\tDefaultValue" + root = root + "\n\t\t\t\t" + root = root + "\n\t\t" + root = root + "\n\t\t" + root = root + "\n\t\t\tType" + root = root + "\n\t\t\tPSChildPaneSpecifier" + root = root + "\n\t\t\tFile" + root = root + "\n\t\t\tgeneral" + root = root + "\n\t\t\tTitle" + root = root + "\n\t\t\tGeneral" + root = root + "\n\t\t" + root = root + "\n\t\t" + root = root + "\n\t\t\tType" + root = root + "\n\t\t\tPSGroupSpecifier" + root = root + "\n\t\t\tTitle" + root = root + "\n\t\t\tMOCK" + root = root + "\n\t\t" + + # Endpoints plist file + plist = '' + plist = plist + '\n' + plist = plist + '\n' + plist = plist + "\n" + plist = plist + "\n\tPreferenceSpecifiers" + plist = plist + "\n\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t\tType" + plist = plist + "\n\t\t\tPSToggleSwitchSpecifier" + plist = plist + "\n\t\t\tTitle" + plist = plist + "\n\t\t\tUse real API" + plist = plist + "\n\t\t\tKey" + plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" + plist = plist + "\n\t\t\tDefaultValue" + plist = plist + "\n\t\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t\tDefaultValue" + plist = plist + "\n\t\t\t$endpointPathName" + plist = plist + "\n\t\t\tType" + plist = plist + "\n\t\t\tPSTitleValueSpecifier" + plist = plist + "\n\t\t\tTitle" + plist = plist + "\n\t\t\tEndpoint" + plist = plist + "\n\t\t\tKey" + plist = plist + "\n\t\t\t$endpointPathKey_endpoint" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t\tType" + plist = plist + "\n\t\t\tPSTextFieldSpecifier" + plist = plist + "\n\t\t\tDefaultValue" + plist = plist + "\n\t\t\t400" + plist = plist + "\n\t\t\tTitle" + plist = plist + "\n\t\t\tResponse Time (ms)" + plist = plist + "\n\t\t\tKey" + plist = plist + "\n\t\t\t$endpointPathKey_response_time" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t\tType" + plist = plist + "\n\t\t\tPSTextFieldSpecifier" + plist = plist + "\n\t\t\tDefaultValue" + plist = plist + "\n\t\t\t200" + plist = plist + "\n\t\t\tTitle" + plist = plist + "\n\t\t\tStatus Code" + plist = plist + "\n\t\t\tKey" + plist = plist + "\n\t\t\t$endpointPathKey_status_code" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t\t\tType" + plist = plist + "\n\t\t\tPSMultiValueSpecifier" + plist = plist + "\n\t\t\tTitle" + plist = plist + "\n\t\t\tMock file" + plist = plist + "\n\t\t\tKey" + plist = plist + "\n\t\t\t$endpointPathKey_mock_file" + plist = plist + "\n\t\t\tDefaultValue" + plist = plist + "\n\t\t\t0" + plist = plist + "\n\t\t\tValues" + plist = plist + "\n\t\t\t" + plist = plist + "\n\t\t\t\t$indexMockFiles" + plist = plist + "\n\t\t\t" + plist = plist + "\n\t\t\tTitles" + plist = plist + "\n\t\t\t" + plist = plist + "\n\t\t\t\t$mockFiles" + plist = plist + "\n\t\t\t" + plist = plist + "\n\t\t" + plist = plist + "\n\t" + plist = plist + "\n" + plist = plist + "\n" + + for endpointPath, files in map.items(): + filename = endpointPath.replace("/", ".") + # add endpoint to root plist + root = root + "\n\t\t" + root = root + "\n\t\t\tType" + root = root + "\n\t\t\tPSChildPaneSpecifier" + root = root + "\n\t\t\tFile" + root = root + "\n\t\t\t" + filename + "" + root = root + "\n\t\t\tTitle" + root = root + "\n\t\t\t" + filename + "" + root = root + "\n\t\t" + + print("Creating plist file for " + endpointPath + "...") + + # todo: endpoints added to plist here + with open(settings_location + filename + ".plist", "w+") as fout: + newplist = plist + + newplist = newplist.replace("$endpointPathName", endpointPath).replace( + "$endpointPathKey", filename) + + indexes = "0" + for i in range(1, len(files)): + indexes = indexes + "\n\t\t\t\t" + \ + str(i) + "" + newplist = newplist.replace("$indexMockFiles", indexes) + + mockFiles = "" + files[0] + "" + for i in range(1, len(files)): + mockFiles = mockFiles + "\n\t\t\t\t" + \ + files[i] + "" + newplist = newplist.replace("$mockFiles", mockFiles) + + fout.write(newplist) + + # insert: mb generate general plist? even from some config? + + # general_plist_path = "DDMockiOS/DDMockiOS/general.plist" + + + # create general plist from json + with open("DDMockiOS/resources/general.json", "r") as general: + with open(os.path.join(settings_location, "general.plist"), "wb") as output: + plist = plistlib.dump(general.read(), output, fmt=plistlib.FMT_XML) + + # copy static file + # failing here because it's not from cwd or a variable + # todo: some var for location + # copies the "general.plist" + # todo: generate from the lib + # shutil.copyfile(general_plist_path, + # os.path.join(settings_location, "general.plist")) + + # copies from one static path to another (pointlessly?) + + # create root plist + # todo: create this from a dictionary + print("Creating root plist...") + root = root + "\n\t" + root = root + "\n" + root = root + "\n" + + # write root plist + with open(settings_location + "Root.plist", "w+") as fout: + fout.write(root) + + # finished + print("Done!") + + +if __name__ == "__main__": + main() diff --git a/DDMockiOS/py/src/plist.py b/DDMockiOS/py/src/plist.py new file mode 100644 index 0000000..1252e6c --- /dev/null +++ b/DDMockiOS/py/src/plist.py @@ -0,0 +1,16 @@ +import plistlib +import json + +def create_general_plist(): + print("creating plist in future") + + +def plist_to_json(path): + + with open(path, "rb") as general: + # string = general.read() + # print(string) + plist = plistlib.load(general, fmt=plistlib.FMT_XML) + print(plist) + with open("general.json", "w") as output: + json.dump(plist, output, indent=4) \ No newline at end of file diff --git a/DDMockiOS/py/src/swagger_to_plist.py b/DDMockiOS/py/src/swagger_to_plist.py new file mode 100644 index 0000000..21eda21 --- /dev/null +++ b/DDMockiOS/py/src/swagger_to_plist.py @@ -0,0 +1,4 @@ +# convert swagger api spec into ddmock plist + +def swagger_to_plist(): + print("swag") \ No newline at end of file diff --git a/DDMockiOS/resources/general.json b/DDMockiOS/resources/general.json new file mode 100644 index 0000000..2b43178 --- /dev/null +++ b/DDMockiOS/resources/general.json @@ -0,0 +1,56 @@ +{ + "PreferenceSpecifiers": [ + { + "Type": "PSToggleSwitchSpecifier", + "Title": "Stubbed Token Refresh Succeeds", + "Key": "stubbed_token_refresh_success", + "DefaultValue": true + }, + { + "Type": "PSTextFieldSpecifier", + "Title": "Stubbed Token Refresh Delay (ms)", + "Key": "stubbed_token_refresh_delay", + "DefaultValue": 100 + }, + { + "Type": "PSGroupSpecifier", + "Title": "Launch Options" + }, + { + "Type": "PSToggleSwitchSpecifier", + "Title": "Force Jailbroken", + "Key": "launch_options.force_jailbroken", + "DefaultValue": false + }, + { + "Type": "PSToggleSwitchSpecifier", + "Title": "Force Upgrade", + "Key": "launch_options.force_upgrade", + "DefaultValue": false + }, + { + "Type": "PSToggleSwitchSpecifier", + "Title": "Is Subsequent Login", + "Key": "isSubsequentLogin", + "DefaultValue": false + }, + { + "Type": "PSMultiValueSpecifier", + "Title": "is_sqa_enabled", + "Key": "isSQAEnabled_debug", + "DefaultValue": "TRUE", + "Values": [ + "TRUE", + "FALSE", + "ONE", + "TWO" + ], + "Titles": [ + "TRUE (already set)", + "FALSE (must be set)", + "ONE (can skip once)", + "TWO (can skip twice)" + ] + } + ] +} \ No newline at end of file diff --git a/DDMockiOS/resources/general.plist b/DDMockiOS/resources/general.plist new file mode 100644 index 0000000..c6a7323 --- /dev/null +++ b/DDMockiOS/resources/general.plist @@ -0,0 +1,89 @@ + + + + + PreferenceSpecifiers + + + Type + PSToggleSwitchSpecifier + Title + Stubbed Token Refresh Succeeds + Key + stubbed_token_refresh_success + DefaultValue + + + + Type + PSTextFieldSpecifier + Title + Stubbed Token Refresh Delay (ms) + Key + stubbed_token_refresh_delay + DefaultValue + 100 + + + Type + PSGroupSpecifier + Title + Launch Options + + + Type + PSToggleSwitchSpecifier + Title + Force Jailbroken + Key + launch_options.force_jailbroken + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Force Upgrade + Key + launch_options.force_upgrade + DefaultValue + + + + Type + PSToggleSwitchSpecifier + Title + Is Subsequent Login + Key + isSubsequentLogin + DefaultValue + + + + Type + PSMultiValueSpecifier + Title + is_sqa_enabled + Key + isSQAEnabled_debug + DefaultValue + TRUE + Values + + TRUE + FALSE + ONE + TWO + + Titles + + TRUE (already set) + FALSE (must be set) + ONE (can skip once) + TWO (can skip twice) + + + + + From 8e6687f888654c49ecad607b463d7ae4a59b00ec Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 14 May 2021 07:51:29 +1000 Subject: [PATCH 02/26] change min supported version --- DDMockiOS.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 8d48c0c..20025a9 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -422,7 +422,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -481,7 +481,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 10.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; From 2ad4b5a762e6d4f5035a549db89d655f7366b23e Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 10:55:32 +1000 Subject: [PATCH 03/26] in progress rf + examples + docs --- DDMockiOS.xcodeproj/project.pbxproj | 18 +- DDMockiOS/DDMock.swift | 142 +++++------ DDMockiOS/DDMockProtocol.swift | 39 ++- DDMockiOS/DDMockURLProtocolClass.swift | 130 ++++++++++ DDMockiOS/String+Regex.swift | 35 +++ DDMockiOS/py/ddmock.py | 228 +++++++++++------- .../mockfiles/example/get/01_success.json | 3 + DDMockiOS/resources/root.json | 20 ++ init-mocks.py | 160 ------------ 9 files changed, 433 insertions(+), 342 deletions(-) create mode 100644 DDMockiOS/DDMockURLProtocolClass.swift create mode 100644 DDMockiOS/String+Regex.swift create mode 100644 DDMockiOS/resources/mockfiles/example/get/01_success.json create mode 100644 DDMockiOS/resources/root.json delete mode 100644 init-mocks.py diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 20025a9..8bb9b40 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -10,13 +10,15 @@ 52C27F802609C1A600D04B74 /* DDMockiOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52C27F7F2609C1A600D04B74 /* DDMockiOSTests.swift */; }; 52C27F822609C1A600D04B74 /* DDMockiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9118D8B1223F1AE300195DC1 /* DDMockiOS.framework */; }; 9118D8B6223F1AE300195DC1 /* DDMockiOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 9118D8C0223F1B4C00195DC1 /* DDMockProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BC223F1B4C00195DC1 /* DDMockProtocol.swift */; }; + 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */; }; 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMock.swift */; }; 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */; }; C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; C9334FC2264D1CB200190EB7 /* plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC0264D1CB200190EB7 /* plist.py */; }; C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */; }; + C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9334FC7264E998D00190EB7 /* String+Regex.swift */; }; + C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -37,11 +39,10 @@ 52C27F8D2609C1F600D04B74 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 52C27F8E2609C20400D04B74 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 52C27F8F2609C20400D04B74 /* DDMockiOS.podspec */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; path = DDMockiOS.podspec; sourceTree = ""; }; - 52C27F902609C23700D04B74 /* init-mocks.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = "init-mocks.py"; sourceTree = ""; }; 9118D8B1223F1AE300195DC1 /* DDMockiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = DDMockiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DDMockiOS.h; sourceTree = ""; }; 9118D8B5223F1AE300195DC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9118D8BC223F1B4C00195DC1 /* DDMockProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockProtocol.swift; sourceTree = ""; }; + 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockURLProtocolClass.swift; sourceTree = ""; }; 9118D8BD223F1B4C00195DC1 /* DDMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMock.swift; sourceTree = ""; }; 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEntry.swift; sourceTree = ""; }; 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockSettingsBundleHelper.swift; sourceTree = ""; }; @@ -50,6 +51,8 @@ C9334FBC264D1AB500190EB7 /* general.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = general.json; sourceTree = ""; }; C9334FC0264D1CB200190EB7 /* plist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = plist.py; sourceTree = ""; }; C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = swagger_to_plist.py; sourceTree = ""; }; + C9334FC7264E998D00190EB7 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; + C9334FDC265611E100190EB7 /* mockfiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = mockfiles; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -83,7 +86,6 @@ 52C27F8A2609C1AE00D04B74 /* Pod */ = { isa = PBXGroup; children = ( - 52C27F902609C23700D04B74 /* init-mocks.py */, 52C27F8F2609C20400D04B74 /* DDMockiOS.podspec */, 52C27F8E2609C20400D04B74 /* LICENSE */, ); @@ -123,10 +125,11 @@ isa = PBXGroup; children = ( 9118D8BD223F1B4C00195DC1 /* DDMock.swift */, - 9118D8BC223F1B4C00195DC1 /* DDMockProtocol.swift */, + 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */, 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, + C9334FC7264E998D00190EB7 /* String+Regex.swift */, 9118D8B5223F1AE300195DC1 /* Info.plist */, C9334F98264CC11100190EB7 /* py */, C9334FBF264D1C9200190EB7 /* resources */, @@ -163,6 +166,7 @@ C9334FBF264D1C9200190EB7 /* resources */ = { isa = PBXGroup; children = ( + C9334FDC265611E100190EB7 /* mockfiles */, C9334FBC264D1AB500190EB7 /* general.json */, C9334FB3264CF90400190EB7 /* general.plist */, ); @@ -271,6 +275,7 @@ files = ( C9334FBD264D1AB500190EB7 /* general.json in Resources */, C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */, + C9334FDD265611E100190EB7 /* mockfiles in Resources */, C9334FC2264D1CB200190EB7 /* plist.py in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -312,7 +317,8 @@ buildActionMask = 2147483647; files = ( 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */, - 9118D8C0223F1B4C00195DC1 /* DDMockProtocol.swift in Sources */, + C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */, + 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */, 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */, 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */, ); diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index 41a8287..0403f2d 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -1,45 +1,64 @@ import Foundation -/* - DDMock - +/** + This is the main DDMock entry point. */ +public final class DDMock { -// -public class DDMock { - - // path under resources directory - + /// path under resources directory private let mockDirectory = "/mockfiles" + /// map private var mockEntries: [String: MockEntry] = [:] - // Enforces mocks only and no API fall-through - private(set) var strict: Bool = false + /// enforces mocks only and no API fall-through + internal var strict: Bool = false - // chronological order of paths - public private(set) var matchedPaths: [String] = [] + // todo: this should be thread safe + // and have a max size + /// chronological order of paths + private(set) var matchedPaths: [String] = [] - public var onMissingMock: (_ path: String?) -> Void = {path in + /// needed for singleton + private init() {} + + /** + Assignable handler when a mock is not present in strict mode. + By default this is a panic! + */ + public var onMissingMock: (_ path: String?) -> Void = { path in fatalError("missing stub for path: \(path ?? "")") } - // todo: no singletons in libraries + // todo: remove the singleton if possible, require a single instance + /// Singleton instance of DDMock public static let shared = DDMock() - // initialise DDMock library - // todo: kinda not great maybe + /** + Initialise DDMock library + This must be called on the DDMock.shared singleton + by the client before DDMock can be used. + */ public func initialise(strict: Bool = false) { + // todo: kinda not great maybe + // this is called by the client on the singleton? archaic self.strict = strict - let docsPath = Bundle.main.resourcePath! + mockDirectory - let fileManager = FileManager.default - fileManager - .enumerator(atPath: docsPath)? - .forEach{ + // todo: resource path + let path = Bundle.main.resourcePath! + mockDirectory + + // parse the files in the mock directory + readMockFiles(path: path, fm: FileManager.default) + + } + + private func readMockFiles(path: String, fm: FileManager) { + fm + .enumerator(atPath: path)? + .forEach { if let e = $0 as? String, let url = URL(string: e), @@ -50,6 +69,7 @@ public class DDMock { } } + /// reset the history public func clearHistory() { matchedPaths.removeAll() } @@ -58,6 +78,7 @@ public class DDMock { let fileName = "/" + url.lastPathComponent let key = url.path.replacingOccurrences(of: fileName, with: "") + // separate the assignment if var entry = mockEntries[key] { entry.files.append(url.path) mockEntries[key] = entry @@ -67,7 +88,10 @@ public class DDMock { } } - private func mockEntry(for path: String, isTest: Bool) -> MockEntry? { + /** + get the mock entry + */ + private func getMockEntry(path: String, isTest: Bool) -> MockEntry? { let entry = mockEntries[path] ?? getRegexEntry(path: path) guard !isTest else { return entry @@ -77,12 +101,21 @@ public class DDMock { onMissingMock(path) } // Here we log the entries so that clients (like a unit test) can verify a call was made. + // todo: this is guarded by isTest flag so doesn't apply to tests + // todo: remove istest flag matchedPaths.append(path) return entry } - func hasMockEntry(path: String, method: String) -> EntrySetting { - switch getMockEntry(path: path, method: method, isTest: true)?.useRealAPI() { + /// + func getMockEntryByPath(path: String, method: String) -> MockEntry? { + return getMockEntryInternal(path: path, method: method, isTest: false) + } + + + // todo: what is the point of EntrySetting + func hasMockEntryByPath(path: String, method: String) -> EntrySetting { + switch getMockEntryInternal(path: path, method: method, isTest: true)?.useRealAPI() { case .none: return .notFound case .some(false): @@ -92,51 +125,30 @@ public class DDMock { } } - func getMockEntry(path: String, method: String) -> MockEntry? { - return getMockEntry(path: path, method: method, isTest: false) - } - - // - func mockPath(request: URLRequest) -> String? { - if let url = request.url, - let method = request.httpMethod { - return mockPath(path: url.path, method: method) - } else { + // called by the two above functions + private func getMockEntryInternal(path: String, method: String, isTest: Bool) -> MockEntry? { + guard + let path = getMockPath(path: path, method: method) else { return nil } + return getMockEntry(path: path, isTest: isTest) } + // - func mockPath(path: String, method: String) -> String? { + private func getMockPath(path: String, method: String) -> String? { return path.replacingRegexMatches( pattern: "^/", replaceWith: "") + "/" + method.lowercased() } - private func getMockEntry(path: String, method: String, isTest: Bool) -> MockEntry? { - guard - let path = mockPath(path: path, method: method) else { - return nil - } - return mockEntry(for: path, isTest: isTest) - } - - func hasMockEntry(request: URLRequest) -> EntrySetting { - guard let path = request.url?.path, let method = request.httpMethod else { return .notFound } - return hasMockEntry(path: path, method: method) - } - - func getMockEntry(request: URLRequest) -> MockEntry? { - guard let path = request.url?.path, let method = request.httpMethod else { return nil } - return getMockEntry(path: path, method: method, isTest: false) - } private func getRegexEntry(path: String) -> MockEntry? { var matches = [MockEntry]() for key in mockEntries.keys { if (key.contains("_")) { let regex = key.replacingRegexMatches(pattern: "_[^/]*_", replaceWith: "[^/]*") - if (path.matches(regex)) { + if path.matches(regex) { if let match = mockEntries[key] { matches.append(match) } @@ -161,27 +173,3 @@ public class DDMock { return data } } - -// todo: extension on string idk -extension String { - - func matches(_ regex: String) -> Bool { - return range(of: regex, options: .regularExpression, range: nil, locale: nil) != nil - } - - func replacingRegexMatches( - pattern: String, - replaceWith: String = "") -> String { - - var newString = "" - do { - let regex = try NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options.caseInsensitive) - let range = NSMakeRange(0, self.count) - newString = regex.stringByReplacingMatches(in: self, options: [], range: range, withTemplate: replaceWith) - } - catch { - debugPrint("Error \(error)") - } - return newString - } -} diff --git a/DDMockiOS/DDMockProtocol.swift b/DDMockiOS/DDMockProtocol.swift index 410dcb0..1a9dd68 100644 --- a/DDMockiOS/DDMockProtocol.swift +++ b/DDMockiOS/DDMockProtocol.swift @@ -8,16 +8,24 @@ enum EntrySetting { } // implements URLProtocol for some reason -public class DDMockProtocol: URLProtocol { +public class DDMockProtocolClass: URLProtocol { + // convenience function + public static func protocolClass(_ protocolClasses: [AnyClass])-> [AnyClass] { + var protocolClasses = protocolClasses + protocolClasses.insert(DDMockProtocolClass.self, at: 0) + return protocolClasses + } // initiailise: note this is mutating config and not returning + // todo: remove public static func initialise(config: inout URLSessionConfiguration) { var protocolClasses = config.protocolClasses ?? [] - protocolClasses.insert(DDMockProtocol.self, at: 0) + protocolClasses.insert(DDMockProtocolClass.self, at: 0) config.protocolClasses = protocolClasses } + // todo: what is this switch override public class func canInit(with request: URLRequest) -> Bool { switch DDMock.shared.hasMockEntry(request: request) { case .mocked: return true @@ -26,6 +34,7 @@ public class DDMockProtocol: URLProtocol { } // canonical request does nothing atm + // todo: what is this override public class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } @@ -44,31 +53,47 @@ public class DDMockProtocol: URLProtocol { // create mock response let data: Data? = DDMock.shared.getData(entry) - var headers = [String: String]() + + // header dictionary + var headers: [String: String] = [:] + + // content type + // todo headers["Content-Type"] = "application/json" if let data = data { headers["Content-Length"] = "\(data.count)" } - let response = HTTPURLResponse(url: self.request.url!, statusCode: entry.getStatusCode(), httpVersion: "HTTP/1.1", headerFields: headers)! + + // create response + // todo: dynamically + let response = HTTPURLResponse( + url: request.url!, + statusCode: entry.getStatusCode(), + httpVersion: "HTTP/1.1", + headerFields: headers)! // Simulate response time + // todo: check thread model Thread.sleep(forTimeInterval: TimeInterval(entry.getResponseTime() / 1000)) // send response - client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) + client!.urlProtocol( + self, + didReceive: response, + cacheStoragePolicy: .notAllowed) // send response data if available if let data = data { client!.urlProtocol(self, didLoad: data) } - // finish up + // finish loading client!.urlProtocolDidFinishLoading(self) } } } override public func stopLoading() { - // do nothing + // nothing is ever in flight so always do nothing } } diff --git a/DDMockiOS/DDMockURLProtocolClass.swift b/DDMockiOS/DDMockURLProtocolClass.swift new file mode 100644 index 0000000..019d629 --- /dev/null +++ b/DDMockiOS/DDMockURLProtocolClass.swift @@ -0,0 +1,130 @@ +import Foundation + +// ? +enum EntrySetting { + case notFound + case mocked + case useRealAPI +} + +// implements URLProtocol for some reason +public class DDMockURLProtocolClass: URLProtocol { + + // convenience function + // todo: this api doesn't make sense + public static func insertProtocolClass(_ protocolClasses: [AnyClass])-> [AnyClass] { + var protocolClasses = protocolClasses + protocolClasses.insert( + DDMockURLProtocolClass.self, + at: 0) + return protocolClasses + } + + // todo: what is this switch + override public class func canInit(with request: URLRequest) -> Bool { + // this canInit is the only place that calls hasMockEntry + guard + let path = request.url?.path, + let method = request.httpMethod else { + return false + } + + switch DDMock.shared.hasMockEntryByPath(path: path, method: method) { + case .mocked: return true + case .notFound, .useRealAPI: return false + } + } + + // canonical request does nothing atm + // todo: what is this + override public class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + // todo: allow headers to be cnfigurable + func getMockHeaders(contentLength: Int?) -> [String: String] { + var headers: [String: String] = [:] + // content type + // todo: get these from somewhere + headers["Content-Type"] = "application/json" + if let contentLength = contentLength { + headers["Content-Length"] = "\(contentLength)" + } + return headers + } + + func createMockResponse( + url: URL, + statusCode: Int, + headers: [String: String]) -> HTTPURLResponse? { + + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: "HTTP/1.1", + headerFields: headers) + } + + override public func startLoading() { + // copy bang to local scope + let client = self.client! + + // fetch item + guard + let path = request.url?.path, + let method = request.httpMethod, + let url = request.url else { + + return + } + + // todo: remove singleton + // this is the only thing that is used i think + guard let entry = DDMock.shared.getMockEntryByPath( + path: path, + method: method) else { + + return + } + + // create mock response + // todo: check in what case is this nil + // todo: also remove singleton + let data: Data? = DDMock.shared.getData(entry) + + // header dictionary + let headers = getMockHeaders(contentLength: data?.count) + + // create response + guard let response = createMockResponse( + url: url, + statusCode: entry.getStatusCode(), + headers: headers) else { + + return + } + + // Simulate response time + // todo: check threading + let time = TimeInterval(entry.getResponseTime() / 1000) + Thread.sleep(forTimeInterval: time) + + // send response + client.urlProtocol( + self, + didReceive: response, + cacheStoragePolicy: .notAllowed) + + // send response data if available + if let data = data { + client.urlProtocol(self, didLoad: data) + } + + // finish loading + client.urlProtocolDidFinishLoading(self) + } + + override public func stopLoading() { + // nothing is ever in flight so always do nothing + } +} diff --git a/DDMockiOS/String+Regex.swift b/DDMockiOS/String+Regex.swift new file mode 100644 index 0000000..262e34f --- /dev/null +++ b/DDMockiOS/String+Regex.swift @@ -0,0 +1,35 @@ + +// todo: extension on string idk +extension String { + + func matches(_ regex: String) -> Bool { + let matches = range( + of: regex, + options: .regularExpression, + range: nil, + locale: nil) + return matches != nil + } + + func replacingRegexMatches( + pattern: String, + replaceWith: String = "") -> String { + + var newString = "" + do { + let regex = try NSRegularExpression( + pattern: pattern, + options: NSRegularExpression.Options.caseInsensitive) + let range = NSMakeRange(0, count) + newString = regex.stringByReplacingMatches( + in: self, + options: [], + range: range, + withTemplate: replaceWith) + } + catch { + debugPrint("Error \(error)") + } + return newString + } +} diff --git a/DDMockiOS/py/ddmock.py b/DDMockiOS/py/ddmock.py index 52950e3..028258f 100755 --- a/DDMockiOS/py/ddmock.py +++ b/DDMockiOS/py/ddmock.py @@ -4,29 +4,18 @@ import plistlib import logging import json - -# todo: document args - -# todo: is this not passed straight to main -# path to mockfiles passed as argument -# e.g. IOOF/Resources/mockfiles -mock_files_location = sys.argv[1] - -# todo: dynamic / configuration -# is this from cwd or root of project? -# todo: move to correct location -settings_location = "DDMockiOS/Settings.bundle/" +import argparse # create the map of endpoints from mockfiles -def generate_map(): +def generate_map(mockfiles_path): map = {} ## walks all the mockfiles and for each creates just the leading path? # recursive directory traversal # todo: what is subdir - # damn dynamic languages - for subdir, dirs, files in os.walk(mock_files_location): + # damn dynamic types + for subdir, dirs, files in os.walk(mockfiles_path): print(subdir) # iterate through mockfiles @@ -37,7 +26,7 @@ def generate_map(): # only the json files if filepath.endswith(".json"): - endpointPath = subdir.replace(mock_files_location, "") + endpointPath = subdir.replace(mockfiles_path, "") # strip the leading slash if present # todo: presumably this is always present? wt @@ -54,56 +43,80 @@ def generate_map(): return map -def main(): - map = generate_map() +def load_root_json(): + with open("DDMockiOS/resources/root.json", "r") as root: + return json.load(root) + + +def load_endpoint_json(): + with open("DDMockiOS/resources/root.json", "r") as root: + return json.load(root) + +def main(mockfiles_path): + + # todo: is this not passed straight to main + # path to mockfiles passed as argument + # e.g. IOOF/Resources/mockfiles - # todo: can we log as part of build process? is that what this does? - # todo: yes it is + # first create the map + # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") + map = generate_map(mockfiles_path) # start creating settings bundle + # todo: what is the settings bundle & where are we creating it? print("Creating Settings.bundle...") - # create file if it doesn't exist + # todo: args in python are weird, need to check their usage + + # todo: dynamic / configuration + # is this from cwd or root of project? + # todo: this should come from arguments + settings_location = "DDMockiOS/Settings.bundle/" + + + # Settings.bundle is really just a directory + # first create directory if it doesn't exist if not os.path.exists(settings_location): os.makedirs(settings_location) - - # todo: update to use the plist interpreter module and a model plist + # create root plist + print("Creating root plist...") + root = load_root_json() # Root plist file - root = '' - root = root + '\n' - root = root + '\n' - root = root + "\n" - root = root + "\n\tStringsTable" - root = root + "\n\tRoot" - root = root + "\n\tPreferenceSpecifiers" - root = root + "\n\t" - root = root + "\n\t\t" - root = root + "\n\t\t\tType" - root = root + "\n\t\t\t\tPSToggleSwitchSpecifier" - root = root + "\n\t\t\t\tTitle" - root = root + "\n\t\t\t\tUse real APIs" - root = root + "\n\t\t\t\tKey" - root = root + "\n\t\t\t\tuse_real_apis" - root = root + "\n\t\t\t\tDefaultValue" - root = root + "\n\t\t\t\t" - root = root + "\n\t\t" - root = root + "\n\t\t" - root = root + "\n\t\t\tType" - root = root + "\n\t\t\tPSChildPaneSpecifier" - root = root + "\n\t\t\tFile" - root = root + "\n\t\t\tgeneral" - root = root + "\n\t\t\tTitle" - root = root + "\n\t\t\tGeneral" - root = root + "\n\t\t" - root = root + "\n\t\t" - root = root + "\n\t\t\tType" - root = root + "\n\t\t\tPSGroupSpecifier" - root = root + "\n\t\t\tTitle" - root = root + "\n\t\t\tMOCK" - root = root + "\n\t\t" + # root = '' + # root = root + '\n' + # root = root + '\n' + # root = root + "\n" + # root = root + "\n\tStringsTable" + # root = root + "\n\tRoot" + # root = root + "\n\tPreferenceSpecifiers" + # root = root + "\n\t" + # root = root + "\n\t\t" + # root = root + "\n\t\t\tType" + # root = root + "\n\t\t\t\tPSToggleSwitchSpecifier" + # root = root + "\n\t\t\t\tTitle" + # root = root + "\n\t\t\t\tUse real APIs" + # root = root + "\n\t\t\t\tKey" + # root = root + "\n\t\t\t\tuse_real_apis" + # root = root + "\n\t\t\t\tDefaultValue" + # root = root + "\n\t\t\t\t" + # root = root + "\n\t\t" + # root = root + "\n\t\t" + # root = root + "\n\t\t\tType" + # root = root + "\n\t\t\tPSChildPaneSpecifier" + # root = root + "\n\t\t\tFile" + # root = root + "\n\t\t\tgeneral" + # root = root + "\n\t\t\tTitle" + # root = root + "\n\t\t\tGeneral" + # root = root + "\n\t\t" + # root = root + "\n\t\t" + # root = root + "\n\t\t\tType" + # root = root + "\n\t\t\tPSGroupSpecifier" + # root = root + "\n\t\t\tTitle" + # root = root + "\n\t\t\tMOCK" + # root = root + "\n\t\t" # Endpoints plist file plist = '' @@ -118,7 +131,7 @@ def main(): plist = plist + "\n\t\t\tTitle" plist = plist + "\n\t\t\tUse real API" plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" + plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" # $endpointPathKey plist = plist + "\n\t\t\tDefaultValue" plist = plist + "\n\t\t\t" plist = plist + "\n\t\t" @@ -130,7 +143,7 @@ def main(): plist = plist + "\n\t\t\tTitle" plist = plist + "\n\t\t\tEndpoint" plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_endpoint" + plist = plist + "\n\t\t\t$endpointPathKey_endpoint" # $endpointPathKey plist = plist + "\n\t\t" plist = plist + "\n\t\t" plist = plist + "\n\t\t\tType" @@ -140,7 +153,7 @@ def main(): plist = plist + "\n\t\t\tTitle" plist = plist + "\n\t\t\tResponse Time (ms)" plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_response_time" + plist = plist + "\n\t\t\t$endpointPathKey_response_time" # $endpointPathKey plist = plist + "\n\t\t" plist = plist + "\n\t\t" plist = plist + "\n\t\t\tType" @@ -150,7 +163,7 @@ def main(): plist = plist + "\n\t\t\tTitle" plist = plist + "\n\t\t\tStatus Code" plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_status_code" + plist = plist + "\n\t\t\t$endpointPathKey_status_code" # $endpointPathKey plist = plist + "\n\t\t" plist = plist + "\n\t\t" plist = plist + "\n\t\t\tType" @@ -158,12 +171,12 @@ def main(): plist = plist + "\n\t\t\tTitle" plist = plist + "\n\t\t\tMock file" plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_mock_file" + plist = plist + "\n\t\t\t$endpointPathKey_mock_file" # $endpointPathKey plist = plist + "\n\t\t\tDefaultValue" plist = plist + "\n\t\t\t0" plist = plist + "\n\t\t\tValues" plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t\t\t$indexMockFiles" + plist = plist + "\n\t\t\t\t$indexMockFiles" # $indexMockFiles plist = plist + "\n\t\t\t" plist = plist + "\n\t\t\tTitles" plist = plist + "\n\t\t\t" @@ -174,38 +187,61 @@ def main(): plist = plist + "\n" plist = plist + "\n" + # ** short circuit for testing + + with open(settings_location + "Root.plist", "rb") as root: + with open("DDMockiOS/resources/root.json", "w+") as output: + plist = plistlib.load(root) + json.dump(plist, output, indent=4) + print("dumped root json") + return + + # ** + + # for path & files in map for endpointPath, files in map.items(): + + # replaces the slashes with periods for filename = endpointPath.replace("/", ".") + + print(root) + # add endpoint to root plist - root = root + "\n\t\t" - root = root + "\n\t\t\tType" - root = root + "\n\t\t\tPSChildPaneSpecifier" - root = root + "\n\t\t\tFile" - root = root + "\n\t\t\t" + filename + "" - root = root + "\n\t\t\tTitle" - root = root + "\n\t\t\t" + filename + "" - root = root + "\n\t\t" + new_item = {} + new_item['Type'] = 'PSChildPaneSpecifier' + new_item['File'] = filename + new_item['Title'] = filename + + root['PreferenceSpecifiers'].append(new_item) + + + # create a copy of the endpoint plist replacing + # the $endpointPathName key -> endpointPath + # the $indexMockFiles key -> indexes + # the $mockfiles key -> files[i] + # then write the new file at settings_location + filename + .plist + # creating plist file for endpoint print("Creating plist file for " + endpointPath + "...") # todo: endpoints added to plist here with open(settings_location + filename + ".plist", "w+") as fout: newplist = plist - newplist = newplist.replace("$endpointPathName", endpointPath).replace( - "$endpointPathKey", filename) + # newplist = newplist.replace("$endpointPathName", endpointPath).replace( + # "$endpointPathKey", filename) - indexes = "0" - for i in range(1, len(files)): - indexes = indexes + "\n\t\t\t\t" + \ - str(i) + "" - newplist = newplist.replace("$indexMockFiles", indexes) + # indexes = "0" + # for i in range(1, len(files)): + # indexes = indexes + "\n\t\t\t\t" + \ + # str(i) + "" + # newplist = newplist.replace("$indexMockFiles", indexes) - mockFiles = "" + files[0] + "" - for i in range(1, len(files)): - mockFiles = mockFiles + "\n\t\t\t\t" + \ - files[i] + "" - newplist = newplist.replace("$mockFiles", mockFiles) + # mockFiles = "" + files[0] + "" + # for i in range(1, len(files)): + # mockFiles = mockFiles + "\n\t\t\t\t" + \ + # files[i] + "" + # newplist = newplist.replace("$mockFiles", mockFiles) fout.write(newplist) @@ -215,10 +251,11 @@ def main(): # create general plist from json + # this could be from + print("Creating general.plist...") with open("DDMockiOS/resources/general.json", "r") as general: with open(os.path.join(settings_location, "general.plist"), "wb") as output: - plist = plistlib.dump(general.read(), output, fmt=plistlib.FMT_XML) - + plistlib.dump(general.read(), output, fmt=plistlib.FMT_XML) # copy static file # failing here because it's not from cwd or a variable # todo: some var for location @@ -229,20 +266,27 @@ def main(): # copies from one static path to another (pointlessly?) - # create root plist - # todo: create this from a dictionary - print("Creating root plist...") - root = root + "\n\t" - root = root + "\n" - root = root + "\n" + # close the plist + # root = root + "\n\t" + # root = root + "\n" + # root = root + "\n" # write root plist - with open(settings_location + "Root.plist", "w+") as fout: - fout.write(root) + with open(settings_location + "Root.plist", "wb") as output: + print("Writing root plist...") + plistlib.dump(root, output, fmt=plistlib.FMT_XML) # finished print("Done!") if __name__ == "__main__": - main() + + # parse arguments + parser = argparse.ArgumentParser(description='Generate Settings.bundle for DDMockiOS') + parser.add_argument('mockfiles_path', nargs='?', default="DDMockiOS/resources/mockfiles") + + args = parser.parse_args() + + # start execution + main(args.mockfiles_path) diff --git a/DDMockiOS/resources/mockfiles/example/get/01_success.json b/DDMockiOS/resources/mockfiles/example/get/01_success.json new file mode 100644 index 0000000..9feac80 --- /dev/null +++ b/DDMockiOS/resources/mockfiles/example/get/01_success.json @@ -0,0 +1,3 @@ +{ + "message": "good job" +} diff --git a/DDMockiOS/resources/root.json b/DDMockiOS/resources/root.json new file mode 100644 index 0000000..bf244a7 --- /dev/null +++ b/DDMockiOS/resources/root.json @@ -0,0 +1,20 @@ +{ + "PreferenceSpecifiers": [ + { + "DefaultValue": true, + "Key": "use_real_apis", + "Title": "Use real APIs", + "Type": "PSToggleSwitchSpecifier" + }, + { + "File": "general", + "Title": "General", + "Type": "PSChildPaneSpecifier" + }, + { + "Title": "MOCK", + "Type": "PSGroupSpecifier" + } + ], + "StringsTable": "Root" +} \ No newline at end of file diff --git a/init-mocks.py b/init-mocks.py deleted file mode 100644 index cc6c454..0000000 --- a/init-mocks.py +++ /dev/null @@ -1,160 +0,0 @@ -import os -import shutil -import sys - -mock_files_location = sys.argv[1] -settings_location = "DDMockiOS/Settings.bundle/" - -map = {} - -print "Creating map of endpoint paths and mock files..." -for subdir, dirs, files in os.walk(mock_files_location): - for file in files: - filepath = subdir + os.sep + file - - if filepath.endswith(".json"): - endpointPath = subdir.replace(mock_files_location, "") - if endpointPath.startswith("/"): - endpointPath = endpointPath.replace("/", "", 1) - if endpointPath in map: - files = map[endpointPath] - files.append(file) - else: - map[endpointPath] = [file] - -print "Creating Settings.bundle..." -if not os.path.exists(settings_location): - os.makedirs(settings_location) - -# Root plist file -root = '' -root = root + '\n' -root = root + '\n' -root = root + "\n" -root = root + "\n\tStringsTable" -root = root + "\n\tRoot" -root = root + "\n\tPreferenceSpecifiers" -root = root + "\n\t" -root = root + "\n\t\t" -root = root + "\n\t\t\tType" -root = root + "\n\t\t\t\tPSToggleSwitchSpecifier" -root = root + "\n\t\t\t\tTitle" -root = root + "\n\t\t\t\tUse real APIs" -root = root + "\n\t\t\t\tKey" -root = root + "\n\t\t\t\tuse_real_apis" -root = root + "\n\t\t\t\tDefaultValue" -root = root + "\n\t\t\t\t" -root = root + "\n\t\t" -root = root + "\n\t\t" -root = root + "\n\t\t\tType" -root = root + "\n\t\t\tPSGroupSpecifier" -root = root + "\n\t\t\tTitle" -root = root + "\n\t\t\tMOCK" -root = root + "\n\t\t" - -# Endpoints plist file -plist = '' -plist = plist + '\n' -plist = plist + '\n' -plist = plist + "\n" -plist = plist + "\n\tPreferenceSpecifiers" -plist = plist + "\n\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t\tType" -plist = plist + "\n\t\t\tPSToggleSwitchSpecifier" -plist = plist + "\n\t\t\tTitle" -plist = plist + "\n\t\t\tUse real API" -plist = plist + "\n\t\t\tKey" -plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" -plist = plist + "\n\t\t\tDefaultValue" -plist = plist + "\n\t\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t\tDefaultValue" -plist = plist + "\n\t\t\t$endpointPathName" -plist = plist + "\n\t\t\tType" -plist = plist + "\n\t\t\tPSTitleValueSpecifier" -plist = plist + "\n\t\t\tTitle" -plist = plist + "\n\t\t\tEndpoint" -plist = plist + "\n\t\t\tKey" -plist = plist + "\n\t\t\t$endpointPathKey_endpoint" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t\tType" -plist = plist + "\n\t\t\tPSTextFieldSpecifier" -plist = plist + "\n\t\t\tDefaultValue" -plist = plist + "\n\t\t\t400" -plist = plist + "\n\t\t\tTitle" -plist = plist + "\n\t\t\tResponse Time (ms)" -plist = plist + "\n\t\t\tKey" -plist = plist + "\n\t\t\t$endpointPathKey_response_time" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t\tType" -plist = plist + "\n\t\t\tPSTextFieldSpecifier" -plist = plist + "\n\t\t\tDefaultValue" -plist = plist + "\n\t\t\t200" -plist = plist + "\n\t\t\tTitle" -plist = plist + "\n\t\t\tStatus Code" -plist = plist + "\n\t\t\tKey" -plist = plist + "\n\t\t\t$endpointPathKey_status_code" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t\t\tType" -plist = plist + "\n\t\t\tPSMultiValueSpecifier" -plist = plist + "\n\t\t\tTitle" -plist = plist + "\n\t\t\tMock file" -plist = plist + "\n\t\t\tKey" -plist = plist + "\n\t\t\t$endpointPathKey_mock_file" -plist = plist + "\n\t\t\tDefaultValue" -plist = plist + "\n\t\t\t0" -plist = plist + "\n\t\t\tValues" -plist = plist + "\n\t\t\t" -plist = plist + "\n\t\t\t\t$indexMockFiles" -plist = plist + "\n\t\t\t" -plist = plist + "\n\t\t\tTitles" -plist = plist + "\n\t\t\t" -plist = plist + "\n\t\t\t\t$mockFiles" -plist = plist + "\n\t\t\t" -plist = plist + "\n\t\t" -plist = plist + "\n\t" -plist = plist + "\n" -plist = plist + "\n" - -for endpointPath, files in map.items(): - filename = endpointPath.replace("/", ".") - # add endpoint to root plist - root = root + "\n\t\t" - root = root + "\n\t\t\tType" - root = root + "\n\t\t\tPSChildPaneSpecifier" - root = root + "\n\t\t\tFile" - root = root + "\n\t\t\t" + filename + "" - root = root + "\n\t\t\tTitle" - root = root + "\n\t\t\t" + filename + "" - root = root + "\n\t\t" - - print "Creating plist file for " + endpointPath + "..." - with open(settings_location + filename + ".plist", "w+") as fout: - newplist = plist - - newplist = newplist.replace("$endpointPathName", endpointPath).replace("$endpointPathKey", filename) - - indexes = "0" - for i in range(1, len(files)): - indexes = indexes + "\n\t\t\t\t" + str(i) + "" - newplist = newplist.replace("$indexMockFiles", indexes) - - mockFiles = "" + files[0] + "" - for i in range(1, len(files)): - mockFiles = mockFiles + "\n\t\t\t\t" + files[i] + "" - newplist = newplist.replace("$mockFiles", mockFiles) - - fout.write(newplist) - -print "Creating root plist..." -root = root + "\n\t" -root = root + "\n" -root = root + "\n" -with open(settings_location + "Root.plist", "w+") as fout: - fout.write(root) -print "Done!" From fb815d58b094a0b76de7663e765b20daba36654a Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 12:46:48 +1000 Subject: [PATCH 04/26] in progress rf 2, make static + move to helpers + rename internals --- DDMockiOS.xcodeproj/project.pbxproj | 12 +- DDMockiOS/DDMock.swift | 37 ++++-- DDMockiOS/DDMockURLProtocolClass.swift | 116 +++++++++--------- DDMockiOS/ResponseHelper.swift | 31 +++++ ...eHelper.swift => UserDefaultsHelper.swift} | 0 5 files changed, 121 insertions(+), 75 deletions(-) create mode 100644 DDMockiOS/ResponseHelper.swift rename DDMockiOS/{DDMockSettingsBundleHelper.swift => UserDefaultsHelper.swift} (100%) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 8bb9b40..cd07789 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -13,12 +13,13 @@ 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */; }; 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMock.swift */; }; 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; - 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */; }; + 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */; }; C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; C9334FC2264D1CB200190EB7 /* plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC0264D1CB200190EB7 /* plist.py */; }; C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */; }; C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9334FC7264E998D00190EB7 /* String+Regex.swift */; }; C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; + C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,7 +46,7 @@ 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockURLProtocolClass.swift; sourceTree = ""; }; 9118D8BD223F1B4C00195DC1 /* DDMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMock.swift; sourceTree = ""; }; 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEntry.swift; sourceTree = ""; }; - 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockSettingsBundleHelper.swift; sourceTree = ""; }; + 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsHelper.swift; sourceTree = ""; }; C9334FAE264CF8A800190EB7 /* ddmock.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ddmock.py; sourceTree = ""; }; C9334FB3264CF90400190EB7 /* general.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = general.plist; sourceTree = ""; }; C9334FBC264D1AB500190EB7 /* general.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = general.json; sourceTree = ""; }; @@ -53,6 +54,7 @@ C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = swagger_to_plist.py; sourceTree = ""; }; C9334FC7264E998D00190EB7 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; C9334FDC265611E100190EB7 /* mockfiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = mockfiles; sourceTree = ""; }; + C9815D95267060EF0056AC3E /* ResponseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHelper.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -126,7 +128,8 @@ children = ( 9118D8BD223F1B4C00195DC1 /* DDMock.swift */, 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, - 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */, + C9815D95267060EF0056AC3E /* ResponseHelper.swift */, + 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, C9334FC7264E998D00190EB7 /* String+Regex.swift */, @@ -316,10 +319,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */, + 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */, C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */, 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */, 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */, + C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */, 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index 0403f2d..bccd5cc 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -44,7 +44,6 @@ public final class DDMock { public func initialise(strict: Bool = false) { // todo: kinda not great maybe // this is called by the client on the singleton? archaic - self.strict = strict // todo: resource path @@ -74,11 +73,14 @@ public final class DDMock { matchedPaths.removeAll() } + // should be a "get or insert" function in the map private func createMockEntry(url: URL) { + // todo: check this matching let fileName = "/" + url.lastPathComponent let key = url.path.replacingOccurrences(of: fileName, with: "") - // separate the assignment + + // todo: separate the assignment if var entry = mockEntries[key] { entry.files.append(url.path) mockEntries[key] = entry @@ -107,22 +109,29 @@ public final class DDMock { return entry } - /// - func getMockEntryByPath(path: String, method: String) -> MockEntry? { + /// wraps internal to always use false for isTest param + internal func getMockEntryByPath(path: String, method: String) -> MockEntry? { return getMockEntryInternal(path: path, method: method, isTest: false) } - // todo: what is the point of EntrySetting - func hasMockEntryByPath(path: String, method: String) -> EntrySetting { - switch getMockEntryInternal(path: path, method: method, isTest: true)?.useRealAPI() { - case .none: - return .notFound - case .some(false): - return .mocked - case .some(true): - return .useRealAPI + /// todo: doc + internal func hasMockEntryByPath(path: String, method: String) -> Bool { + + // idk what the idea of this map function useRealAPI + + // get the entry + // todo: cache + guard let entry = getMockEntryInternal( + path: path, + method: method, + isTest: true) else { + + return false } + + // entry can override this value itself + return entry.useRealAPI() } // called by the two above functions @@ -143,6 +152,7 @@ public final class DDMock { } + // todo: high complexity function in the public interface private func getRegexEntry(path: String) -> MockEntry? { var matches = [MockEntry]() for key in mockEntries.keys { @@ -161,6 +171,7 @@ public final class DDMock { return matches.first } + // todo: this response should be configurable somehow like the header func getData(_ entry: MockEntry) -> Data? { var data: Data? = nil let f = entry.files[entry.getSelectedFile()] diff --git a/DDMockiOS/DDMockURLProtocolClass.swift b/DDMockiOS/DDMockURLProtocolClass.swift index 019d629..7d051bf 100644 --- a/DDMockiOS/DDMockURLProtocolClass.swift +++ b/DDMockiOS/DDMockURLProtocolClass.swift @@ -1,18 +1,19 @@ import Foundation -// ? -enum EntrySetting { - case notFound - case mocked - case useRealAPI -} +/** + Implementation of NSURLProtocol used to intercept requests. + Needs to be inserted into the list protocal classes ... -// implements URLProtocol for some reason + */ public class DDMockURLProtocolClass: URLProtocol { - // convenience function - // todo: this api doesn't make sense - public static func insertProtocolClass(_ protocolClasses: [AnyClass])-> [AnyClass] { + /** + convenience function to insert + todo: more detail and change this interface somehow, check what others do + */ + public static func insertProtocolClass( + _ protocolClasses: [AnyClass])-> [AnyClass] { + var protocolClasses = protocolClasses protocolClasses.insert( DDMockURLProtocolClass.self, @@ -20,54 +21,38 @@ public class DDMockURLProtocolClass: URLProtocol { return protocolClasses } - // todo: what is this switch - override public class func canInit(with request: URLRequest) -> Bool { - // this canInit is the only place that calls hasMockEntry + // todo: is this called for every request? is the mock retreived 2ce? + /// + public override class func canInit(with task: URLSessionTask) -> Bool { guard - let path = request.url?.path, - let method = request.httpMethod else { + let req = task.currentRequest, + let path = req.url?.path, + let method = req.httpMethod else { + return false } - switch DDMock.shared.hasMockEntryByPath(path: path, method: method) { - case .mocked: return true - case .notFound, .useRealAPI: return false - } + // this canInit is the only place that calls hasMockEntry + // this actually retreives the mock as part of its execution + // todo: caching + return DDMock.shared.hasMockEntryByPath(path: path, method: method) } - // canonical request does nothing atm - // todo: what is this + /** + The canonical version of a request is used to lookup objects in the URL cache. + This process performs equality checks between URLRequest instances. + + This is an abstract class by default. + */ override public class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - // todo: allow headers to be cnfigurable - func getMockHeaders(contentLength: Int?) -> [String: String] { - var headers: [String: String] = [:] - // content type - // todo: get these from somewhere - headers["Content-Type"] = "application/json" - if let contentLength = contentLength { - headers["Content-Length"] = "\(contentLength)" - } - return headers - } - - func createMockResponse( - url: URL, - statusCode: Int, - headers: [String: String]) -> HTTPURLResponse? { - - return HTTPURLResponse( - url: url, - statusCode: statusCode, - httpVersion: "HTTP/1.1", - headerFields: headers) - } - + // todo: move logic to correct lifecycle point + /** + this is where everything happens + */ override public func startLoading() { - // copy bang to local scope - let client = self.client! // fetch item guard @@ -78,8 +63,10 @@ public class DDMockURLProtocolClass: URLProtocol { return } + // note: remove singleton could just mean restrict its usage to + // within the public interface boundary or make it more explicit + // todo: remove singleton - // this is the only thing that is used i think guard let entry = DDMock.shared.getMockEntryByPath( path: path, method: method) else { @@ -88,27 +75,41 @@ public class DDMockURLProtocolClass: URLProtocol { } // create mock response - // todo: check in what case is this nil + // todo: check in what case could this be nil // todo: also remove singleton let data: Data? = DDMock.shared.getData(entry) // header dictionary - let headers = getMockHeaders(contentLength: data?.count) + // todo: more configuration + let headers = ResponseHelper.getMockHeaders(contentLength: data?.count) + + // get status code + let statusCode = entry.getStatusCode() // create response - guard let response = createMockResponse( + guard let response = ResponseHelper.createMockResponse( url: url, - statusCode: entry.getStatusCode(), + statusCode: statusCode, headers: headers) else { return } - // Simulate response time - // todo: check threading + // simulate response time + // todo: use timer instead of sleep let time = TimeInterval(entry.getResponseTime() / 1000) Thread.sleep(forTimeInterval: time) + // finally send the mock response to the client + let client = self.client! + sendMockResponse(client: client, response: response, data: data) + } + + private func sendMockResponse( + client: URLProtocolClient, + response: HTTPURLResponse, + data: Data?) { + // send response client.urlProtocol( self, @@ -117,14 +118,13 @@ public class DDMockURLProtocolClass: URLProtocol { // send response data if available if let data = data { - client.urlProtocol(self, didLoad: data) + client.urlProtocol( + self, + didLoad: data) } // finish loading client.urlProtocolDidFinishLoading(self) } - override public func stopLoading() { - // nothing is ever in flight so always do nothing - } } diff --git a/DDMockiOS/ResponseHelper.swift b/DDMockiOS/ResponseHelper.swift new file mode 100644 index 0000000..2633c4a --- /dev/null +++ b/DDMockiOS/ResponseHelper.swift @@ -0,0 +1,31 @@ +import Foundation + +// todo: maybe think about replacing this with a builder +// todo: move this out of amorphous 'helper' +internal class ResponseHelper { + + // todo: allow headers to be configurable + static func getMockHeaders(contentLength: Int?) -> [String: String] { + var headers: [String: String] = [:] + // content type + // todo: get these from somewhere + headers["Content-Type"] = "application/json" + if let contentLength = contentLength { + headers["Content-Length"] = "\(contentLength)" + } + return headers + } + + static func createMockResponse( + url: URL, + statusCode: Int, + headers: [String: String]) -> HTTPURLResponse? { + + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: "HTTP/1.1", + headerFields: headers) + } + +} diff --git a/DDMockiOS/DDMockSettingsBundleHelper.swift b/DDMockiOS/UserDefaultsHelper.swift similarity index 100% rename from DDMockiOS/DDMockSettingsBundleHelper.swift rename to DDMockiOS/UserDefaultsHelper.swift From ba01705123dfecdcfdeab4bf7c519780e3f7e6af Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 13:36:25 +1000 Subject: [PATCH 05/26] in progress rf mock logic to repository + simplify api --- DDMockiOS.xcodeproj/project.pbxproj | 4 + DDMockiOS/DDMock.swift | 144 +++++++------------------ DDMockiOS/DDMockURLProtocolClass.swift | 4 +- DDMockiOS/MockEntry.swift | 4 +- DDMockiOS/MockRepository.swift | 108 +++++++++++++++++++ DDMockiOS/String+Regex.swift | 7 ++ 6 files changed, 162 insertions(+), 109 deletions(-) create mode 100644 DDMockiOS/MockRepository.swift diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index cd07789..b935da6 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9334FC7264E998D00190EB7 /* String+Regex.swift */; }; C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; + C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D9D2670635C0056AC3E /* MockRepository.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,7 @@ C9334FC7264E998D00190EB7 /* String+Regex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Regex.swift"; sourceTree = ""; }; C9334FDC265611E100190EB7 /* mockfiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = mockfiles; sourceTree = ""; }; C9815D95267060EF0056AC3E /* ResponseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHelper.swift; sourceTree = ""; }; + C9815D9D2670635C0056AC3E /* MockRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRepository.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -128,6 +130,7 @@ children = ( 9118D8BD223F1B4C00195DC1 /* DDMock.swift */, 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, + C9815D9D2670635C0056AC3E /* MockRepository.swift */, C9815D95267060EF0056AC3E /* ResponseHelper.swift */, 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, @@ -320,6 +323,7 @@ buildActionMask = 2147483647; files = ( 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */, + C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */, C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */, 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */, 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */, diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index bccd5cc..af9228c 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -3,16 +3,13 @@ import Foundation /** This is the main DDMock entry point. - */ public final class DDMock { + // todo: make this more obvious or configurable /// path under resources directory private let mockDirectory = "/mockfiles" - /// map - private var mockEntries: [String: MockEntry] = [:] - /// enforces mocks only and no API fall-through internal var strict: Bool = false @@ -25,17 +22,21 @@ public final class DDMock { private init() {} /** - Assignable handler when a mock is not present in strict mode. - By default this is a panic! + Assignable handler invoked when a mock is not present in strict mode. + By default this is a panic, strict mode users may want + to configure something more graceful. */ public var onMissingMock: (_ path: String?) -> Void = { path in fatalError("missing stub for path: \(path ?? "")") } // todo: remove the singleton if possible, require a single instance - /// Singleton instance of DDMock + /// singleton instance of DDMock public static let shared = DDMock() + /// repository for storing mocks + private let repository = MockRepository() + /** Initialise DDMock library This must be called on the DDMock.shared singleton @@ -43,7 +44,6 @@ public final class DDMock { */ public func initialise(strict: Bool = false) { // todo: kinda not great maybe - // this is called by the client on the singleton? archaic self.strict = strict // todo: resource path @@ -51,9 +51,12 @@ public final class DDMock { // parse the files in the mock directory readMockFiles(path: path, fm: FileManager.default) - } + /** + iterate through files & populate the mocks + todo: move out the "create mock entry" call + */ private func readMockFiles(path: String, fm: FileManager) { fm .enumerator(atPath: path)? @@ -63,124 +66,53 @@ public final class DDMock { let url = URL(string: e), url.pathExtension == "json" { - createMockEntry(url: url) + // does it open the file? no + // url is actually the path of the file (very strange) + repository.createMockEntry(url: url) } } } - /// reset the history - public func clearHistory() { - matchedPaths.removeAll() - } - - // should be a "get or insert" function in the map - private func createMockEntry(url: URL) { - - // todo: check this matching - let fileName = "/" + url.lastPathComponent - let key = url.path.replacingOccurrences(of: fileName, with: "") - - // todo: separate the assignment - if var entry = mockEntries[key] { - entry.files.append(url.path) - mockEntries[key] = entry - } - else { - mockEntries[key] = MockEntry(path: key, files: [url.path]) - } - } - - /** - get the mock entry - */ - private func getMockEntry(path: String, isTest: Bool) -> MockEntry? { - let entry = mockEntries[path] ?? getRegexEntry(path: path) - guard !isTest else { - return entry - } - // If strict mode is enabled, a missing entry is an error. Call handler. - if strict && entry == nil { - onMissingMock(path) - } - // Here we log the entries so that clients (like a unit test) can verify a call was made. - // todo: this is guarded by isTest flag so doesn't apply to tests - // todo: remove istest flag - matchedPaths.append(path) - return entry - } - - /// wraps internal to always use false for isTest param - internal func getMockEntryByPath(path: String, method: String) -> MockEntry? { - return getMockEntryInternal(path: path, method: method, isTest: false) + func hasEntry(path: String, method: String) -> Bool { + return repository.hasEntry(path: path, method: method) } - - /// todo: doc - internal func hasMockEntryByPath(path: String, method: String) -> Bool { - - // idk what the idea of this map function useRealAPI - + func getEntry(path: String, method: String) -> MockEntry? { // get the entry - // todo: cache - guard let entry = getMockEntryInternal( + guard + let entry = repository.getEntry( path: path, - method: method, - isTest: true) else { + method: method) else { - return false - } - - // entry can override this value itself - return entry.useRealAPI() - } - - // called by the two above functions - private func getMockEntryInternal(path: String, method: String, isTest: Bool) -> MockEntry? { - guard - let path = getMockPath(path: path, method: method) else { return nil } - return getMockEntry(path: path, isTest: isTest) - } + // add to history + matchedPaths.append(path) - // - private func getMockPath(path: String, method: String) -> String? { - return path.replacingRegexMatches( - pattern: "^/", - replaceWith: "") + "/" + method.lowercased() + // return the entry + return entry } - - // todo: high complexity function in the public interface - private func getRegexEntry(path: String) -> MockEntry? { - var matches = [MockEntry]() - for key in mockEntries.keys { - if (key.contains("_")) { - let regex = key.replacingRegexMatches(pattern: "_[^/]*_", replaceWith: "[^/]*") - if path.matches(regex) { - if let match = mockEntries[key] { - matches.append(match) - } - } - } - } - guard matches.count <= 1 else { - fatalError("Fatal Error: Multiple matches for regex entry.") - } - return matches.first + /// reset the history + public func clearHistory() { + matchedPaths.removeAll() } // todo: this response should be configurable somehow like the header + // todo: hide this func getData(_ entry: MockEntry) -> Data? { + var data: Data? = nil let f = entry.files[entry.getSelectedFile()] - do { - let docsPath = Bundle.main.resourcePath! + mockDirectory - data = try Data(contentsOf: URL(fileURLWithPath: "\(docsPath)/\(f)"), options: .mappedIfSafe) - } catch { - data = nil - } + + let docsPath = Bundle.main.resourcePath! + mockDirectory + let url = URL(fileURLWithPath: "\(docsPath)/\(f)") + + data = try? Data( + contentsOf: url, + options: .mappedIfSafe) + return data } } diff --git a/DDMockiOS/DDMockURLProtocolClass.swift b/DDMockiOS/DDMockURLProtocolClass.swift index 7d051bf..c529920 100644 --- a/DDMockiOS/DDMockURLProtocolClass.swift +++ b/DDMockiOS/DDMockURLProtocolClass.swift @@ -35,7 +35,7 @@ public class DDMockURLProtocolClass: URLProtocol { // this canInit is the only place that calls hasMockEntry // this actually retreives the mock as part of its execution // todo: caching - return DDMock.shared.hasMockEntryByPath(path: path, method: method) + return DDMock.shared.hasEntry(path: path, method: method) } /** @@ -67,7 +67,7 @@ public class DDMockURLProtocolClass: URLProtocol { // within the public interface boundary or make it more explicit // todo: remove singleton - guard let entry = DDMock.shared.getMockEntryByPath( + guard let entry = DDMock.shared.getEntry( path: path, method: method) else { diff --git a/DDMockiOS/MockEntry.swift b/DDMockiOS/MockEntry.swift index a683dd5..fd3b471 100644 --- a/DDMockiOS/MockEntry.swift +++ b/DDMockiOS/MockEntry.swift @@ -1,5 +1,8 @@ import Foundation +/** + mock entry struct + */ struct MockEntry: Codable { // internal? why not private private static let defaultResponseTime = 400 @@ -15,7 +18,6 @@ struct MockEntry: Codable { private var statusCode = defaultStatusCode var responseTime = defaultResponseTime - init(path: String, files: [String]) { self.path = path self.files = files diff --git a/DDMockiOS/MockRepository.swift b/DDMockiOS/MockRepository.swift new file mode 100644 index 0000000..83f7a2f --- /dev/null +++ b/DDMockiOS/MockRepository.swift @@ -0,0 +1,108 @@ +import Foundation + +// maybe protocol? +internal class MockRepository { + + /// map + private var mockEntries: [String: MockEntry] = [:] + + // should be a "get or insert" function in the map + func createMockEntry(url: URL) { + + // todo: check this matching + let fileName = "/" + url.lastPathComponent + let key = url.path.replacingOccurrences(of: fileName, with: "") + + // todo: separate the assignment + if var entry = mockEntries[key] { + entry.files.append(url.path) + mockEntries[key] = entry + } + else { + mockEntries[key] = MockEntry(path: key, files: [url.path]) + } + } + + /// todo: doc + func hasEntry(path: String, method: String) -> Bool { + + // idk what the idea of this map function useRealAPI + + // get the entry + // todo: cache + guard + let entry = getEntry(path: path, method: method) else { + + return false + } + + // entry can override this value itself + return entry.useRealAPI() + } + + /** + this returns an entry simply by path + need to include the method to get it in a way that makes sense + */ + func getEntry(path: String, method: String) -> MockEntry? { + let fullPath = path.replacingRegexMatches( + pattern: "^/", + replaceWith: "") + "/" + method.lowercased() + + // return an entry for either a non-wildcard or wildcard path + return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) + } + + /** + get the mock entry, respecting strict mode, "isTest" + */ + private func getMockEntry(path: String, strict: Bool, onMissing: (_ path: String?) -> Void) -> MockEntry? { + + // get the entry + let entry = getEntry(path: path, method: "") // todo + + // If strict mode is enabled, a missing entry is an error. Call handler. + // this will still fall through + if strict && entry == nil { + onMissing(path) + } + // Here we log the entries so that clients (like a unit test) can verify a call was made. + // todo: this is guarded by isTest flag so doesn't apply to tests + // todo: remove istest flag + + return entry + } + + // todo: simplify this a little + + private func getRegexEntry(path: String) -> MockEntry? { + // + var matches: [MockEntry] = [] + + // iterate through mock entry keys (what are these) + for key in mockEntries.keys { + + // if key contains _ + // this is to test if there is a wildcard to replace + // todo: mock entry should have its own regex + // shouldn't have to recompile for every request + if (key.contains("_")) { + + // replace matches of the wildcard with ... + let regex = key.replacingRegexMatches(pattern: "_[^/]*_", replaceWith: "[^/]*") + + // + if path.matches(regex) { + + if let match = mockEntries[key] { + matches.append(match) + } + } + } + } + guard matches.count <= 1 else { + fatalError("Fatal Error: Multiple matches for regex entry.") + } + return matches.first + } +} diff --git a/DDMockiOS/String+Regex.swift b/DDMockiOS/String+Regex.swift index 262e34f..aae3e2e 100644 --- a/DDMockiOS/String+Regex.swift +++ b/DDMockiOS/String+Regex.swift @@ -2,6 +2,9 @@ // todo: extension on string idk extension String { + /** + gets matches & checks they are not equal to nil + */ func matches(_ regex: String) -> Bool { let matches = range( of: regex, @@ -11,6 +14,10 @@ extension String { return matches != nil } + /** + Replace regex matches + replaceWith is the template (withTemplate) + */ func replacingRegexMatches( pattern: String, replaceWith: String = "") -> String { From 0b506db6e7c814bb583bdc5168f54e4d7864757b Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 13:56:20 +1000 Subject: [PATCH 06/26] rf complete separation of mock entry fns --- DDMockiOS/DDMock.swift | 4 +++- DDMockiOS/MockRepository.swift | 40 +++++++++++++++++++--------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index af9228c..1f8bd0c 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -82,7 +82,9 @@ public final class DDMock { guard let entry = repository.getEntry( path: path, - method: method) else { + method: method, + strict: strict, + onMissing: onMissingMock) else { return nil } diff --git a/DDMockiOS/MockRepository.swift b/DDMockiOS/MockRepository.swift index 83f7a2f..d5351fe 100644 --- a/DDMockiOS/MockRepository.swift +++ b/DDMockiOS/MockRepository.swift @@ -24,14 +24,14 @@ internal class MockRepository { } /// todo: doc - func hasEntry(path: String, method: String) -> Bool { + internal func hasEntry(path: String, method: String) -> Bool { // idk what the idea of this map function useRealAPI // get the entry // todo: cache guard - let entry = getEntry(path: path, method: method) else { + let entry = getMockEntry(path: path, method: method) else { return false } @@ -40,26 +40,17 @@ internal class MockRepository { return entry.useRealAPI() } - /** - this returns an entry simply by path - need to include the method to get it in a way that makes sense - */ - func getEntry(path: String, method: String) -> MockEntry? { - let fullPath = path.replacingRegexMatches( - pattern: "^/", - replaceWith: "") + "/" + method.lowercased() - - // return an entry for either a non-wildcard or wildcard path - return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) - } - /** get the mock entry, respecting strict mode, "isTest" */ - private func getMockEntry(path: String, strict: Bool, onMissing: (_ path: String?) -> Void) -> MockEntry? { + internal func getEntry( + path: String, + method: String, + strict: Bool, + onMissing: (_ path: String?) -> Void) -> MockEntry? { // get the entry - let entry = getEntry(path: path, method: "") // todo + let entry = getMockEntry(path: path, method: "") // todo // If strict mode is enabled, a missing entry is an error. Call handler. // this will still fall through @@ -73,6 +64,20 @@ internal class MockRepository { return entry } + /** + this returns an entry simply by path + need to include the method to get it in a way that makes sense + */ + private func getMockEntry(path: String, method: String) -> MockEntry? { + let fullPath = path.replacingRegexMatches( + pattern: "^/", + replaceWith: "") + "/" + method.lowercased() + + // return an entry for either a non-wildcard or wildcard path + return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) + } + + // todo: simplify this a little private func getRegexEntry(path: String) -> MockEntry? { @@ -80,6 +85,7 @@ internal class MockRepository { var matches: [MockEntry] = [] // iterate through mock entry keys (what are these) + // for key in mockEntries.keys { // if key contains _ From 10c928ddc327c889d1c85b6c88827fcfb52b4777 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 16:34:43 +1000 Subject: [PATCH 07/26] continue rf make entries immutable in repository --- DDMockiOS/DDMock.swift | 48 +++++++--------- DDMockiOS/MockEntry.swift | 16 ++++-- DDMockiOS/MockRepository.swift | 88 ++++++++++++++++++++++-------- DDMockiOS/UserDefaultsHelper.swift | 2 +- 4 files changed, 97 insertions(+), 57 deletions(-) diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index 1f8bd0c..ca4c6a4 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -35,7 +35,7 @@ public final class DDMock { public static let shared = DDMock() /// repository for storing mocks - private let repository = MockRepository() + private var repository: MockRepository! /** Initialise DDMock library @@ -43,40 +43,26 @@ public final class DDMock { by the client before DDMock can be used. */ public func initialise(strict: Bool = false) { - // todo: kinda not great maybe + // todo: more consistent configuration self.strict = strict // todo: resource path let path = Bundle.main.resourcePath! + mockDirectory - // parse the files in the mock directory - readMockFiles(path: path, fm: FileManager.default) + // load the files in the mock directory + repository = MockRepository(path: path, fm: FileManager.default) } /** - iterate through files & populate the mocks - todo: move out the "create mock entry" call + Check if an entry exists for a given path */ - private func readMockFiles(path: String, fm: FileManager) { - fm - .enumerator(atPath: path)? - .forEach { - if - let e = $0 as? String, - let url = URL(string: e), - url.pathExtension == "json" { - - // does it open the file? no - // url is actually the path of the file (very strange) - repository.createMockEntry(url: url) - } - } - } - func hasEntry(path: String, method: String) -> Bool { return repository.hasEntry(path: path, method: method) } + /** + Return the entry for a given path, if one exists + */ func getEntry(path: String, method: String) -> MockEntry? { // get the entry guard @@ -96,25 +82,29 @@ public final class DDMock { return entry } - /// reset the history + /** + reset the history + */ public func clearHistory() { matchedPaths.removeAll() } // todo: this response should be configurable somehow like the header // todo: hide this + // should know what the path is from the entry func getData(_ entry: MockEntry) -> Data? { - var data: Data? = nil - let f = entry.files[entry.getSelectedFile()] + let file = entry.getSelectedFile() + + // get the path + // todo: isn't this encoded in the entry? + let path = Bundle.main.resourcePath! + mockDirectory - let docsPath = Bundle.main.resourcePath! + mockDirectory - let url = URL(fileURLWithPath: "\(docsPath)/\(f)") + let url = URL(fileURLWithPath: "\(path)/\(file)") - data = try? Data( + return try? Data( contentsOf: url, options: .mappedIfSafe) - return data } } diff --git a/DDMockiOS/MockEntry.swift b/DDMockiOS/MockEntry.swift index fd3b471..25892e4 100644 --- a/DDMockiOS/MockEntry.swift +++ b/DDMockiOS/MockEntry.swift @@ -4,29 +4,35 @@ import Foundation mock entry struct */ struct MockEntry: Codable { - // internal? why not private + // constants private static let defaultResponseTime = 400 private static let defaultStatusCode = 200 // ok let path: String + // todo: less mutability var files: [String] = [] + + // todo: more thread safety var selectedFile = 0 // private var statusCode = defaultStatusCode + + // var responseTime = defaultResponseTime + /// init(path: String, files: [String]) { self.path = path self.files = files } - // this is where all the "settings bundle helper" is used - // why is this an int? - func getSelectedFile() -> Int { - return UserDefaultsHelper.getInteger(key: path, item: .mockFile) + // this is the index of the selected file in the files list for an entry + func getSelectedFile() -> String { + let index = UserDefaultsHelper.getInteger(key: path, item: .mockFile) + return files[index] } // get status code for an entry diff --git a/DDMockiOS/MockRepository.swift b/DDMockiOS/MockRepository.swift index d5351fe..b758124 100644 --- a/DDMockiOS/MockRepository.swift +++ b/DDMockiOS/MockRepository.swift @@ -1,26 +1,55 @@ import Foundation -// maybe protocol? +/** + Internal storage wrapper for mock entries. + To reinitialise a list of mocks, just create a new MockRepository + and drop the old one. + */ internal class MockRepository { - /// map - private var mockEntries: [String: MockEntry] = [:] + /// map storage of mock entries + private let mockEntries: [String: MockEntry] - // should be a "get or insert" function in the map - func createMockEntry(url: URL) { + /** + iterate through files & populate the mocks + */ + init(path: String, fm: FileManager) { + var entries: [String: MockEntry] = [:] - // todo: check this matching - let fileName = "/" + url.lastPathComponent - let key = url.path.replacingOccurrences(of: fileName, with: "") + // load mock files + fm + .enumerator(atPath: path)? + .forEach { - // todo: separate the assignment - if var entry = mockEntries[key] { - entry.files.append(url.path) - mockEntries[key] = entry - } - else { - mockEntries[key] = MockEntry(path: key, files: [url.path]) - } + guard + let path = $0 as? String, + let url = URL(string: path) else { + + return + } + guard + // todo: new file schema + url.pathExtension == "json" else { + + return + } + + // get the key + // todo: check absolute string is sane key + let key = url.deletingLastPathComponent().absoluteString + + // put into the dictionary + if var entry = entries[key] { + // add the mock to the existing file list for this entry + entry.files.append(url.path) + } + else { + // create a new entry + entries[key] = MockEntry(path: key, files: [url.path]) + } + } + + self.mockEntries = entries } /// todo: doc @@ -41,7 +70,7 @@ internal class MockRepository { } /** - get the mock entry, respecting strict mode, "isTest" + get the mock entry, respecting strict mode */ internal func getEntry( path: String, @@ -57,18 +86,23 @@ internal class MockRepository { if strict && entry == nil { onMissing(path) } - // Here we log the entries so that clients (like a unit test) can verify a call was made. - // todo: this is guarded by isTest flag so doesn't apply to tests - // todo: remove istest flag return entry } + /* + todo: consolidate mock entry types, add regex to mock entry itself + If regex entries were included in the same ds + hasMockEntry would look much simpler + and would not require retreiving the item + */ + /** this returns an entry simply by path need to include the method to get it in a way that makes sense */ private func getMockEntry(path: String, method: String) -> MockEntry? { + // method string is always lowercased let fullPath = path.replacingRegexMatches( pattern: "^/", replaceWith: "") + "/" + method.lowercased() @@ -77,7 +111,6 @@ internal class MockRepository { return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) } - // todo: simplify this a little private func getRegexEntry(path: String) -> MockEntry? { @@ -85,7 +118,9 @@ internal class MockRepository { var matches: [MockEntry] = [] // iterate through mock entry keys (what are these) - // + // these are the path without the filename, including method + // whatever we're looking for should be in the value + // to keep this lookup O(1) for key in mockEntries.keys { // if key contains _ @@ -106,9 +141,18 @@ internal class MockRepository { } } } + // maximum of 1 match or panic guard matches.count <= 1 else { fatalError("Fatal Error: Multiple matches for regex entry.") } + // return first or none return matches.first } + + /** + Create a mock entry for the given url and returns it. + If a mock entry exists for this path it returns a new entry with the + new path added. + // todo: can we do this all at once? + */ } diff --git a/DDMockiOS/UserDefaultsHelper.swift b/DDMockiOS/UserDefaultsHelper.swift index 712aa9e..a4fc1c8 100644 --- a/DDMockiOS/UserDefaultsHelper.swift +++ b/DDMockiOS/UserDefaultsHelper.swift @@ -21,7 +21,7 @@ class UserDefaultsHelper { } } -// replaces / with . for some reason +// replaces / with . for some undocumented reason private func getSettingsBundleKey(key: String) -> String { return key.replacingOccurrences(of: "/", with: ".") } From 945cc920710c2a8a6196cb8640146810d7b631f6 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 17:38:40 +1000 Subject: [PATCH 08/26] continue rf make send response static + py path in podspec --- DDMockiOS.podspec | 2 +- DDMockiOS/DDMock.swift | 19 ---------- DDMockiOS/DDMockURLProtocolClass.swift | 48 +++++++++----------------- DDMockiOS/ResponseHelper.swift | 44 ++++++++++++++++++++++- 4 files changed, 61 insertions(+), 52 deletions(-) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 1799c2d..490b0c3 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -32,7 +32,7 @@ Pod::Spec.new do |spec| spec.source_files = 'DDMockiOS' spec.preserve_paths = [ - 'init-mocks.py', + 'py/*', ] spec.swift_version = '5' diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift index ca4c6a4..a8df8be 100644 --- a/DDMockiOS/DDMock.swift +++ b/DDMockiOS/DDMock.swift @@ -88,23 +88,4 @@ public final class DDMock { public func clearHistory() { matchedPaths.removeAll() } - - // todo: this response should be configurable somehow like the header - // todo: hide this - // should know what the path is from the entry - func getData(_ entry: MockEntry) -> Data? { - - let file = entry.getSelectedFile() - - // get the path - // todo: isn't this encoded in the entry? - let path = Bundle.main.resourcePath! + mockDirectory - - let url = URL(fileURLWithPath: "\(path)/\(file)") - - return try? Data( - contentsOf: url, - options: .mappedIfSafe) - - } } diff --git a/DDMockiOS/DDMockURLProtocolClass.swift b/DDMockiOS/DDMockURLProtocolClass.swift index c529920..73ab64f 100644 --- a/DDMockiOS/DDMockURLProtocolClass.swift +++ b/DDMockiOS/DDMockURLProtocolClass.swift @@ -74,10 +74,9 @@ public class DDMockURLProtocolClass: URLProtocol { return } - // create mock response - // todo: check in what case could this be nil - // todo: also remove singleton - let data: Data? = DDMock.shared.getData(entry) + // get response data + // todo: check in what case could this be nil\\ + let data: Data? = ResponseHelper.getData(entry) // header dictionary // todo: more configuration @@ -96,35 +95,22 @@ public class DDMockURLProtocolClass: URLProtocol { } // simulate response time - // todo: use timer instead of sleep let time = TimeInterval(entry.getResponseTime() / 1000) - Thread.sleep(forTimeInterval: time) - // finally send the mock response to the client - let client = self.client! - sendMockResponse(client: client, response: response, data: data) - } - - private func sendMockResponse( - client: URLProtocolClient, - response: HTTPURLResponse, - data: Data?) { - - // send response - client.urlProtocol( - self, - didReceive: response, - cacheStoragePolicy: .notAllowed) - - // send response data if available - if let data = data { - client.urlProtocol( - self, - didLoad: data) - } - - // finish loading - client.urlProtocolDidFinishLoading(self) + // just use regular timer to async return the response + // async + await would be better + Timer.scheduledTimer( + withTimeInterval: time, + repeats: false, + block: + { timer in + // finally send the mock response to the client + ResponseHelper.sendMockResponse( + urlProtocol: self, + client: self.client!, + response: response, + data: data) + }) } } diff --git a/DDMockiOS/ResponseHelper.swift b/DDMockiOS/ResponseHelper.swift index 2633c4a..4db4959 100644 --- a/DDMockiOS/ResponseHelper.swift +++ b/DDMockiOS/ResponseHelper.swift @@ -2,7 +2,7 @@ import Foundation // todo: maybe think about replacing this with a builder // todo: move this out of amorphous 'helper' -internal class ResponseHelper { +internal struct ResponseHelper { // todo: allow headers to be configurable static func getMockHeaders(contentLength: Int?) -> [String: String] { @@ -28,4 +28,46 @@ internal class ResponseHelper { headerFields: headers) } + // todo: this response should be configurable somehow like the header + // todo: hide this + // should know what the path is from the entry + static func getData(_ entry: MockEntry) -> Data? { + + let file = entry.getSelectedFile() + + // get the path + // todo: isn't this encoded in the entry? + let path = entry.path + + let url = URL(fileURLWithPath: "\(path)/\(file)") + + return try? Data( + contentsOf: url, + options: .mappedIfSafe) + + } + + /// + static func sendMockResponse( + urlProtocol: URLProtocol, + client: URLProtocolClient, + response: HTTPURLResponse, + data: Data?) { + + // send response + client.urlProtocol( + urlProtocol, + didReceive: response, + cacheStoragePolicy: .notAllowed) + + // send response data if available + if let data = data { + client.urlProtocol( + urlProtocol, + didLoad: data) + } + + // finish loading + client.urlProtocolDidFinishLoading(urlProtocol) + } } From 60573b66a12d63886312e6ed015d7186c0fc6c76 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 17:50:10 +1000 Subject: [PATCH 09/26] revert rename of entry point file --- DDMockiOS.xcodeproj/project.pbxproj | 8 ++++---- DDMockiOS/{DDMock.swift => DDMockiOS.swift} | 0 2 files changed, 4 insertions(+), 4 deletions(-) rename DDMockiOS/{DDMock.swift => DDMockiOS.swift} (100%) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index b935da6..a641b17 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -11,7 +11,7 @@ 52C27F822609C1A600D04B74 /* DDMockiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9118D8B1223F1AE300195DC1 /* DDMockiOS.framework */; }; 9118D8B6223F1AE300195DC1 /* DDMockiOS.h in Headers */ = {isa = PBXBuildFile; fileRef = 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */; }; - 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMock.swift */; }; + 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */; }; 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */; }; C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; @@ -45,7 +45,7 @@ 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DDMockiOS.h; sourceTree = ""; }; 9118D8B5223F1AE300195DC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockURLProtocolClass.swift; sourceTree = ""; }; - 9118D8BD223F1B4C00195DC1 /* DDMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMock.swift; sourceTree = ""; }; + 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockiOS.swift; sourceTree = ""; }; 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockEntry.swift; sourceTree = ""; }; 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsHelper.swift; sourceTree = ""; }; C9334FAE264CF8A800190EB7 /* ddmock.py */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.python; path = ddmock.py; sourceTree = ""; }; @@ -128,7 +128,7 @@ 9118D8B3223F1AE300195DC1 /* DDMockiOS */ = { isa = PBXGroup; children = ( - 9118D8BD223F1B4C00195DC1 /* DDMock.swift */, + 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */, 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, C9815D9D2670635C0056AC3E /* MockRepository.swift */, C9815D95267060EF0056AC3E /* ResponseHelper.swift */, @@ -326,7 +326,7 @@ C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */, C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */, 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */, - 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */, + 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */, C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */, 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */, ); diff --git a/DDMockiOS/DDMock.swift b/DDMockiOS/DDMockiOS.swift similarity index 100% rename from DDMockiOS/DDMock.swift rename to DDMockiOS/DDMockiOS.swift From ccaa61d56e43e4bfa54b5453bf0f4fd1bb179099 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 17:52:55 +1000 Subject: [PATCH 10/26] remove dangling file --- DDMockiOS/DDMockProtocol.swift | 99 ---------------------------------- 1 file changed, 99 deletions(-) delete mode 100644 DDMockiOS/DDMockProtocol.swift diff --git a/DDMockiOS/DDMockProtocol.swift b/DDMockiOS/DDMockProtocol.swift deleted file mode 100644 index 1a9dd68..0000000 --- a/DDMockiOS/DDMockProtocol.swift +++ /dev/null @@ -1,99 +0,0 @@ -import Foundation - -// ? -enum EntrySetting { - case notFound - case mocked - case useRealAPI -} - -// implements URLProtocol for some reason -public class DDMockProtocolClass: URLProtocol { - - // convenience function - public static func protocolClass(_ protocolClasses: [AnyClass])-> [AnyClass] { - var protocolClasses = protocolClasses - protocolClasses.insert(DDMockProtocolClass.self, at: 0) - return protocolClasses - } - - // initiailise: note this is mutating config and not returning - // todo: remove - public static func initialise(config: inout URLSessionConfiguration) { - var protocolClasses = config.protocolClasses ?? [] - protocolClasses.insert(DDMockProtocolClass.self, at: 0) - config.protocolClasses = protocolClasses - } - - // todo: what is this switch - override public class func canInit(with request: URLRequest) -> Bool { - switch DDMock.shared.hasMockEntry(request: request) { - case .mocked: return true - case .notFound, .useRealAPI: return false - } - } - - // canonical request does nothing atm - // todo: what is this - override public class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request - } - - override public func startLoading() { - - // fetch item - if - let path = request.url?.path, - let method = request.httpMethod { - - // todo: singleton - if let entry = DDMock.shared.getMockEntry( - path: path, - method: method) { - - // create mock response - let data: Data? = DDMock.shared.getData(entry) - - // header dictionary - var headers: [String: String] = [:] - - // content type - // todo - headers["Content-Type"] = "application/json" - if let data = data { - headers["Content-Length"] = "\(data.count)" - } - - // create response - // todo: dynamically - let response = HTTPURLResponse( - url: request.url!, - statusCode: entry.getStatusCode(), - httpVersion: "HTTP/1.1", - headerFields: headers)! - - // Simulate response time - // todo: check thread model - Thread.sleep(forTimeInterval: TimeInterval(entry.getResponseTime() / 1000)) - - // send response - client!.urlProtocol( - self, - didReceive: response, - cacheStoragePolicy: .notAllowed) - - // send response data if available - if let data = data { - client!.urlProtocol(self, didLoad: data) - } - - // finish loading - client!.urlProtocolDidFinishLoading(self) - } - } - } - - override public func stopLoading() { - // nothing is ever in flight so always do nothing - } -} From ade7e23b2657af2b53db6f10da6759d130ff4d70 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 18:34:26 +1000 Subject: [PATCH 11/26] simplify podspec --- DDMockiOS.podspec | 15 +++------------ DDMockiOS.xcodeproj/project.pbxproj | 4 ---- DDMockiOS/py/src/swagger_to_plist.py | 2 +- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 490b0c3..1acb0e7 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -8,21 +8,12 @@ Pod::Spec.new do |spec| spec.name = 'DDMockiOS' - spec.version = '0.1.5' + spec.version = '2.0' spec.summary = 'Deloitte Digital simple network mocking library for iOS' -# This description is used to generate tags and improve search results. -# * Think: What does it do? Why did you write it? What is the focus? -# * Try to keep it short, snappy and to the point. -# * Write the description between the DESC delimiters below. -# * Finally, don't worry about the indent, CocoaPods strips it! - -# spec.description = <<-DESC -# Deloitte Digital simple network mocking library for iOS -# DESC + spec.description = 'Deloitte Digital simple network mocking library for iOS' spec.homepage = 'https://github.com/DeloitteDigitalAPAC/ddmock-ios' - # spec.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' spec.license = { :type => 'MIT', :file => 'LICENSE' } spec.author = 'Deloitte Digital Asia Pacific' spec.source = { :git => "https://github.com/DeloitteDigitalAPAC/ddmock-ios.git", :tag => 'v' + spec.version.to_s } @@ -32,7 +23,7 @@ Pod::Spec.new do |spec| spec.source_files = 'DDMockiOS' spec.preserve_paths = [ - 'py/*', + 'DDMockiOS/py/**', ] spec.swift_version = '5' diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index a641b17..4151677 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -15,8 +15,6 @@ 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */; }; C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; - C9334FC2264D1CB200190EB7 /* plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC0264D1CB200190EB7 /* plist.py */; }; - C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */ = {isa = PBXBuildFile; fileRef = C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */; }; C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9334FC7264E998D00190EB7 /* String+Regex.swift */; }; C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; @@ -280,9 +278,7 @@ buildActionMask = 2147483647; files = ( C9334FBD264D1AB500190EB7 /* general.json in Resources */, - C9334FC3264D1CB200190EB7 /* swagger_to_plist.py in Resources */, C9334FDD265611E100190EB7 /* mockfiles in Resources */, - C9334FC2264D1CB200190EB7 /* plist.py in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/DDMockiOS/py/src/swagger_to_plist.py b/DDMockiOS/py/src/swagger_to_plist.py index 21eda21..9c48cf0 100644 --- a/DDMockiOS/py/src/swagger_to_plist.py +++ b/DDMockiOS/py/src/swagger_to_plist.py @@ -1,4 +1,4 @@ # convert swagger api spec into ddmock plist def swagger_to_plist(): - print("swag") \ No newline at end of file + print("swag") From c633f2ca6badfa5dc2d58d2bc69f5afa48828f55 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 18:46:18 +1000 Subject: [PATCH 12/26] change py paths to enable compilation + move Resources --- DDMockiOS.podspec | 6 +++--- DDMockiOS.xcodeproj/project.pbxproj | 10 +++++----- .../resources => Resources}/general.json | 0 .../resources => Resources}/general.plist | 0 .../mockfiles/example/get/01_success.json | 0 {DDMockiOS/resources => Resources}/root.json | 0 {DDMockiOS/py => py}/ddmock.py | 18 +++++++++--------- {DDMockiOS/py => py}/src/plist.py | 0 {DDMockiOS/py => py}/src/swagger_to_plist.py | 0 9 files changed, 17 insertions(+), 17 deletions(-) rename {DDMockiOS/resources => Resources}/general.json (100%) rename {DDMockiOS/resources => Resources}/general.plist (100%) rename {DDMockiOS/resources => Resources}/mockfiles/example/get/01_success.json (100%) rename {DDMockiOS/resources => Resources}/root.json (100%) rename {DDMockiOS/py => py}/ddmock.py (95%) rename {DDMockiOS/py => py}/src/plist.py (100%) rename {DDMockiOS/py => py}/src/swagger_to_plist.py (100%) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 1acb0e7..cc26829 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -2,7 +2,6 @@ # Be sure to run `pod lib lint DDMockiOS.podspec' to ensure this is a # valid spec before submitting. # -# Any lines starting with a # are optional, but their use is encouraged # To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html # @@ -23,8 +22,9 @@ Pod::Spec.new do |spec| spec.source_files = 'DDMockiOS' spec.preserve_paths = [ - 'DDMockiOS/py/**', - ] + 'py/ddmock.py', + 'Resources/general.json' + ] spec.swift_version = '5' spec.static_framework = true diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 4151677..042e2b5 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -106,6 +106,8 @@ 9118D8A7223F1AE300195DC1 = { isa = PBXGroup; children = ( + C9334FBF264D1C9200190EB7 /* Resources */, + C9334F98264CC11100190EB7 /* py */, 52C27F8B2609C1B600D04B74 /* Documentation */, 52C27F8A2609C1AE00D04B74 /* Pod */, 9118D8B3223F1AE300195DC1 /* DDMockiOS */, @@ -135,8 +137,6 @@ 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, C9334FC7264E998D00190EB7 /* String+Regex.swift */, 9118D8B5223F1AE300195DC1 /* Info.plist */, - C9334F98264CC11100190EB7 /* py */, - C9334FBF264D1C9200190EB7 /* resources */, ); path = DDMockiOS; sourceTree = ""; @@ -167,14 +167,14 @@ path = src; sourceTree = ""; }; - C9334FBF264D1C9200190EB7 /* resources */ = { + C9334FBF264D1C9200190EB7 /* Resources */ = { isa = PBXGroup; children = ( C9334FDC265611E100190EB7 /* mockfiles */, C9334FBC264D1AB500190EB7 /* general.json */, C9334FB3264CF90400190EB7 /* general.plist */, ); - path = resources; + path = Resources; sourceTree = ""; }; /* End PBXGroup section */ @@ -301,7 +301,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 ${SRCROOT}/DDMockiOS/py/ddmock.py IOOF/Resources/mockfiles\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 ${SRCROOT}/py/ddmock.py Resources/mockfiles\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/DDMockiOS/resources/general.json b/Resources/general.json similarity index 100% rename from DDMockiOS/resources/general.json rename to Resources/general.json diff --git a/DDMockiOS/resources/general.plist b/Resources/general.plist similarity index 100% rename from DDMockiOS/resources/general.plist rename to Resources/general.plist diff --git a/DDMockiOS/resources/mockfiles/example/get/01_success.json b/Resources/mockfiles/example/get/01_success.json similarity index 100% rename from DDMockiOS/resources/mockfiles/example/get/01_success.json rename to Resources/mockfiles/example/get/01_success.json diff --git a/DDMockiOS/resources/root.json b/Resources/root.json similarity index 100% rename from DDMockiOS/resources/root.json rename to Resources/root.json diff --git a/DDMockiOS/py/ddmock.py b/py/ddmock.py similarity index 95% rename from DDMockiOS/py/ddmock.py rename to py/ddmock.py index 028258f..6d28ebb 100755 --- a/DDMockiOS/py/ddmock.py +++ b/py/ddmock.py @@ -44,12 +44,12 @@ def generate_map(mockfiles_path): def load_root_json(): - with open("DDMockiOS/resources/root.json", "r") as root: + with open("Resources/root.json", "r") as root: return json.load(root) def load_endpoint_json(): - with open("DDMockiOS/resources/root.json", "r") as root: + with open("Resources/root.json", "r") as root: return json.load(root) def main(mockfiles_path): @@ -189,12 +189,12 @@ def main(mockfiles_path): # ** short circuit for testing - with open(settings_location + "Root.plist", "rb") as root: - with open("DDMockiOS/resources/root.json", "w+") as output: - plist = plistlib.load(root) - json.dump(plist, output, indent=4) - print("dumped root json") - return +# with open(settings_location + "Root.plist", "rb") as root: +# with open("resources/root.json", "w+") as output: +# plist = plistlib.load(root) +# json.dump(plist, output, indent=4) +# print("dumped root json") +# return # ** @@ -253,7 +253,7 @@ def main(mockfiles_path): # create general plist from json # this could be from print("Creating general.plist...") - with open("DDMockiOS/resources/general.json", "r") as general: + with open("Resources/general.json", "r") as general: with open(os.path.join(settings_location, "general.plist"), "wb") as output: plistlib.dump(general.read(), output, fmt=plistlib.FMT_XML) # copy static file diff --git a/DDMockiOS/py/src/plist.py b/py/src/plist.py similarity index 100% rename from DDMockiOS/py/src/plist.py rename to py/src/plist.py diff --git a/DDMockiOS/py/src/swagger_to_plist.py b/py/src/swagger_to_plist.py similarity index 100% rename from DDMockiOS/py/src/swagger_to_plist.py rename to py/src/swagger_to_plist.py From afadb2ae0f2e3d0164b1eb13dc3addca9a373e33 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 18:48:38 +1000 Subject: [PATCH 13/26] add root.json template resource --- DDMockiOS.podspec | 3 ++- DDMockiOS.xcodeproj/project.pbxproj | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index cc26829..2d9a9ed 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -23,7 +23,8 @@ Pod::Spec.new do |spec| spec.preserve_paths = [ 'py/ddmock.py', - 'Resources/general.json' + 'Resources/general.json', + 'Resources/root.json' ] spec.swift_version = '5' diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 042e2b5..7fd6797 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D9D2670635C0056AC3E /* MockRepository.swift */; }; + C9815E602670B7B00056AC3E /* root.json in Resources */ = {isa = PBXBuildFile; fileRef = C9815E5F2670B7B00056AC3E /* root.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,7 @@ C9334FDC265611E100190EB7 /* mockfiles */ = {isa = PBXFileReference; lastKnownFileType = folder; path = mockfiles; sourceTree = ""; }; C9815D95267060EF0056AC3E /* ResponseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHelper.swift; sourceTree = ""; }; C9815D9D2670635C0056AC3E /* MockRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRepository.swift; sourceTree = ""; }; + C9815E5F2670B7B00056AC3E /* root.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = root.json; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -172,6 +174,7 @@ children = ( C9334FDC265611E100190EB7 /* mockfiles */, C9334FBC264D1AB500190EB7 /* general.json */, + C9815E5F2670B7B00056AC3E /* root.json */, C9334FB3264CF90400190EB7 /* general.plist */, ); path = Resources; @@ -277,6 +280,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C9815E602670B7B00056AC3E /* root.json in Resources */, C9334FBD264D1AB500190EB7 /* general.json in Resources */, C9334FDD265611E100190EB7 /* mockfiles in Resources */, ); From 00191b37c9999692935cfea899bf117fca1e597a Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 19:19:10 +1000 Subject: [PATCH 14/26] folder restructure --- DDMockiOS.podspec | 4 +- DDMockiOS.xcodeproj/project.pbxproj | 64 ++++++++----------- {py => Generate}/ddmock.py | 1 + {py/src => Generate}/plist.py | 0 {py/src => Generate}/swagger_to_plist.py | 0 DDMockiOS/Info.plist => Info.plist | 0 .../DDMockURLProtocolClass.swift | 0 {DDMockiOS => Sources}/DDMockiOS.h | 0 {DDMockiOS => Sources}/DDMockiOS.swift | 0 {DDMockiOS => Sources}/MockEntry.swift | 0 {DDMockiOS => Sources}/MockRepository.swift | 4 +- {DDMockiOS => Sources}/ResponseHelper.swift | 30 ++++++++- {DDMockiOS => Sources}/String+Regex.swift | 0 .../UserDefaultsHelper.swift | 0 14 files changed, 58 insertions(+), 45 deletions(-) rename {py => Generate}/ddmock.py (99%) rename {py/src => Generate}/plist.py (100%) rename {py/src => Generate}/swagger_to_plist.py (100%) rename DDMockiOS/Info.plist => Info.plist (100%) rename {DDMockiOS => Sources}/DDMockURLProtocolClass.swift (100%) rename {DDMockiOS => Sources}/DDMockiOS.h (100%) rename {DDMockiOS => Sources}/DDMockiOS.swift (100%) rename {DDMockiOS => Sources}/MockEntry.swift (100%) rename {DDMockiOS => Sources}/MockRepository.swift (97%) rename {DDMockiOS => Sources}/ResponseHelper.swift (74%) rename {DDMockiOS => Sources}/String+Regex.swift (100%) rename {DDMockiOS => Sources}/UserDefaultsHelper.swift (100%) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 2d9a9ed..7cb379b 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -15,14 +15,14 @@ Pod::Spec.new do |spec| spec.homepage = 'https://github.com/DeloitteDigitalAPAC/ddmock-ios' spec.license = { :type => 'MIT', :file => 'LICENSE' } spec.author = 'Deloitte Digital Asia Pacific' - spec.source = { :git => "https://github.com/DeloitteDigitalAPAC/ddmock-ios.git", :tag => 'v' + spec.version.to_s } + spec.source = { :git => "https://github.com/will-rigney/ddmock-ios.git", :tag => 'v' + spec.version.to_s } spec.ios.deployment_target = '11.0' spec.source_files = 'DDMockiOS' spec.preserve_paths = [ - 'py/ddmock.py', + 'Generate/ddmock.py', 'Resources/general.json', 'Resources/root.json' ] diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 7fd6797..4abbbfb 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -14,12 +14,10 @@ 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */; }; 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */; }; 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */; }; - C9334FBD264D1AB500190EB7 /* general.json in Resources */ = {isa = PBXBuildFile; fileRef = C9334FBC264D1AB500190EB7 /* general.json */; }; C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9334FC7264E998D00190EB7 /* String+Regex.swift */; }; C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D9D2670635C0056AC3E /* MockRepository.swift */; }; - C9815E602670B7B00056AC3E /* root.json in Resources */ = {isa = PBXBuildFile; fileRef = C9815E5F2670B7B00056AC3E /* root.json */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -35,7 +33,6 @@ /* Begin PBXFileReference section */ 52C27F7D2609C1A600D04B74 /* DDMockiOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DDMockiOSTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 52C27F7F2609C1A600D04B74 /* DDMockiOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDMockiOSTests.swift; sourceTree = ""; }; - 52C27F812609C1A600D04B74 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52C27F8C2609C1F600D04B74 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 52C27F8D2609C1F600D04B74 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 52C27F8E2609C20400D04B74 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -82,7 +79,6 @@ isa = PBXGroup; children = ( 52C27F7F2609C1A600D04B74 /* DDMockiOSTests.swift */, - 52C27F812609C1A600D04B74 /* Info.plist */, ); path = DDMockiOSTests; sourceTree = ""; @@ -108,11 +104,12 @@ 9118D8A7223F1AE300195DC1 = { isa = PBXGroup; children = ( + 9118D8B5223F1AE300195DC1 /* Info.plist */, + C9334F98264CC11100190EB7 /* Generate */, + C9B28E972670C587007C31A9 /* Sources */, C9334FBF264D1C9200190EB7 /* Resources */, - C9334F98264CC11100190EB7 /* py */, 52C27F8B2609C1B600D04B74 /* Documentation */, 52C27F8A2609C1AE00D04B74 /* Pod */, - 9118D8B3223F1AE300195DC1 /* DDMockiOS */, 52C27F7E2609C1A600D04B74 /* DDMockiOSTests */, 9118D8B2223F1AE300195DC1 /* Products */, ); @@ -127,30 +124,15 @@ name = Products; sourceTree = ""; }; - 9118D8B3223F1AE300195DC1 /* DDMockiOS */ = { - isa = PBXGroup; - children = ( - 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */, - 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, - C9815D9D2670635C0056AC3E /* MockRepository.swift */, - C9815D95267060EF0056AC3E /* ResponseHelper.swift */, - 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, - 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, - 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, - C9334FC7264E998D00190EB7 /* String+Regex.swift */, - 9118D8B5223F1AE300195DC1 /* Info.plist */, - ); - path = DDMockiOS; - sourceTree = ""; - }; - C9334F98264CC11100190EB7 /* py */ = { + C9334F98264CC11100190EB7 /* Generate */ = { isa = PBXGroup; children = ( C9334FAE264CF8A800190EB7 /* ddmock.py */, - C9334FAF264CF8A800190EB7 /* src */, + C9334FC0264D1CB200190EB7 /* plist.py */, + C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */, C9334FAD264CF8A800190EB7 /* tests */, ); - path = py; + path = Generate; sourceTree = ""; }; C9334FAD264CF8A800190EB7 /* tests */ = { @@ -160,15 +142,6 @@ path = tests; sourceTree = ""; }; - C9334FAF264CF8A800190EB7 /* src */ = { - isa = PBXGroup; - children = ( - C9334FC0264D1CB200190EB7 /* plist.py */, - C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */, - ); - path = src; - sourceTree = ""; - }; C9334FBF264D1C9200190EB7 /* Resources */ = { isa = PBXGroup; children = ( @@ -180,6 +153,21 @@ path = Resources; sourceTree = ""; }; + C9B28E972670C587007C31A9 /* Sources */ = { + isa = PBXGroup; + children = ( + 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, + 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */, + 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, + C9815D9D2670635C0056AC3E /* MockRepository.swift */, + C9815D95267060EF0056AC3E /* ResponseHelper.swift */, + 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, + C9334FC7264E998D00190EB7 /* String+Regex.swift */, + 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, + ); + path = Sources; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -280,8 +268,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - C9815E602670B7B00056AC3E /* root.json in Resources */, - C9334FBD264D1AB500190EB7 /* general.json in Resources */, C9334FDD265611E100190EB7 /* mockfiles in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -305,7 +291,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 ${SRCROOT}/py/ddmock.py Resources/mockfiles\n"; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\npython3 ${SRCROOT}/Generate/ddmock.py Resources/mockfiles\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -518,7 +504,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = DDMockiOS/Info.plist; + INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -546,7 +532,7 @@ DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; - INFOPLIST_FILE = DDMockiOS/Info.plist; + INFOPLIST_FILE = Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/py/ddmock.py b/Generate/ddmock.py similarity index 99% rename from py/ddmock.py rename to Generate/ddmock.py index 6d28ebb..3a6901d 100755 --- a/py/ddmock.py +++ b/Generate/ddmock.py @@ -53,6 +53,7 @@ def load_endpoint_json(): return json.load(root) def main(mockfiles_path): + print("wd: " + os.getcwd()) # todo: is this not passed straight to main # path to mockfiles passed as argument diff --git a/py/src/plist.py b/Generate/plist.py similarity index 100% rename from py/src/plist.py rename to Generate/plist.py diff --git a/py/src/swagger_to_plist.py b/Generate/swagger_to_plist.py similarity index 100% rename from py/src/swagger_to_plist.py rename to Generate/swagger_to_plist.py diff --git a/DDMockiOS/Info.plist b/Info.plist similarity index 100% rename from DDMockiOS/Info.plist rename to Info.plist diff --git a/DDMockiOS/DDMockURLProtocolClass.swift b/Sources/DDMockURLProtocolClass.swift similarity index 100% rename from DDMockiOS/DDMockURLProtocolClass.swift rename to Sources/DDMockURLProtocolClass.swift diff --git a/DDMockiOS/DDMockiOS.h b/Sources/DDMockiOS.h similarity index 100% rename from DDMockiOS/DDMockiOS.h rename to Sources/DDMockiOS.h diff --git a/DDMockiOS/DDMockiOS.swift b/Sources/DDMockiOS.swift similarity index 100% rename from DDMockiOS/DDMockiOS.swift rename to Sources/DDMockiOS.swift diff --git a/DDMockiOS/MockEntry.swift b/Sources/MockEntry.swift similarity index 100% rename from DDMockiOS/MockEntry.swift rename to Sources/MockEntry.swift diff --git a/DDMockiOS/MockRepository.swift b/Sources/MockRepository.swift similarity index 97% rename from DDMockiOS/MockRepository.swift rename to Sources/MockRepository.swift index b758124..9423e14 100644 --- a/DDMockiOS/MockRepository.swift +++ b/Sources/MockRepository.swift @@ -53,7 +53,7 @@ internal class MockRepository { } /// todo: doc - internal func hasEntry(path: String, method: String) -> Bool { + func hasEntry(path: String, method: String) -> Bool { // idk what the idea of this map function useRealAPI @@ -72,7 +72,7 @@ internal class MockRepository { /** get the mock entry, respecting strict mode */ - internal func getEntry( + func getEntry( path: String, method: String, strict: Bool, diff --git a/DDMockiOS/ResponseHelper.swift b/Sources/ResponseHelper.swift similarity index 74% rename from DDMockiOS/ResponseHelper.swift rename to Sources/ResponseHelper.swift index 4db4959..288b369 100644 --- a/DDMockiOS/ResponseHelper.swift +++ b/Sources/ResponseHelper.swift @@ -1,8 +1,35 @@ import Foundation +//struct MockResponse { +// let headers: [String: String] +// // other elements a response might have +// fileprivate init( +// headers: [String: String]) { +// +// self.headers = headers +// } +//} +// +//fileprivate class MockResponseBuilder { +// private var headers: [String: String] = [:] +// +// func addHeaders(contentLength: Int?) { +// self.headers = ResponseHelper.getMockHeaders(contentLength: contentLength) +// } +// +// func build() -> MockResponse { +// return MockResponse(headers: headers) +// } +//} +/* + basically we want to have some response type, and we want to both create it + and send it + + */ + // todo: maybe think about replacing this with a builder // todo: move this out of amorphous 'helper' -internal struct ResponseHelper { +class ResponseHelper { // todo: allow headers to be configurable static func getMockHeaders(contentLength: Int?) -> [String: String] { @@ -44,7 +71,6 @@ internal struct ResponseHelper { return try? Data( contentsOf: url, options: .mappedIfSafe) - } /// diff --git a/DDMockiOS/String+Regex.swift b/Sources/String+Regex.swift similarity index 100% rename from DDMockiOS/String+Regex.swift rename to Sources/String+Regex.swift diff --git a/DDMockiOS/UserDefaultsHelper.swift b/Sources/UserDefaultsHelper.swift similarity index 100% rename from DDMockiOS/UserDefaultsHelper.swift rename to Sources/UserDefaultsHelper.swift From 3840101465e4c31eac90dcb427fa9ed6533eee16 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 20:09:40 +1000 Subject: [PATCH 15/26] update podspec --- DDMockiOS.podspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 7cb379b..ac86e4c 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -10,7 +10,7 @@ Pod::Spec.new do |spec| spec.version = '2.0' spec.summary = 'Deloitte Digital simple network mocking library for iOS' - spec.description = 'Deloitte Digital simple network mocking library for iOS' + spec.description = 'Deloitte Digital simple network mocking library for iOS. Runtime configurable mocking library with highly flexible usage. Integrated tooling for delivery and testing teams.' spec.homepage = 'https://github.com/DeloitteDigitalAPAC/ddmock-ios' spec.license = { :type => 'MIT', :file => 'LICENSE' } @@ -19,7 +19,7 @@ Pod::Spec.new do |spec| spec.ios.deployment_target = '11.0' - spec.source_files = 'DDMockiOS' + spec.source_files = 'Sources' spec.preserve_paths = [ 'Generate/ddmock.py', From 0bf145e1a60b6644d00edda78ef4555448febd3e Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 20:38:42 +1000 Subject: [PATCH 16/26] enable optimisation in debug builds --- DDMockiOS.xcodeproj/project.pbxproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 4abbbfb..80e9638 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.dd.DDMockiOS; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; From 29ceb79c26e190b981ed0f1defedf2c707b99a95 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Wed, 9 Jun 2021 22:11:58 +1000 Subject: [PATCH 17/26] generate endpoint plist from json in non pythonic ways --- DDMockiOS.podspec | 3 +- DDMockiOS.xcodeproj/project.pbxproj | 2 + Generate/ddmock.py | 252 +++++++++++++--------------- Resources/endpoint.json | 36 ++++ 4 files changed, 156 insertions(+), 137 deletions(-) create mode 100644 Resources/endpoint.json diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index ac86e4c..82b218a 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -24,7 +24,8 @@ Pod::Spec.new do |spec| spec.preserve_paths = [ 'Generate/ddmock.py', 'Resources/general.json', - 'Resources/root.json' + 'Resources/root.json', + 'Resources/endpoint.json', ] spec.swift_version = '5' diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 80e9638..8dbcc1a 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -54,6 +54,7 @@ C9815D95267060EF0056AC3E /* ResponseHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseHelper.swift; sourceTree = ""; }; C9815D9D2670635C0056AC3E /* MockRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRepository.swift; sourceTree = ""; }; C9815E5F2670B7B00056AC3E /* root.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = root.json; sourceTree = ""; }; + C9B28EE62670D9CE007C31A9 /* endpoint.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = endpoint.json; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -147,6 +148,7 @@ children = ( C9334FDC265611E100190EB7 /* mockfiles */, C9334FBC264D1AB500190EB7 /* general.json */, + C9B28EE62670D9CE007C31A9 /* endpoint.json */, C9815E5F2670B7B00056AC3E /* root.json */, C9334FB3264CF90400190EB7 /* general.plist */, ); diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 3a6901d..ceca7b9 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -7,10 +7,12 @@ import argparse # create the map of endpoints from mockfiles + + def generate_map(mockfiles_path): - map = {} + endpoint_map = {} - ## walks all the mockfiles and for each creates just the leading path? + # walks all the mockfiles and for each creates just the leading path? # recursive directory traversal # todo: what is subdir @@ -34,13 +36,13 @@ def generate_map(mockfiles_path): endpointPath = endpointPath.replace("/", "", 1) # map is accessed here (therefore make this a function duh) - if endpointPath in map: - files = map[endpointPath] + if endpointPath in endpoint_map: + files = endpoint_map[endpointPath] files.append(file) else: - map[endpointPath] = [file] + endpoint_map[endpointPath] = [file] - return map + return endpoint_map def load_root_json(): @@ -49,20 +51,17 @@ def load_root_json(): def load_endpoint_json(): - with open("Resources/root.json", "r") as root: - return json.load(root) + with open("Resources/endpoint.json", "r") as endpoint: + return json.load(endpoint) + def main(mockfiles_path): print("wd: " + os.getcwd()) - # todo: is this not passed straight to main - # path to mockfiles passed as argument - # e.g. IOOF/Resources/mockfiles - # first create the map # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") - map = generate_map(mockfiles_path) + endpoint_map = generate_map(mockfiles_path) # start creating settings bundle # todo: what is the settings bundle & where are we creating it? @@ -73,8 +72,7 @@ def main(mockfiles_path): # todo: dynamic / configuration # is this from cwd or root of project? # todo: this should come from arguments - settings_location = "DDMockiOS/Settings.bundle/" - + settings_location = "Settings.bundle/" # Settings.bundle is really just a directory # first create directory if it doesn't exist @@ -85,124 +83,94 @@ def main(mockfiles_path): print("Creating root plist...") root = load_root_json() - # Root plist file - # root = '' - # root = root + '\n' - # root = root + '\n' - # root = root + "\n" - # root = root + "\n\tStringsTable" - # root = root + "\n\tRoot" - # root = root + "\n\tPreferenceSpecifiers" - # root = root + "\n\t" - # root = root + "\n\t\t" - # root = root + "\n\t\t\tType" - # root = root + "\n\t\t\t\tPSToggleSwitchSpecifier" - # root = root + "\n\t\t\t\tTitle" - # root = root + "\n\t\t\t\tUse real APIs" - # root = root + "\n\t\t\t\tKey" - # root = root + "\n\t\t\t\tuse_real_apis" - # root = root + "\n\t\t\t\tDefaultValue" - # root = root + "\n\t\t\t\t" - # root = root + "\n\t\t" - # root = root + "\n\t\t" - # root = root + "\n\t\t\tType" - # root = root + "\n\t\t\tPSChildPaneSpecifier" - # root = root + "\n\t\t\tFile" - # root = root + "\n\t\t\tgeneral" - # root = root + "\n\t\t\tTitle" - # root = root + "\n\t\t\tGeneral" - # root = root + "\n\t\t" - # root = root + "\n\t\t" - # root = root + "\n\t\t\tType" - # root = root + "\n\t\t\tPSGroupSpecifier" - # root = root + "\n\t\t\tTitle" - # root = root + "\n\t\t\tMOCK" - # root = root + "\n\t\t" - - # Endpoints plist file - plist = '' - plist = plist + '\n' - plist = plist + '\n' - plist = plist + "\n" - plist = plist + "\n\tPreferenceSpecifiers" - plist = plist + "\n\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t\tType" - plist = plist + "\n\t\t\tPSToggleSwitchSpecifier" - plist = plist + "\n\t\t\tTitle" - plist = plist + "\n\t\t\tUse real API" - plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" # $endpointPathKey - plist = plist + "\n\t\t\tDefaultValue" - plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t\tDefaultValue" - plist = plist + "\n\t\t\t$endpointPathName" - plist = plist + "\n\t\t\tType" - plist = plist + "\n\t\t\tPSTitleValueSpecifier" - plist = plist + "\n\t\t\tTitle" - plist = plist + "\n\t\t\tEndpoint" - plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_endpoint" # $endpointPathKey - plist = plist + "\n\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t\tType" - plist = plist + "\n\t\t\tPSTextFieldSpecifier" - plist = plist + "\n\t\t\tDefaultValue" - plist = plist + "\n\t\t\t400" - plist = plist + "\n\t\t\tTitle" - plist = plist + "\n\t\t\tResponse Time (ms)" - plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_response_time" # $endpointPathKey - plist = plist + "\n\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t\tType" - plist = plist + "\n\t\t\tPSTextFieldSpecifier" - plist = plist + "\n\t\t\tDefaultValue" - plist = plist + "\n\t\t\t200" - plist = plist + "\n\t\t\tTitle" - plist = plist + "\n\t\t\tStatus Code" - plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_status_code" # $endpointPathKey - plist = plist + "\n\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t\t\tType" - plist = plist + "\n\t\t\tPSMultiValueSpecifier" - plist = plist + "\n\t\t\tTitle" - plist = plist + "\n\t\t\tMock file" - plist = plist + "\n\t\t\tKey" - plist = plist + "\n\t\t\t$endpointPathKey_mock_file" # $endpointPathKey - plist = plist + "\n\t\t\tDefaultValue" - plist = plist + "\n\t\t\t0" - plist = plist + "\n\t\t\tValues" - plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t\t\t$indexMockFiles" # $indexMockFiles - plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t\tTitles" - plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t\t\t$mockFiles" - plist = plist + "\n\t\t\t" - plist = plist + "\n\t\t" - plist = plist + "\n\t" - plist = plist + "\n" - plist = plist + "\n" + print("Creating endpoint plist...") + endpoint = load_endpoint_json() + + # # Endpoints plist file + # plist = '' + # plist = plist + '\n' + # plist = plist + '\n' + # plist = plist + "\n" + # plist = plist + "\n\tPreferenceSpecifiers" + # plist = plist + "\n\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t\tType" + # plist = plist + "\n\t\t\tPSToggleSwitchSpecifier" + # plist = plist + "\n\t\t\tTitle" + # plist = plist + "\n\t\t\tUse real API" + # plist = plist + "\n\t\t\tKey" + # plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" # $endpointPathKey + # plist = plist + "\n\t\t\tDefaultValue" + # plist = plist + "\n\t\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t\tDefaultValue" + # plist = plist + "\n\t\t\t$endpointPathName" + # plist = plist + "\n\t\t\tType" + # plist = plist + "\n\t\t\tPSTitleValueSpecifier" + # plist = plist + "\n\t\t\tTitle" + # plist = plist + "\n\t\t\tEndpoint" + # plist = plist + "\n\t\t\tKey" + # plist = plist + "\n\t\t\t$endpointPathKey_endpoint" # $endpointPathKey + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t\tType" + # plist = plist + "\n\t\t\tPSTextFieldSpecifier" + # plist = plist + "\n\t\t\tDefaultValue" + # plist = plist + "\n\t\t\t400" + # plist = plist + "\n\t\t\tTitle" + # plist = plist + "\n\t\t\tResponse Time (ms)" + # plist = plist + "\n\t\t\tKey" + # plist = plist + "\n\t\t\t$endpointPathKey_response_time" # $endpointPathKey + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t\tType" + # plist = plist + "\n\t\t\tPSTextFieldSpecifier" + # plist = plist + "\n\t\t\tDefaultValue" + # plist = plist + "\n\t\t\t200" + # plist = plist + "\n\t\t\tTitle" + # plist = plist + "\n\t\t\tStatus Code" + # plist = plist + "\n\t\t\tKey" + # plist = plist + "\n\t\t\t$endpointPathKey_status_code" # $endpointPathKey + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t\t\tType" + # plist = plist + "\n\t\t\tPSMultiValueSpecifier" + # plist = plist + "\n\t\t\tTitle" + # plist = plist + "\n\t\t\tMock file" + # plist = plist + "\n\t\t\tKey" + # plist = plist + "\n\t\t\t$endpointPathKey_mock_file" # $endpointPathKey + # plist = plist + "\n\t\t\tDefaultValue" + # plist = plist + "\n\t\t\t0" + # plist = plist + "\n\t\t\tValues" + # plist = plist + "\n\t\t\t" + # plist = plist + "\n\t\t\t\t$indexMockFiles" # $indexMockFiles + # plist = plist + "\n\t\t\t" + # plist = plist + "\n\t\t\tTitles" + # plist = plist + "\n\t\t\t" + # plist = plist + "\n\t\t\t\t$mockFiles" # $mockFiles + # plist = plist + "\n\t\t\t" + # plist = plist + "\n\t\t" + # plist = plist + "\n\t" + # plist = plist + "\n" + # plist = plist + "\n" # ** short circuit for testing -# with open(settings_location + "Root.plist", "rb") as root: -# with open("resources/root.json", "w+") as output: -# plist = plistlib.load(root) -# json.dump(plist, output, indent=4) -# print("dumped root json") -# return +# with open(settings_location + "Root.plist", "rb") as root: + # with open("resources/endpoint.json", "w+") as output: + # plist_bytes = plist.encode(encoding='utf-8') + # plist = plistlib.loads(plist_bytes, fmt=plistlib.FMT_XML) + # json.dump(plist, output, indent=4) + # print("dumped root json") - # ** + # return + # ** # for path & files in map - for endpointPath, files in map.items(): + for endpointPath, files in endpoint_map.items(): - # replaces the slashes with periods for + # replaces the slashes with periods for filename = endpointPath.replace("/", ".") print(root) @@ -215,7 +183,6 @@ def main(mockfiles_path): root['PreferenceSpecifiers'].append(new_item) - # create a copy of the endpoint plist replacing # the $endpointPathName key -> endpointPath # the $indexMockFiles key -> indexes @@ -225,15 +192,26 @@ def main(mockfiles_path): # creating plist file for endpoint print("Creating plist file for " + endpointPath + "...") + def replaceKeys(item, path, filename): + # todo: clarify what is happening + item['DefaultValue'] = item['key'].replace( + "$endpointPathName", path) + item['Key'] = item['key'].replace("$endpointPathKey", filename) + + # todo: endpoints added to plist here - with open(settings_location + filename + ".plist", "w+") as fout: - newplist = plist + with open(settings_location + filename + ".plist", "wb") as fout: + new_endpoint = endpoint + + map(lambda item: replaceKeys, new_endpoint["PreferenceSpecifiers"]) # newplist = newplist.replace("$endpointPathName", endpointPath).replace( # "$endpointPathKey", filename) # indexes = "0" - # for i in range(1, len(files)): + for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): + setting["Values"] = list(range(0, len(files))) + setting["Titles"] = files # indexes = indexes + "\n\t\t\t\t" + \ # str(i) + "" # newplist = newplist.replace("$indexMockFiles", indexes) @@ -244,15 +222,15 @@ def main(mockfiles_path): # files[i] + "" # newplist = newplist.replace("$mockFiles", mockFiles) - fout.write(newplist) + plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) + # fout.write(new_endpoint) # insert: mb generate general plist? even from some config? # general_plist_path = "DDMockiOS/DDMockiOS/general.plist" - # create general plist from json - # this could be from + # this could be from print("Creating general.plist...") with open("Resources/general.json", "r") as general: with open(os.path.join(settings_location, "general.plist"), "wb") as output: @@ -263,7 +241,7 @@ def main(mockfiles_path): # copies the "general.plist" # todo: generate from the lib # shutil.copyfile(general_plist_path, - # os.path.join(settings_location, "general.plist")) + # os.path.join(settings_location, "general.plist")) # copies from one static path to another (pointlessly?) @@ -284,8 +262,10 @@ def main(mockfiles_path): if __name__ == "__main__": # parse arguments - parser = argparse.ArgumentParser(description='Generate Settings.bundle for DDMockiOS') - parser.add_argument('mockfiles_path', nargs='?', default="DDMockiOS/resources/mockfiles") + parser = argparse.ArgumentParser( + description='Generate Settings.bundle for DDMockiOS') + parser.add_argument('mockfiles_path', nargs='?', + default="DDMockiOS/resources/mockfiles") args = parser.parse_args() diff --git a/Resources/endpoint.json b/Resources/endpoint.json new file mode 100644 index 0000000..e243dae --- /dev/null +++ b/Resources/endpoint.json @@ -0,0 +1,36 @@ +{ + "PreferenceSpecifiers": [ + { + "Type": "PSToggleSwitchSpecifier", + "Title": "Use real API", + "Key": "$endpointPathKey_use_real_api", + "DefaultValue": false + }, + { + "DefaultValue": "$endpointPathName", + "Type": "PSTitleValueSpecifier", + "Title": "Endpoint", + "Key": "$endpointPathKey_endpoint" + }, + { + "Type": "PSTextFieldSpecifier", + "DefaultValue": "400", + "Title": "Response Time (ms)", + "Key": "$endpointPathKey_response_time" + }, + { + "Type": "PSTextFieldSpecifier", + "DefaultValue": "200", + "Title": "Status Code", + "Key": "$endpointPathKey_status_code" + }, + { + "Type": "PSMultiValueSpecifier", + "Title": "Mock file", + "Key": "$endpointPathKey_mock_file", + "DefaultValue": 0, + "Values": [], + "Titles": [] + } + ] +} \ No newline at end of file From cc61967ed48ec4a75e16708fec1783d7bada2a56 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Thu, 10 Jun 2021 09:45:22 +1000 Subject: [PATCH 18/26] simplify py json template load --- Generate/ddmock.py | 124 ++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 93 deletions(-) diff --git a/Generate/ddmock.py b/Generate/ddmock.py index ceca7b9..2623c64 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -2,6 +2,7 @@ import shutil import sys import plistlib +import pathlib import logging import json import argparse @@ -16,7 +17,6 @@ def generate_map(mockfiles_path): # recursive directory traversal # todo: what is subdir - # damn dynamic types for subdir, dirs, files in os.walk(mockfiles_path): print(subdir) @@ -36,6 +36,8 @@ def generate_map(mockfiles_path): endpointPath = endpointPath.replace("/", "", 1) # map is accessed here (therefore make this a function duh) + # this does the same thing as swift code to run it + # "get or insert" if endpointPath in endpoint_map: files = endpoint_map[endpointPath] files.append(file) @@ -45,18 +47,20 @@ def generate_map(mockfiles_path): return endpoint_map -def load_root_json(): - with open("Resources/root.json", "r") as root: - return json.load(root) - - -def load_endpoint_json(): - with open("Resources/endpoint.json", "r") as endpoint: - return json.load(endpoint) +def load_json(path): + with open(path, "r") as file: + return json.load(file) def main(mockfiles_path): - print("wd: " + os.getcwd()) + cwd = os.getcwd() + print(f"wd: {cwd}") + + # get resource path from canonical path of script + path = os.path.dirname(os.path.realpath(__file__)) + path = pathlib.Path(path) + path = path.parent.joinpath("Resources").absolute() + print(f"path: {path}") # first create the map # this is where the directory traversal happens @@ -79,81 +83,14 @@ def main(mockfiles_path): if not os.path.exists(settings_location): os.makedirs(settings_location) - # create root plist - print("Creating root plist...") - root = load_root_json() - - print("Creating endpoint plist...") - endpoint = load_endpoint_json() - - # # Endpoints plist file - # plist = '' - # plist = plist + '\n' - # plist = plist + '\n' - # plist = plist + "\n" - # plist = plist + "\n\tPreferenceSpecifiers" - # plist = plist + "\n\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t\tType" - # plist = plist + "\n\t\t\tPSToggleSwitchSpecifier" - # plist = plist + "\n\t\t\tTitle" - # plist = plist + "\n\t\t\tUse real API" - # plist = plist + "\n\t\t\tKey" - # plist = plist + "\n\t\t\t$endpointPathKey_use_real_api" # $endpointPathKey - # plist = plist + "\n\t\t\tDefaultValue" - # plist = plist + "\n\t\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t\tDefaultValue" - # plist = plist + "\n\t\t\t$endpointPathName" - # plist = plist + "\n\t\t\tType" - # plist = plist + "\n\t\t\tPSTitleValueSpecifier" - # plist = plist + "\n\t\t\tTitle" - # plist = plist + "\n\t\t\tEndpoint" - # plist = plist + "\n\t\t\tKey" - # plist = plist + "\n\t\t\t$endpointPathKey_endpoint" # $endpointPathKey - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t\tType" - # plist = plist + "\n\t\t\tPSTextFieldSpecifier" - # plist = plist + "\n\t\t\tDefaultValue" - # plist = plist + "\n\t\t\t400" - # plist = plist + "\n\t\t\tTitle" - # plist = plist + "\n\t\t\tResponse Time (ms)" - # plist = plist + "\n\t\t\tKey" - # plist = plist + "\n\t\t\t$endpointPathKey_response_time" # $endpointPathKey - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t\tType" - # plist = plist + "\n\t\t\tPSTextFieldSpecifier" - # plist = plist + "\n\t\t\tDefaultValue" - # plist = plist + "\n\t\t\t200" - # plist = plist + "\n\t\t\tTitle" - # plist = plist + "\n\t\t\tStatus Code" - # plist = plist + "\n\t\t\tKey" - # plist = plist + "\n\t\t\t$endpointPathKey_status_code" # $endpointPathKey - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t\t\tType" - # plist = plist + "\n\t\t\tPSMultiValueSpecifier" - # plist = plist + "\n\t\t\tTitle" - # plist = plist + "\n\t\t\tMock file" - # plist = plist + "\n\t\t\tKey" - # plist = plist + "\n\t\t\t$endpointPathKey_mock_file" # $endpointPathKey - # plist = plist + "\n\t\t\tDefaultValue" - # plist = plist + "\n\t\t\t0" - # plist = plist + "\n\t\t\tValues" - # plist = plist + "\n\t\t\t" - # plist = plist + "\n\t\t\t\t$indexMockFiles" # $indexMockFiles - # plist = plist + "\n\t\t\t" - # plist = plist + "\n\t\t\tTitles" - # plist = plist + "\n\t\t\t" - # plist = plist + "\n\t\t\t\t$mockFiles" # $mockFiles - # plist = plist + "\n\t\t\t" - # plist = plist + "\n\t\t" - # plist = plist + "\n\t" - # plist = plist + "\n" - # plist = plist + "\n" + # load templates + print("Loading JSON templates...") + + root = path.joinpath("root.json") + root = load_json(root) + + endpoint = path.joinpath("endpoint.json") + endpoint = load_json(endpoint) # ** short circuit for testing @@ -168,10 +105,11 @@ def main(mockfiles_path): # ** # for path & files in map - for endpointPath, files in endpoint_map.items(): + for endpoint_path, files in endpoint_map.items(): + print(f"Creating endpoint plist for {endpoint_path}...") # replaces the slashes with periods for - filename = endpointPath.replace("/", ".") + filename = endpoint_path.replace("/", ".") print(root) @@ -190,7 +128,7 @@ def main(mockfiles_path): # then write the new file at settings_location + filename + .plist # creating plist file for endpoint - print("Creating plist file for " + endpointPath + "...") + print("Creating plist file for " + endpoint_path + "...") def replaceKeys(item, path, filename): # todo: clarify what is happening @@ -198,7 +136,6 @@ def replaceKeys(item, path, filename): "$endpointPathName", path) item['Key'] = item['key'].replace("$endpointPathKey", filename) - # todo: endpoints added to plist here with open(settings_location + filename + ".plist", "wb") as fout: new_endpoint = endpoint @@ -232,16 +169,17 @@ def replaceKeys(item, path, filename): # create general plist from json # this could be from print("Creating general.plist...") - with open("Resources/general.json", "r") as general: - with open(os.path.join(settings_location, "general.plist"), "wb") as output: - plistlib.dump(general.read(), output, fmt=plistlib.FMT_XML) + general = path.joinpath("general.json") + general = load_json(general) + with open(os.path.join(settings_location, "general.plist"), "wb") as output: + plistlib.dump(general, output, fmt=plistlib.FMT_XML) # copy static file # failing here because it's not from cwd or a variable # todo: some var for location # copies the "general.plist" # todo: generate from the lib # shutil.copyfile(general_plist_path, - # os.path.join(settings_location, "general.plist")) + # os.path.join(settings_location, "general.plist")) # copies from one static path to another (pointlessly?) From 34644541553b5ea6dac143b8309b40a7499ba1f5 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Thu, 10 Jun 2021 10:12:48 +1000 Subject: [PATCH 19/26] small cleanup + DefaultValue --- Generate/ddmock.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 2623c64..3a2327c 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -132,7 +132,7 @@ def main(mockfiles_path): def replaceKeys(item, path, filename): # todo: clarify what is happening - item['DefaultValue'] = item['key'].replace( + item['DefaultValue'] = item['DefaultValue'].replace( "$endpointPathName", path) item['Key'] = item['key'].replace("$endpointPathKey", filename) @@ -160,37 +160,21 @@ def replaceKeys(item, path, filename): # newplist = newplist.replace("$mockFiles", mockFiles) plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) - # fout.write(new_endpoint) - - # insert: mb generate general plist? even from some config? - - # general_plist_path = "DDMockiOS/DDMockiOS/general.plist" # create general plist from json - # this could be from print("Creating general.plist...") + + # load the template general = path.joinpath("general.json") general = load_json(general) + + # dump plist with open(os.path.join(settings_location, "general.plist"), "wb") as output: plistlib.dump(general, output, fmt=plistlib.FMT_XML) - # copy static file - # failing here because it's not from cwd or a variable - # todo: some var for location - # copies the "general.plist" - # todo: generate from the lib - # shutil.copyfile(general_plist_path, - # os.path.join(settings_location, "general.plist")) - - # copies from one static path to another (pointlessly?) - - # close the plist - # root = root + "\n\t" - # root = root + "\n" - # root = root + "\n" # write root plist + print("Writing root plist...") with open(settings_location + "Root.plist", "wb") as output: - print("Writing root plist...") plistlib.dump(root, output, fmt=plistlib.FMT_XML) # finished From d80094b328b3b01e1fed6a62a75e32ea3ac67414 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Thu, 10 Jun 2021 20:00:58 +1000 Subject: [PATCH 20/26] fix issues + cleanup --- DDMockiOS.xcodeproj/project.pbxproj | 4 + Generate/ddmock.py | 120 +++++++++++++++------------ Sources/DDMockURLProtocolClass.swift | 7 +- Sources/DDMockiOS.swift | 6 +- Sources/MockEntry.swift | 2 +- Sources/MockRepository.swift | 12 +-- Sources/ResponseHelper.swift | 4 +- 7 files changed, 85 insertions(+), 70 deletions(-) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 8dbcc1a..f7ae2bc 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ C9334FDD265611E100190EB7 /* mockfiles in Resources */ = {isa = PBXBuildFile; fileRef = C9334FDC265611E100190EB7 /* mockfiles */; }; C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D9D2670635C0056AC3E /* MockRepository.swift */; }; + C9B28FB32671C209007C31A9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B28FB22671C209007C31A9 /* Constants.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -55,6 +56,7 @@ C9815D9D2670635C0056AC3E /* MockRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockRepository.swift; sourceTree = ""; }; C9815E5F2670B7B00056AC3E /* root.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = root.json; sourceTree = ""; }; C9B28EE62670D9CE007C31A9 /* endpoint.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = endpoint.json; sourceTree = ""; }; + C9B28FB22671C209007C31A9 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -163,6 +165,7 @@ 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, C9815D9D2670635C0056AC3E /* MockRepository.swift */, C9815D95267060EF0056AC3E /* ResponseHelper.swift */, + C9B28FB22671C209007C31A9 /* Constants.swift */, 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, C9334FC7264E998D00190EB7 /* String+Regex.swift */, 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, @@ -317,6 +320,7 @@ 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */, C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */, 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */, + C9B28FB32671C209007C31A9 /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 3a2327c..c363342 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -7,10 +7,13 @@ import json import argparse +# todo: visibility + # create the map of endpoints from mockfiles def generate_map(mockfiles_path): + # init an empty map object endpoint_map = {} # walks all the mockfiles and for each creates just the leading path? @@ -22,16 +25,22 @@ def generate_map(mockfiles_path): # iterate through mockfiles for file in files: + # only the json files + if files.endswith(".json"): - # create path - filepath = subdir + os.sep + file + # todo: think there is a more normal way to check for only json files - # only the json files - if filepath.endswith(".json"): + # create path + # filepath = f"{subdir}/{file}" + + # this is to get the key from the json object path + # we can do this more directly endpointPath = subdir.replace(mockfiles_path, "") # strip the leading slash if present - # todo: presumably this is always present? wt + # todo: presumably this is always present? + # maybe not for relative paths + # we should use a Path object instead if endpointPath.startswith("/"): endpointPath = endpointPath.replace("/", "", 1) @@ -52,7 +61,31 @@ def load_json(path): return json.load(file) -def main(mockfiles_path): +def replace_keys(item, path, filename): + # todo: clarify what is happening + # todo: make a type for these string keys + item['DefaultValue'] = item['DefaultValue'].replace( + "$endpointPathName", path) + print(f"dv: {item['DefaultValue']}") + item['Key'] = item['key'].replace("$endpointPathKey", filename) + return item + + +def load_json_resource(res): + print(f"{res}") + res = load_json(res) + return res + + +def create_item(filename): + new_item = {} + new_item['Type'] = 'PSChildPaneSpecifier' + new_item['File'] = filename + new_item['Title'] = filename + return new_item + + +def main(mockfiles_path, output_path): cwd = os.getcwd() print(f"wd: {cwd}") @@ -60,23 +93,21 @@ def main(mockfiles_path): path = os.path.dirname(os.path.realpath(__file__)) path = pathlib.Path(path) path = path.parent.joinpath("Resources").absolute() - print(f"path: {path}") + print(f"templates: {path}") # first create the map # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") endpoint_map = generate_map(mockfiles_path) + print(f"{endpoint_map}") + + # todo: better / dynamic configuration + # this is from invokation site, turns out + settings_location = output_path # start creating settings bundle # todo: what is the settings bundle & where are we creating it? - print("Creating Settings.bundle...") - - # todo: args in python are weird, need to check their usage - - # todo: dynamic / configuration - # is this from cwd or root of project? - # todo: this should come from arguments - settings_location = "Settings.bundle/" + print(f"Creating Settings.bundle at {settings_location}...") # Settings.bundle is really just a directory # first create directory if it doesn't exist @@ -86,11 +117,8 @@ def main(mockfiles_path): # load templates print("Loading JSON templates...") - root = path.joinpath("root.json") - root = load_json(root) - - endpoint = path.joinpath("endpoint.json") - endpoint = load_json(endpoint) + root = load_json_resource(path.joinpath("root.json")) + endpoint = load_json_resource(path.joinpath("endpoint.json")) # ** short circuit for testing @@ -107,18 +135,14 @@ def main(mockfiles_path): # for path & files in map for endpoint_path, files in endpoint_map.items(): - print(f"Creating endpoint plist for {endpoint_path}...") + print(f"Adding endpoint: {endpoint_path}") + # replaces the slashes with periods for filename = endpoint_path.replace("/", ".") - print(root) - # add endpoint to root plist - new_item = {} - new_item['Type'] = 'PSChildPaneSpecifier' - new_item['File'] = filename - new_item['Title'] = filename + new_item = create_item(filename) root['PreferenceSpecifiers'].append(new_item) # create a copy of the endpoint plist replacing @@ -128,37 +152,21 @@ def main(mockfiles_path): # then write the new file at settings_location + filename + .plist # creating plist file for endpoint - print("Creating plist file for " + endpoint_path + "...") - - def replaceKeys(item, path, filename): - # todo: clarify what is happening - item['DefaultValue'] = item['DefaultValue'].replace( - "$endpointPathName", path) - item['Key'] = item['key'].replace("$endpointPathKey", filename) - # todo: endpoints added to plist here - with open(settings_location + filename + ".plist", "wb") as fout: - new_endpoint = endpoint + # make a new endpoint plist instance + new_endpoint = endpoint - map(lambda item: replaceKeys, new_endpoint["PreferenceSpecifiers"]) + # replace keys in all the endpoint items + # todo: this doesn't work, try again - # newplist = newplist.replace("$endpointPathName", endpointPath).replace( - # "$endpointPathKey", filename) + new_endpoint["PreferenceSpecifiers"] = map(lambda item: replace_keys, new_endpoint["PreferenceSpecifiers"]) - # indexes = "0" - for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): - setting["Values"] = list(range(0, len(files))) - setting["Titles"] = files - # indexes = indexes + "\n\t\t\t\t" + \ - # str(i) + "" - # newplist = newplist.replace("$indexMockFiles", indexes) - - # mockFiles = "" + files[0] + "" - # for i in range(1, len(files)): - # mockFiles = mockFiles + "\n\t\t\t\t" + \ - # files[i] + "" - # newplist = newplist.replace("$mockFiles", mockFiles) + # set the mockfile "values" and "titles" fields + for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): + setting["Values"] = list(range(0, len(files))) + setting["Titles"] = files + with open(settings_location + filename + ".plist", "wb") as fout: plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) # create general plist from json @@ -187,9 +195,11 @@ def replaceKeys(item, path, filename): parser = argparse.ArgumentParser( description='Generate Settings.bundle for DDMockiOS') parser.add_argument('mockfiles_path', nargs='?', - default="DDMockiOS/resources/mockfiles") + default="Resources/mockfiles") + parser.add_argument('output_path', nargs='?', + default="Settings.bundle/") args = parser.parse_args() # start execution - main(args.mockfiles_path) + main(args.mockfiles_path, args.output_path) diff --git a/Sources/DDMockURLProtocolClass.swift b/Sources/DDMockURLProtocolClass.swift index 73ab64f..72b8bee 100644 --- a/Sources/DDMockURLProtocolClass.swift +++ b/Sources/DDMockURLProtocolClass.swift @@ -44,7 +44,7 @@ public class DDMockURLProtocolClass: URLProtocol { This is an abstract class by default. */ - override public class func canonicalRequest(for request: URLRequest) -> URLRequest { + public override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } @@ -52,7 +52,7 @@ public class DDMockURLProtocolClass: URLProtocol { /** this is where everything happens */ - override public func startLoading() { + public override func startLoading() { // fetch item guard @@ -113,4 +113,7 @@ public class DDMockURLProtocolClass: URLProtocol { }) } + public override func stopLoading() { + // nothing actually loading + } } diff --git a/Sources/DDMockiOS.swift b/Sources/DDMockiOS.swift index a8df8be..f8abd43 100644 --- a/Sources/DDMockiOS.swift +++ b/Sources/DDMockiOS.swift @@ -6,10 +6,6 @@ import Foundation */ public final class DDMock { - // todo: make this more obvious or configurable - /// path under resources directory - private let mockDirectory = "/mockfiles" - /// enforces mocks only and no API fall-through internal var strict: Bool = false @@ -47,7 +43,7 @@ public final class DDMock { self.strict = strict // todo: resource path - let path = Bundle.main.resourcePath! + mockDirectory + let path = Bundle.main.resourcePath! + Constants.mockDirectory // load the files in the mock directory repository = MockRepository(path: path, fm: FileManager.default) diff --git a/Sources/MockEntry.swift b/Sources/MockEntry.swift index 25892e4..a8ef971 100644 --- a/Sources/MockEntry.swift +++ b/Sources/MockEntry.swift @@ -29,7 +29,7 @@ struct MockEntry: Codable { self.files = files } - // this is the index of the selected file in the files list for an entry + // this is the key for the selected file in the files list for an entry func getSelectedFile() -> String { let index = UserDefaultsHelper.getInteger(key: path, item: .mockFile) return files[index] diff --git a/Sources/MockRepository.swift b/Sources/MockRepository.swift index 9423e14..40d1362 100644 --- a/Sources/MockRepository.swift +++ b/Sources/MockRepository.swift @@ -35,7 +35,6 @@ internal class MockRepository { } // get the key - // todo: check absolute string is sane key let key = url.deletingLastPathComponent().absoluteString // put into the dictionary @@ -66,7 +65,7 @@ internal class MockRepository { } // entry can override this value itself - return entry.useRealAPI() + return !entry.useRealAPI() } /** @@ -79,7 +78,7 @@ internal class MockRepository { onMissing: (_ path: String?) -> Void) -> MockEntry? { // get the entry - let entry = getMockEntry(path: path, method: "") // todo + let entry = getMockEntry(path: path, method: method) // If strict mode is enabled, a missing entry is an error. Call handler. // this will still fall through @@ -102,10 +101,11 @@ internal class MockRepository { need to include the method to get it in a way that makes sense */ private func getMockEntry(path: String, method: String) -> MockEntry? { - // method string is always lowercased - let fullPath = path.replacingRegexMatches( + let matches = path.replacingRegexMatches( pattern: "^/", - replaceWith: "") + "/" + method.lowercased() + replaceWith: "") + // method string is always lowercased + let fullPath = "\(matches)/\(method.lowercased())/" // return an entry for either a non-wildcard or wildcard path return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) diff --git a/Sources/ResponseHelper.swift b/Sources/ResponseHelper.swift index 288b369..7e16757 100644 --- a/Sources/ResponseHelper.swift +++ b/Sources/ResponseHelper.swift @@ -64,7 +64,9 @@ class ResponseHelper { // get the path // todo: isn't this encoded in the entry? - let path = entry.path + // this is the path of the mockfiles folder + + let path = Bundle.main.resourcePath! + Constants.mockDirectory let url = URL(fileURLWithPath: "\(path)/\(file)") From f25934fd277755597e3ef5ff79be2e14780c7ead Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 11 Jun 2021 14:29:54 +1000 Subject: [PATCH 21/26] python clean up + fix bugs in endpoint plist generation --- Generate/ddmock.py | 85 ++++++++++++++++++++-------------------------- 1 file changed, 37 insertions(+), 48 deletions(-) diff --git a/Generate/ddmock.py b/Generate/ddmock.py index c363342..792fbaa 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -6,6 +6,8 @@ import logging import json import argparse +import copy + # todo: visibility @@ -19,32 +21,28 @@ def generate_map(mockfiles_path): # walks all the mockfiles and for each creates just the leading path? # recursive directory traversal - # todo: what is subdir + # todo: define subdir - it is the subdirectory configured (?) for subdir, dirs, files in os.walk(mockfiles_path): print(subdir) # iterate through mockfiles for file in files: # only the json files - if files.endswith(".json"): + if file.endswith(".json"): # todo: think there is a more normal way to check for only json files - # create path - # filepath = f"{subdir}/{file}" - # this is to get the key from the json object path # we can do this more directly endpointPath = subdir.replace(mockfiles_path, "") # strip the leading slash if present - # todo: presumably this is always present? - # maybe not for relative paths - # we should use a Path object instead + # todo: we should use a Path object instead if endpointPath.startswith("/"): endpointPath = endpointPath.replace("/", "", 1) - # map is accessed here (therefore make this a function duh) + # map is accessed here (therefore make this a function return point) + # this logic is duplicated in the swift code # this does the same thing as swift code to run it # "get or insert" if endpointPath in endpoint_map: @@ -56,24 +54,10 @@ def generate_map(mockfiles_path): return endpoint_map -def load_json(path): - with open(path, "r") as file: - return json.load(file) - - -def replace_keys(item, path, filename): - # todo: clarify what is happening - # todo: make a type for these string keys - item['DefaultValue'] = item['DefaultValue'].replace( - "$endpointPathName", path) - print(f"dv: {item['DefaultValue']}") - item['Key'] = item['key'].replace("$endpointPathKey", filename) - return item - - def load_json_resource(res): print(f"{res}") - res = load_json(res) + with open(res, "r") as file: + res = json.load(file) return res @@ -90,9 +74,14 @@ def main(mockfiles_path, output_path): print(f"wd: {cwd}") # get resource path from canonical path of script - path = os.path.dirname(os.path.realpath(__file__)) - path = pathlib.Path(path) - path = path.parent.joinpath("Resources").absolute() + def get_ddmock_path(dir): + path = os.path.dirname(os.path.realpath(__file__)) + path = pathlib.Path(path) + path = path.parent.joinpath(dir).absolute() + return path + + path = get_ddmock_path("Resources") + print(f"templates: {path}") # first create the map @@ -120,17 +109,6 @@ def main(mockfiles_path, output_path): root = load_json_resource(path.joinpath("root.json")) endpoint = load_json_resource(path.joinpath("endpoint.json")) - # ** short circuit for testing - -# with open(settings_location + "Root.plist", "rb") as root: - # with open("resources/endpoint.json", "w+") as output: - # plist_bytes = plist.encode(encoding='utf-8') - # plist = plistlib.loads(plist_bytes, fmt=plistlib.FMT_XML) - # json.dump(plist, output, indent=4) - # print("dumped root json") - - # return - # ** # for path & files in map for endpoint_path, files in endpoint_map.items(): @@ -147,19 +125,30 @@ def main(mockfiles_path, output_path): # create a copy of the endpoint plist replacing # the $endpointPathName key -> endpointPath - # the $indexMockFiles key -> indexes - # the $mockfiles key -> files[i] - # then write the new file at settings_location + filename + .plist - # creating plist file for endpoint + # copy endpoint plist + new_endpoint = copy.deepcopy(endpoint) - # make a new endpoint plist instance - new_endpoint = endpoint + print(f"ne: {new_endpoint}") # replace keys in all the endpoint items - # todo: this doesn't work, try again - new_endpoint["PreferenceSpecifiers"] = map(lambda item: replace_keys, new_endpoint["PreferenceSpecifiers"]) + # for each item in preference specifiers list + for index, item in enumerate(new_endpoint["PreferenceSpecifiers"]): + # construct a new item + new_item = {} + # for every key value par in the item dict + for key, value in item.items(): + try: + new_value = value.replace("$endpointPathName", f"{endpoint_path}") + new_value = new_value.replace("$endpointPathKey", filename) + new_item[key] = new_value + except AttributeError: + # value can be any type + new_item[key] = value + + new_endpoint["PreferenceSpecifiers"][index] = new_item + # item['Key'] = item['key'].replace("$endpointPathKey", filename) # set the mockfile "values" and "titles" fields for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): @@ -174,7 +163,7 @@ def main(mockfiles_path, output_path): # load the template general = path.joinpath("general.json") - general = load_json(general) + general = load_json_resource(general) # dump plist with open(os.path.join(settings_location, "general.plist"), "wb") as output: From b80b7a896705561b643dabbcca72c2c78d669429 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 11 Jun 2021 15:48:08 +1000 Subject: [PATCH 22/26] small additional tidy --- Generate/ddmock.py | 78 ++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 792fbaa..c68ad62 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -28,28 +28,29 @@ def generate_map(mockfiles_path): # iterate through mockfiles for file in files: # only the json files - if file.endswith(".json"): - - # todo: think there is a more normal way to check for only json files - - # this is to get the key from the json object path - # we can do this more directly - endpointPath = subdir.replace(mockfiles_path, "") - - # strip the leading slash if present - # todo: we should use a Path object instead - if endpointPath.startswith("/"): - endpointPath = endpointPath.replace("/", "", 1) - - # map is accessed here (therefore make this a function return point) - # this logic is duplicated in the swift code - # this does the same thing as swift code to run it - # "get or insert" - if endpointPath in endpoint_map: - files = endpoint_map[endpointPath] - files.append(file) - else: - endpoint_map[endpointPath] = [file] + if not file.endswith(".json"): + continue + + # todo: think there is a more normal way to check for only json files + + # this is to get the key from the json object path + # we can do this more directly + endpointPath = subdir.replace(mockfiles_path, "") + + # strip the leading slash if present + # todo: we should use a Path object instead + if endpointPath.startswith("/"): + endpointPath = endpointPath.replace("/", "", 1) + + # map is accessed here (therefore make this a function return point) + # this logic is duplicated in the swift code + # this does the same thing as swift code to run it + # "get or insert" + if endpointPath in endpoint_map: + files = endpoint_map[endpointPath] + files.append(file) + else: + endpoint_map[endpointPath] = [file] return endpoint_map @@ -69,25 +70,23 @@ def create_item(filename): return new_item -def main(mockfiles_path, output_path): - cwd = os.getcwd() - print(f"wd: {cwd}") +# get resource path from canonical path of script +def get_ddmock_path(resources_path): + path = os.path.dirname(os.path.realpath(__file__)) + path = pathlib.Path(path) + path = path.parent.joinpath(resources_path).absolute() + return path - # get resource path from canonical path of script - def get_ddmock_path(dir): - path = os.path.dirname(os.path.realpath(__file__)) - path = pathlib.Path(path) - path = path.parent.joinpath(dir).absolute() - return path +def main(mockfiles_path, output_path): path = get_ddmock_path("Resources") - - print(f"templates: {path}") + print(f"Template path: {path}") # first create the map # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") endpoint_map = generate_map(mockfiles_path) + print(f"{endpoint_map}") # todo: better / dynamic configuration @@ -110,6 +109,7 @@ def get_ddmock_path(dir): endpoint = load_json_resource(path.joinpath("endpoint.json")) # ** + # what do we get out of this block? make it a function # for path & files in map for endpoint_path, files in endpoint_map.items(): @@ -148,7 +148,6 @@ def get_ddmock_path(dir): new_item[key] = value new_endpoint["PreferenceSpecifiers"][index] = new_item - # item['Key'] = item['key'].replace("$endpointPathKey", filename) # set the mockfile "values" and "titles" fields for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): @@ -158,19 +157,18 @@ def get_ddmock_path(dir): with open(settings_location + filename + ".plist", "wb") as fout: plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) - # create general plist from json - print("Creating general.plist...") - - # load the template + # create general plist from json template + print("Load general.plist template...") general = path.joinpath("general.json") general = load_json_resource(general) - # dump plist + # write general plist + print("Writing general.plist...") with open(os.path.join(settings_location, "general.plist"), "wb") as output: plistlib.dump(general, output, fmt=plistlib.FMT_XML) # write root plist - print("Writing root plist...") + print("Writing Root.plist...") with open(settings_location + "Root.plist", "wb") as output: plistlib.dump(root, output, fmt=plistlib.FMT_XML) From bd28963fcca0797ed24b41440e7e8d3827e7d4f7 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 11 Jun 2021 16:33:26 +1000 Subject: [PATCH 23/26] move endpoint copy to function + additional clarity cleanup + constants --- Generate/ddmock.py | 98 ++++++++++++++++++++++------------------- Sources/Constants.swift | 5 +++ 2 files changed, 57 insertions(+), 46 deletions(-) create mode 100644 Sources/Constants.swift diff --git a/Generate/ddmock.py b/Generate/ddmock.py index c68ad62..7a4483b 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -9,11 +9,11 @@ import copy -# todo: visibility -# create the map of endpoints from mockfiles +# todo: visibility +# create the map of endpoints from mockfiles def generate_map(mockfiles_path): # init an empty map object endpoint_map = {} @@ -22,8 +22,7 @@ def generate_map(mockfiles_path): # recursive directory traversal # todo: define subdir - it is the subdirectory configured (?) - for subdir, dirs, files in os.walk(mockfiles_path): - print(subdir) + for current, dirs, files in os.walk(mockfiles_path): # iterate through mockfiles for file in files: @@ -31,14 +30,16 @@ def generate_map(mockfiles_path): if not file.endswith(".json"): continue + # currently this only needs to look at files + # and create the key from the path + # should be a simpler api to use + # todo: think there is a more normal way to check for only json files # this is to get the key from the json object path - # we can do this more directly - endpointPath = subdir.replace(mockfiles_path, "") + endpointPath = current.replace(mockfiles_path, "") # strip the leading slash if present - # todo: we should use a Path object instead if endpointPath.startswith("/"): endpointPath = endpointPath.replace("/", "", 1) @@ -55,6 +56,7 @@ def generate_map(mockfiles_path): return endpoint_map +# open a file with the name res and return it def load_json_resource(res): print(f"{res}") with open(res, "r") as file: @@ -62,7 +64,8 @@ def load_json_resource(res): return res -def create_item(filename): +# pure function returns an object to add to root preference specifier +def create_root_item(filename): new_item = {} new_item['Type'] = 'PSChildPaneSpecifier' new_item['File'] = filename @@ -78,6 +81,40 @@ def get_ddmock_path(resources_path): return path +def create_endpoint_plist(endpoint, endpoint_path, filename, files): + + # copy a new endpoint object + new_endpoint = copy.deepcopy(endpoint) + + print(f"new endpoint: {new_endpoint}") + + # replace variable keys in all the endpoint items + + # for each item in preference specifiers list + for index, item in enumerate(new_endpoint["PreferenceSpecifiers"]): + # construct a new item + new_item = {} + # for every key value par in the item dict + for key, value in item.items(): + try: + new_value = value.replace( + "$endpointPathName", f"{endpoint_path}") + new_value = new_value.replace("$endpointPathKey", filename) + new_item[key] = new_value + except AttributeError: + # value can be any type + new_item[key] = value + + new_endpoint["PreferenceSpecifiers"][index] = new_item + + # set the mockfile "values" and "titles" fields + for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): + setting["Values"] = list(range(0, len(files))) + setting["Titles"] = files + + return new_endpoint + + def main(mockfiles_path, output_path): path = get_ddmock_path("Resources") print(f"Template path: {path}") @@ -90,11 +127,9 @@ def main(mockfiles_path, output_path): print(f"{endpoint_map}") # todo: better / dynamic configuration - # this is from invokation site, turns out settings_location = output_path # start creating settings bundle - # todo: what is the settings bundle & where are we creating it? print(f"Creating Settings.bundle at {settings_location}...") # Settings.bundle is really just a directory @@ -108,52 +143,23 @@ def main(mockfiles_path, output_path): root = load_json_resource(path.joinpath("root.json")) endpoint = load_json_resource(path.joinpath("endpoint.json")) - # ** - # what do we get out of this block? make it a function - # for path & files in map + # save each endpoint as a plist for endpoint_path, files in endpoint_map.items(): print(f"Adding endpoint: {endpoint_path}") - # replaces the slashes with periods for + # replaces the slashes with periods for ... filename = endpoint_path.replace("/", ".") # add endpoint to root plist - - new_item = create_item(filename) + new_item = create_root_item(filename) root['PreferenceSpecifiers'].append(new_item) - # create a copy of the endpoint plist replacing - # the $endpointPathName key -> endpointPath - - # copy endpoint plist - new_endpoint = copy.deepcopy(endpoint) - - print(f"ne: {new_endpoint}") - - # replace keys in all the endpoint items - - # for each item in preference specifiers list - for index, item in enumerate(new_endpoint["PreferenceSpecifiers"]): - # construct a new item - new_item = {} - # for every key value par in the item dict - for key, value in item.items(): - try: - new_value = value.replace("$endpointPathName", f"{endpoint_path}") - new_value = new_value.replace("$endpointPathKey", filename) - new_item[key] = new_value - except AttributeError: - # value can be any type - new_item[key] = value - - new_endpoint["PreferenceSpecifiers"][index] = new_item - - # set the mockfile "values" and "titles" fields - for setting in filter(lambda item: item['Title'] == "Mock file", new_endpoint['PreferenceSpecifiers']): - setting["Values"] = list(range(0, len(files))) - setting["Titles"] = files + # create new endpoint object from endpoint template + new_endpoint = create_endpoint_plist( + endpoint, endpoint_path, filename, files) + # dump the endpoint to plist with open(settings_location + filename + ".plist", "wb") as fout: plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) diff --git a/Sources/Constants.swift b/Sources/Constants.swift new file mode 100644 index 0000000..38ff077 --- /dev/null +++ b/Sources/Constants.swift @@ -0,0 +1,5 @@ +enum Constants { + // todo: make this more obvious or configurable + /// path under resources directory + static let mockDirectory = "/mockfiles" +} From 6afd972e7aee4ff5fc771cb81c26c07a55c426ee Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 11 Jun 2021 16:49:12 +1000 Subject: [PATCH 24/26] clean up output --- Generate/ddmock.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 7a4483b..2529d34 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -9,7 +9,6 @@ import copy - # todo: visibility @@ -58,7 +57,7 @@ def generate_map(mockfiles_path): # open a file with the name res and return it def load_json_resource(res): - print(f"{res}") + logging.info(f"loading json resource: {res}") with open(res, "r") as file: res = json.load(file) return res @@ -86,7 +85,7 @@ def create_endpoint_plist(endpoint, endpoint_path, filename, files): # copy a new endpoint object new_endpoint = copy.deepcopy(endpoint) - print(f"new endpoint: {new_endpoint}") + logging.info(f"new endpoint: {new_endpoint}") # replace variable keys in all the endpoint items @@ -116,30 +115,31 @@ def create_endpoint_plist(endpoint, endpoint_path, filename, files): def main(mockfiles_path, output_path): + print("Running *.plist generation...") + print(f"Path to mockfiles: {mockfiles_path}") + print(f"Output path: {output_path}") + path = get_ddmock_path("Resources") - print(f"Template path: {path}") + # print(f"Template path: {path}") # first create the map # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") endpoint_map = generate_map(mockfiles_path) - print(f"{endpoint_map}") - - # todo: better / dynamic configuration - settings_location = output_path + # todo: lazy evaluation in logging + logging.info(f" map: {endpoint_map}") # start creating settings bundle - print(f"Creating Settings.bundle at {settings_location}...") + print(f"Creating Settings.bundle...") # Settings.bundle is really just a directory # first create directory if it doesn't exist - if not os.path.exists(settings_location): - os.makedirs(settings_location) + if not os.path.exists(output_path): + os.makedirs(output_path) # load templates print("Loading JSON templates...") - root = load_json_resource(path.joinpath("root.json")) endpoint = load_json_resource(path.joinpath("endpoint.json")) @@ -160,7 +160,7 @@ def main(mockfiles_path, output_path): endpoint, endpoint_path, filename, files) # dump the endpoint to plist - with open(settings_location + filename + ".plist", "wb") as fout: + with open(output_path + filename + ".plist", "wb") as fout: plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) # create general plist from json template @@ -170,12 +170,12 @@ def main(mockfiles_path, output_path): # write general plist print("Writing general.plist...") - with open(os.path.join(settings_location, "general.plist"), "wb") as output: + with open(os.path.join(output_path, "general.plist"), "wb") as output: plistlib.dump(general, output, fmt=plistlib.FMT_XML) # write root plist print("Writing Root.plist...") - with open(settings_location + "Root.plist", "wb") as output: + with open(output_path + "Root.plist", "wb") as output: plistlib.dump(root, output, fmt=plistlib.FMT_XML) # finished @@ -184,14 +184,19 @@ def main(mockfiles_path, output_path): if __name__ == "__main__": - # parse arguments + # create argument parser parser = argparse.ArgumentParser( description='Generate Settings.bundle for DDMockiOS') + + # 1st argument is mockfiles directory parser.add_argument('mockfiles_path', nargs='?', default="Resources/mockfiles") + + # 2nd argument is output path parser.add_argument('output_path', nargs='?', default="Settings.bundle/") + # parse arguments args = parser.parse_args() # start execution From 620158e334ec1291e33540f9649ad9601d44f5c9 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Fri, 11 Jun 2021 18:10:27 +1000 Subject: [PATCH 25/26] fix strict mode flag --- .../example/get/{01_success.json => 01_success/body.json} | 0 Resources/mockfiles/example/get/01_success/headers.json | 0 Sources/DDMockURLProtocolClass.swift | 4 ++++ 3 files changed, 4 insertions(+) rename Resources/mockfiles/example/get/{01_success.json => 01_success/body.json} (100%) create mode 100644 Resources/mockfiles/example/get/01_success/headers.json diff --git a/Resources/mockfiles/example/get/01_success.json b/Resources/mockfiles/example/get/01_success/body.json similarity index 100% rename from Resources/mockfiles/example/get/01_success.json rename to Resources/mockfiles/example/get/01_success/body.json diff --git a/Resources/mockfiles/example/get/01_success/headers.json b/Resources/mockfiles/example/get/01_success/headers.json new file mode 100644 index 0000000..e69de29 diff --git a/Sources/DDMockURLProtocolClass.swift b/Sources/DDMockURLProtocolClass.swift index 72b8bee..2e3fee1 100644 --- a/Sources/DDMockURLProtocolClass.swift +++ b/Sources/DDMockURLProtocolClass.swift @@ -24,6 +24,9 @@ public class DDMockURLProtocolClass: URLProtocol { // todo: is this called for every request? is the mock retreived 2ce? /// public override class func canInit(with task: URLSessionTask) -> Bool { + + if DDMock.shared.strict { return true } + guard let req = task.currentRequest, let path = req.url?.path, @@ -113,6 +116,7 @@ public class DDMockURLProtocolClass: URLProtocol { }) } + /// Required override of abstract prototype, does nothing. public override func stopLoading() { // nothing actually loading } From 1e61953f0762b671c34c395e9084f03fcec50c17 Mon Sep 17 00:00:00 2001 From: Will Rigney Date: Sun, 13 Jun 2021 01:14:42 +1000 Subject: [PATCH 26/26] headers functioning mvp + significant rf --- DDMockiOS.xcodeproj/project.pbxproj | 42 ++++- Generate/ddmock.py | 143 +++++++++++++--- .../example/get/01_success/headers.json | 0 Resources/mockfiles/example/get/body.h.json | 4 + .../example/get/{01_success => }/body.json | 0 Resources/root.json | 3 +- Sources/{ => Extension}/String+Regex.swift | 0 Sources/{ => Helper}/ResponseHelper.swift | 14 +- Sources/{ => Helper}/UserDefaultsHelper.swift | 23 ++- Sources/{ => Mock}/MockEntry.swift | 22 +++ Sources/Mock/MockRepository.swift | 91 ++++++++++ Sources/Mock/MockStorage.swift | 66 ++++++++ Sources/MockRepository.swift | 158 ------------------ .../{ => Public}/DDMockURLProtocolClass.swift | 18 +- Sources/{ => Public}/DDMockiOS.swift | 14 +- 15 files changed, 398 insertions(+), 200 deletions(-) delete mode 100644 Resources/mockfiles/example/get/01_success/headers.json create mode 100644 Resources/mockfiles/example/get/body.h.json rename Resources/mockfiles/example/get/{01_success => }/body.json (100%) rename Sources/{ => Extension}/String+Regex.swift (100%) rename Sources/{ => Helper}/ResponseHelper.swift (87%) rename Sources/{ => Helper}/UserDefaultsHelper.swift (50%) rename Sources/{ => Mock}/MockEntry.swift (68%) create mode 100644 Sources/Mock/MockRepository.swift create mode 100644 Sources/Mock/MockStorage.swift delete mode 100644 Sources/MockRepository.swift rename Sources/{ => Public}/DDMockURLProtocolClass.swift (87%) rename Sources/{ => Public}/DDMockiOS.swift (100%) diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index f7ae2bc..bc8e394 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D95267060EF0056AC3E /* ResponseHelper.swift */; }; C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9815D9D2670635C0056AC3E /* MockRepository.swift */; }; C9B28FB32671C209007C31A9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B28FB22671C209007C31A9 /* Constants.swift */; }; + C9B29046267508B1007C31A9 /* MockStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9B29045267508B1007C31A9 /* MockStorage.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -57,6 +58,7 @@ C9815E5F2670B7B00056AC3E /* root.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = root.json; sourceTree = ""; }; C9B28EE62670D9CE007C31A9 /* endpoint.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = endpoint.json; sourceTree = ""; }; C9B28FB22671C209007C31A9 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + C9B29045267508B1007C31A9 /* MockStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockStorage.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -161,16 +163,49 @@ isa = PBXGroup; children = ( 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, + C9B28FB22671C209007C31A9 /* Constants.swift */, + C9B2904426750890007C31A9 /* Public */, + C9B2904F267508FD007C31A9 /* Mock */, + C9B29049267508E1007C31A9 /* Helper */, + C9B2904C267508EA007C31A9 /* Extension */, + ); + path = Sources; + sourceTree = ""; + }; + C9B2904426750890007C31A9 /* Public */ = { + isa = PBXGroup; + children = ( 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */, 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, - C9815D9D2670635C0056AC3E /* MockRepository.swift */, + ); + path = Public; + sourceTree = ""; + }; + C9B29049267508E1007C31A9 /* Helper */ = { + isa = PBXGroup; + children = ( C9815D95267060EF0056AC3E /* ResponseHelper.swift */, - C9B28FB22671C209007C31A9 /* Constants.swift */, 9118D8BF223F1B4C00195DC1 /* UserDefaultsHelper.swift */, + ); + path = Helper; + sourceTree = ""; + }; + C9B2904C267508EA007C31A9 /* Extension */ = { + isa = PBXGroup; + children = ( C9334FC7264E998D00190EB7 /* String+Regex.swift */, + ); + path = Extension; + sourceTree = ""; + }; + C9B2904F267508FD007C31A9 /* Mock */ = { + isa = PBXGroup; + children = ( + C9815D9D2670635C0056AC3E /* MockRepository.swift */, + C9B29045267508B1007C31A9 /* MockStorage.swift */, 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, ); - path = Sources; + path = Mock; sourceTree = ""; }; /* End PBXGroup section */ @@ -316,6 +351,7 @@ 9118D8C3223F1B4C00195DC1 /* UserDefaultsHelper.swift in Sources */, C9815D9E2670635C0056AC3E /* MockRepository.swift in Sources */, C9334FC8264E998D00190EB7 /* String+Regex.swift in Sources */, + C9B29046267508B1007C31A9 /* MockStorage.swift in Sources */, 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */, 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */, C9815D96267060EF0056AC3E /* ResponseHelper.swift in Sources */, diff --git a/Generate/ddmock.py b/Generate/ddmock.py index 2529d34..c568aa2 100755 --- a/Generate/ddmock.py +++ b/Generate/ddmock.py @@ -1,6 +1,4 @@ import os -import shutil -import sys import plistlib import pathlib import logging @@ -9,14 +7,18 @@ import copy -# todo: visibility - - # create the map of endpoints from mockfiles def generate_map(mockfiles_path): # init an empty map object endpoint_map = {} + # init header object + # this is actually keyed including the individual file, not just the endpoint + # this is a map between string keys and idk what + header_map = {} + + # todo: may be a way to do this in two traversals using glob + # walks all the mockfiles and for each creates just the leading path? # recursive directory traversal @@ -25,10 +27,35 @@ def generate_map(mockfiles_path): # iterate through mockfiles for file in files: + # todo: open this up to all filetypes + # should potentially change the application type + # or do this with headers? # only the json files if not file.endswith(".json"): continue + # process header files separately + if file.endswith(".h.json"): + with open(current + '/' + file, "r+") as headers: + res = json.load(headers) + # todo: duplicate + key = current.replace(mockfiles_path, "") + + # strip the leading slash if present + if key.startswith("/"): + key = key.replace("/", "", 1) + + # add the trailing file path for header keys + key = f"{get_canonical_key(key)}.{file}" + + # not working in xcode + # key.removesuffix(".h.json") + if key.endswith('.h.json'): + key = key[:-7] # ugly magic + + header_map[key] = res + continue + # currently this only needs to look at files # and create the key from the path # should be a simpler api to use @@ -36,26 +63,30 @@ def generate_map(mockfiles_path): # todo: think there is a more normal way to check for only json files # this is to get the key from the json object path - endpointPath = current.replace(mockfiles_path, "") + key = current.replace(mockfiles_path, "") # strip the leading slash if present - if endpointPath.startswith("/"): - endpointPath = endpointPath.replace("/", "", 1) + if key.startswith("/"): + key = key.replace("/", "", 1) # map is accessed here (therefore make this a function return point) # this logic is duplicated in the swift code # this does the same thing as swift code to run it # "get or insert" - if endpointPath in endpoint_map: - files = endpoint_map[endpointPath] + if key in endpoint_map: + files = endpoint_map[key] files.append(file) else: - endpoint_map[endpointPath] = [file] + endpoint_map[key] = [file] + + return (endpoint_map, header_map) + - return endpoint_map +# def generate_header_map(): + + # open a file with the name res and return it -# open a file with the name res and return it def load_json_resource(res): logging.info(f"loading json resource: {res}") with open(res, "r") as file: @@ -72,6 +103,24 @@ def create_root_item(filename): return new_item +# create an item to add to endpoint plist for the group header for headers +def create_headers_group_item(title): + new_item = {} + new_item['Type'] = 'PSGroupSpecifier' + new_item['Title'] = title + return new_item + + +# create a new item to represent a header from the key, title & value +def create_headers_item(key, title, value): + new_item = {} + new_item['Type'] = "PSTextFieldSpecifier" # PSTitleValueSpecifier" + new_item['DefaultValue'] = value + new_item['Title'] = title + new_item["Key"] = key + return new_item + + # get resource path from canonical path of script def get_ddmock_path(resources_path): path = os.path.dirname(os.path.realpath(__file__)) @@ -80,6 +129,15 @@ def get_ddmock_path(resources_path): return path +# transforms some input (most likely a path) into a canonical key +# relies on the input being unique to be "canonical" +def get_canonical_key(path): + key = path.replace("/", ".") + return key + + +# creates a copy of endpoint & replaces keys +# for endpointh path and endpoint key (filename) def create_endpoint_plist(endpoint, endpoint_path, filename, files): # copy a new endpoint object @@ -101,7 +159,7 @@ def create_endpoint_plist(endpoint, endpoint_path, filename, files): new_value = new_value.replace("$endpointPathKey", filename) new_item[key] = new_value except AttributeError: - # value can be any type + # value can be any type, may not be string new_item[key] = value new_endpoint["PreferenceSpecifiers"][index] = new_item @@ -125,13 +183,13 @@ def main(mockfiles_path, output_path): # first create the map # this is where the directory traversal happens print("Creating map of endpoint paths and mock files...") - endpoint_map = generate_map(mockfiles_path) + (endpoint_map, header_map) = generate_map(mockfiles_path) # todo: lazy evaluation in logging logging.info(f" map: {endpoint_map}") # start creating settings bundle - print(f"Creating Settings.bundle...") + print("Creating Settings.bundle...") # Settings.bundle is really just a directory # first create directory if it doesn't exist @@ -149,18 +207,63 @@ def main(mockfiles_path, output_path): print(f"Adding endpoint: {endpoint_path}") # replaces the slashes with periods for ... - filename = endpoint_path.replace("/", ".") + canonical_key = get_canonical_key(endpoint_path) # add endpoint to root plist - new_item = create_root_item(filename) + new_item = create_root_item(canonical_key) root['PreferenceSpecifiers'].append(new_item) # create new endpoint object from endpoint template new_endpoint = create_endpoint_plist( - endpoint, endpoint_path, filename, files) + endpoint, endpoint_path, canonical_key, files) + + # header generation + + # todo: this is currently not very good, selecting different mockfiles should change headers + + # check if there are any headers and add them if there are + for file in files: + try: + # try and get some headers + # this is the header key e.g. todos.get.010_title + key = get_canonical_key(f"{endpoint_path}.{file[:-5]}") + headers = header_map[key] + + except KeyError: + print(f"no headers for {endpoint_path}.{file}, key: {key}") + continue + + # use python dicts to build ios plists more easily + + # todo: list comprehension is more pythonic + for (index, (title, value)) in enumerate(headers.items()): + + # if we survived this far, add the group settings heading + # group specifiers are needed for the correct ordering + group = create_headers_group_item(title) + new_endpoint['PreferenceSpecifiers'].append(group) + + # separate items for title and value + # keys for headers is endpoint path + header index + # this is the oother header key + key = get_canonical_key(f"{endpoint_path}.{file[:-5]}") + key = f"{key}{index}_title" + # create a new item for the header + group = create_headers_item(key, "Title", title) + # add the item to the list of preference specifiers + new_endpoint['PreferenceSpecifiers'].append(group) + + key = get_canonical_key(f"{endpoint_path}.{file[:-5]}") + key = f"{key}{index}_value" + # create a new item for the header + group = create_headers_item(key, "Value", value) + # add the item to the list of preference specifiers + new_endpoint['PreferenceSpecifiers'].append(group) + + print(f"added headers: {headers}") # dump the endpoint to plist - with open(output_path + filename + ".plist", "wb") as fout: + with open(output_path + canonical_key + ".plist", "wb") as fout: plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) # create general plist from json template diff --git a/Resources/mockfiles/example/get/01_success/headers.json b/Resources/mockfiles/example/get/01_success/headers.json deleted file mode 100644 index e69de29..0000000 diff --git a/Resources/mockfiles/example/get/body.h.json b/Resources/mockfiles/example/get/body.h.json new file mode 100644 index 0000000..c9f1d5a --- /dev/null +++ b/Resources/mockfiles/example/get/body.h.json @@ -0,0 +1,4 @@ +{ + "example-header-1": "some value", + "example-header-2": false +} \ No newline at end of file diff --git a/Resources/mockfiles/example/get/01_success/body.json b/Resources/mockfiles/example/get/body.json similarity index 100% rename from Resources/mockfiles/example/get/01_success/body.json rename to Resources/mockfiles/example/get/body.json diff --git a/Resources/root.json b/Resources/root.json index bf244a7..1d0016f 100644 --- a/Resources/root.json +++ b/Resources/root.json @@ -13,7 +13,8 @@ }, { "Title": "MOCK", - "Type": "PSGroupSpecifier" + "Type": "PSGroupSpecifier", + "FooterText": "Note that strict mode being set at build time will override the 'Use real APIs' setting." } ], "StringsTable": "Root" diff --git a/Sources/String+Regex.swift b/Sources/Extension/String+Regex.swift similarity index 100% rename from Sources/String+Regex.swift rename to Sources/Extension/String+Regex.swift diff --git a/Sources/ResponseHelper.swift b/Sources/Helper/ResponseHelper.swift similarity index 87% rename from Sources/ResponseHelper.swift rename to Sources/Helper/ResponseHelper.swift index 7e16757..8b648f6 100644 --- a/Sources/ResponseHelper.swift +++ b/Sources/Helper/ResponseHelper.swift @@ -43,6 +43,15 @@ class ResponseHelper { return headers } + // this maybe doesn't belong here + static func getKeyFromPath(path: String, method: String) -> String { + let matches = path.replacingRegexMatches( + pattern: "^/", + replaceWith: "") + // method string is always lowercased + return "\(matches)/\(method.lowercased())/" + } + static func createMockResponse( url: URL, statusCode: Int, @@ -62,10 +71,7 @@ class ResponseHelper { let file = entry.getSelectedFile() - // get the path - // todo: isn't this encoded in the entry? - // this is the path of the mockfiles folder - +// todo: file should just be a string of the directory (?) let path = Bundle.main.resourcePath! + Constants.mockDirectory let url = URL(fileURLWithPath: "\(path)/\(file)") diff --git a/Sources/UserDefaultsHelper.swift b/Sources/Helper/UserDefaultsHelper.swift similarity index 50% rename from Sources/UserDefaultsHelper.swift rename to Sources/Helper/UserDefaultsHelper.swift index a4fc1c8..f61316b 100644 --- a/Sources/UserDefaultsHelper.swift +++ b/Sources/Helper/UserDefaultsHelper.swift @@ -7,9 +7,16 @@ class UserDefaultsHelper { case endpoint = "_endpoint" case mockFile = "_mock_file" case useRealApi = "_use_real_api" + case headerValue = "_value" + case headerTitle = "_title" case globalUseRealApis = "use_real_apis" } + /** + Helper function, gets an item from the settings bundle + by replacing '/'s with '.' in the key, then adding the item ray value + Returns userdefaults item for this key. + */ static func getInteger(key: String, item: SettingsKey) -> Int { let key = getSettingsBundleKey(key: key) + item.rawValue return UserDefaults.standard.integer(forKey: key) @@ -19,9 +26,23 @@ class UserDefaultsHelper { let key = getSettingsBundleKey(key: key) + item.rawValue return UserDefaults.standard.object(forKey: key) as? T } + + static func getString(key: String, item: SettingsKey) -> String? { + let key = getSettingsBundleKey(key: key) + item.rawValue + return UserDefaults.standard.string(forKey: key) + } + + static func getTitleValuePair(key: String) -> (title: String?, value: String?)? { + let title = getString(key: key, item: .headerTitle) + let value = getString(key: key, item: .headerValue) + + if title == nil && value == nil { return nil } + + return (title, value) + } } -// replaces / with . for some undocumented reason +// replaces / with . to be consistent with other keys private func getSettingsBundleKey(key: String) -> String { return key.replacingOccurrences(of: "/", with: ".") } diff --git a/Sources/MockEntry.swift b/Sources/Mock/MockEntry.swift similarity index 68% rename from Sources/MockEntry.swift rename to Sources/Mock/MockEntry.swift index a8ef971..cc42c01 100644 --- a/Sources/MockEntry.swift +++ b/Sources/Mock/MockEntry.swift @@ -53,7 +53,29 @@ struct MockEntry: Codable { return UserDefaultsHelper.getObject( key: path, item: .responseTime) ?? MockEntry.defaultResponseTime + } + + func getHeaders() -> [String: String]? { + // get a group or an array? + // ok this one is funny + var headers: [String: String] = [:] + func getHeaderKey(_ i: Int) -> String { + let selectedFile = getSelectedFile() + let trimIndex = selectedFile.lastIndex(of: ".") + // probably a more readable way + let filename = trimIndex != nil + ? String(selectedFile.prefix(upTo: trimIndex!)) + : selectedFile + return "\(filename)\(i)" + } + var i = 0 + while + let (title, value) = UserDefaultsHelper.getTitleValuePair(key: getHeaderKey(i)) { + headers[title ?? ""] = value ?? "" + i += 1 + } + return headers } private func getEndpointUseRealAPI(key: String) -> Bool { diff --git a/Sources/Mock/MockRepository.swift b/Sources/Mock/MockRepository.swift new file mode 100644 index 0000000..36c31b9 --- /dev/null +++ b/Sources/Mock/MockRepository.swift @@ -0,0 +1,91 @@ +import Foundation + +/** + Internal storage wrapper for mock entries. + To reinitialise a list of mocks, just create a new MockRepository + and drop the old one. + */ +final class MockRepository { + + /// map storage of mock entries + private let storage: MockStorage + + /** + iterate through files & populate the mocks + */ + init(path: String, fm: FileManager) { + var entries: [String: MockEntry] = [:] + + // load mock files + fm + .enumerator(atPath: path)? + .forEach { + + guard + let path = $0 as? String, + let url = URL(string: path) else { + + return + } + guard + // todo: new file schema? + url.pathExtension == "json" else { + + return + } + + // get the key + let key = url.deletingLastPathComponent().absoluteString + + // put into the dictionary + if var entry = entries[key] { + // add the mock to the existing file list for this entry + entry.files.append(url.path) + } + else { + // create a new entry + entries[key] = MockEntry(path: key, files: [url.path]) + } + } + + self.storage = MockStorage(entries: entries) + } + + /// todo: doc + func hasEntry(path: String, method: String) -> Bool { + // get the key + let key = ResponseHelper.getKeyFromPath(path: path, method: method) + // return an entry for either a non-wildcard or wildcard path + // todo: slightly confusing names + guard + let entry = storage.getEntry(path: key) else { + return false + } + // entry can override this value itself + return !entry.useRealAPI() + } + + /** + get the mock entry, respecting strict mode + */ + func getEntry( + path: String, + method: String, + strict: Bool, + onMissing: (_ path: String?) -> Void) -> MockEntry? { + + // get the entry + let key = ResponseHelper.getKeyFromPath(path: path, method: method) + + // return an entry for either a non-wildcard or wildcard path + let entry = storage.getEntry(path: key) + + // If strict mode is enabled, a missing entry is an error. Call handler. + // this will still fall through and return nil + if strict && entry == nil { + onMissing(path) + } + + return entry + } +} diff --git a/Sources/Mock/MockStorage.swift b/Sources/Mock/MockStorage.swift new file mode 100644 index 0000000..4b6cb5e --- /dev/null +++ b/Sources/Mock/MockStorage.swift @@ -0,0 +1,66 @@ +/** + Class to wrap storage for mocks with nice get methods + */ +final class MockStorage { + private let entries: [String: MockEntry] + + init(entries: [String: MockEntry]) { + self.entries = entries + } + + /** + get either the value for the key if it exists, + or a regex entry if there is a match, + or nil + */ + func getEntry(path: String) -> MockEntry? { + return entries[path] ?? getRegexEntry(path: path) + } + + /** + get a possible regex entry map + iterates through the keys, for every key with a '_' + turns it into a regex and matches against path + panics on > 1 match + */ + private func getRegexEntry(path: String) -> MockEntry? { + // empty array + var matches: [MockEntry] = [] + + // iterate through mock entry keys (what are these?) + // these are the path without the filename, including method + // whatever we're looking for should be in the value + // to keep this lookup O(1) + for key in entries.keys { + + // if key contains _ + // this is to test if there is a wildcard to replace + // todo: mock entry should have its own regex + // shouldn't have to recompile for every request + if (key.contains("_")) { + + // replace matches of the wildcard with ... + // this matches _[^/]*_ in the path + // and replaces it with the string literal "_[^/]*_ + // this lets us use it as the string matcher later + let regex = key.replacingRegexMatches( + pattern: "_[^/]*_", + replaceWith: "[^/]*") + + // try and match the path with the new regex string + if path.matches(regex) { + + if let match = entries[key] { + matches.append(match) + } + } + } + } + // maximum of 1 match or panic + guard matches.count <= 1 else { + fatalError("Fatal Error: Multiple matches for regex entry.") + } + // return first or none + return matches.first + } +} diff --git a/Sources/MockRepository.swift b/Sources/MockRepository.swift deleted file mode 100644 index 40d1362..0000000 --- a/Sources/MockRepository.swift +++ /dev/null @@ -1,158 +0,0 @@ -import Foundation - -/** - Internal storage wrapper for mock entries. - To reinitialise a list of mocks, just create a new MockRepository - and drop the old one. - */ -internal class MockRepository { - - /// map storage of mock entries - private let mockEntries: [String: MockEntry] - - /** - iterate through files & populate the mocks - */ - init(path: String, fm: FileManager) { - var entries: [String: MockEntry] = [:] - - // load mock files - fm - .enumerator(atPath: path)? - .forEach { - - guard - let path = $0 as? String, - let url = URL(string: path) else { - - return - } - guard - // todo: new file schema - url.pathExtension == "json" else { - - return - } - - // get the key - let key = url.deletingLastPathComponent().absoluteString - - // put into the dictionary - if var entry = entries[key] { - // add the mock to the existing file list for this entry - entry.files.append(url.path) - } - else { - // create a new entry - entries[key] = MockEntry(path: key, files: [url.path]) - } - } - - self.mockEntries = entries - } - - /// todo: doc - func hasEntry(path: String, method: String) -> Bool { - - // idk what the idea of this map function useRealAPI - - // get the entry - // todo: cache - guard - let entry = getMockEntry(path: path, method: method) else { - - return false - } - - // entry can override this value itself - return !entry.useRealAPI() - } - - /** - get the mock entry, respecting strict mode - */ - func getEntry( - path: String, - method: String, - strict: Bool, - onMissing: (_ path: String?) -> Void) -> MockEntry? { - - // get the entry - let entry = getMockEntry(path: path, method: method) - - // If strict mode is enabled, a missing entry is an error. Call handler. - // this will still fall through - if strict && entry == nil { - onMissing(path) - } - - return entry - } - - /* - todo: consolidate mock entry types, add regex to mock entry itself - If regex entries were included in the same ds - hasMockEntry would look much simpler - and would not require retreiving the item - */ - - /** - this returns an entry simply by path - need to include the method to get it in a way that makes sense - */ - private func getMockEntry(path: String, method: String) -> MockEntry? { - let matches = path.replacingRegexMatches( - pattern: "^/", - replaceWith: "") - // method string is always lowercased - let fullPath = "\(matches)/\(method.lowercased())/" - - // return an entry for either a non-wildcard or wildcard path - return mockEntries[fullPath] ?? getRegexEntry(path: fullPath) - } - - // todo: simplify this a little - - private func getRegexEntry(path: String) -> MockEntry? { - // - var matches: [MockEntry] = [] - - // iterate through mock entry keys (what are these) - // these are the path without the filename, including method - // whatever we're looking for should be in the value - // to keep this lookup O(1) - for key in mockEntries.keys { - - // if key contains _ - // this is to test if there is a wildcard to replace - // todo: mock entry should have its own regex - // shouldn't have to recompile for every request - if (key.contains("_")) { - - // replace matches of the wildcard with ... - let regex = key.replacingRegexMatches(pattern: "_[^/]*_", replaceWith: "[^/]*") - - // - if path.matches(regex) { - - if let match = mockEntries[key] { - matches.append(match) - } - } - } - } - // maximum of 1 match or panic - guard matches.count <= 1 else { - fatalError("Fatal Error: Multiple matches for regex entry.") - } - // return first or none - return matches.first - } - - /** - Create a mock entry for the given url and returns it. - If a mock entry exists for this path it returns a new entry with the - new path added. - // todo: can we do this all at once? - */ -} diff --git a/Sources/DDMockURLProtocolClass.swift b/Sources/Public/DDMockURLProtocolClass.swift similarity index 87% rename from Sources/DDMockURLProtocolClass.swift rename to Sources/Public/DDMockURLProtocolClass.swift index 2e3fee1..784fab0 100644 --- a/Sources/DDMockURLProtocolClass.swift +++ b/Sources/Public/DDMockURLProtocolClass.swift @@ -22,7 +22,7 @@ public class DDMockURLProtocolClass: URLProtocol { } // todo: is this called for every request? is the mock retreived 2ce? - /// + // yes it is public override class func canInit(with task: URLSessionTask) -> Bool { if DDMock.shared.strict { return true } @@ -78,12 +78,18 @@ public class DDMockURLProtocolClass: URLProtocol { } // get response data - // todo: check in what case could this be nil\\ + // todo: check in what case could this be nil let data: Data? = ResponseHelper.getData(entry) // header dictionary - // todo: more configuration - let headers = ResponseHelper.getMockHeaders(contentLength: data?.count) + var headers = ResponseHelper.getMockHeaders(contentLength: data?.count) + + // if the entry has headers merge those too + if let entryHeaders = entry.getHeaders() { + headers.merge( + entryHeaders, + uniquingKeysWith: {(_, newValue) in newValue}) + } // get status code let statusCode = entry.getStatusCode() @@ -98,10 +104,10 @@ public class DDMockURLProtocolClass: URLProtocol { } // simulate response time - let time = TimeInterval(entry.getResponseTime() / 1000) + let time = TimeInterval(Float(entry.getResponseTime()) / 1000.0) // just use regular timer to async return the response - // async + await would be better + // todo: this isn't working correctly Timer.scheduledTimer( withTimeInterval: time, repeats: false, diff --git a/Sources/DDMockiOS.swift b/Sources/Public/DDMockiOS.swift similarity index 100% rename from Sources/DDMockiOS.swift rename to Sources/Public/DDMockiOS.swift index f8abd43..1dcf1d1 100644 --- a/Sources/DDMockiOS.swift +++ b/Sources/Public/DDMockiOS.swift @@ -49,6 +49,13 @@ public final class DDMock { repository = MockRepository(path: path, fm: FileManager.default) } + /** + reset the history + */ + public func clearHistory() { + matchedPaths.removeAll() + } + /** Check if an entry exists for a given path */ @@ -77,11 +84,4 @@ public final class DDMock { // return the entry return entry } - - /** - reset the history - */ - public func clearHistory() { - matchedPaths.removeAll() - } }