diff --git a/DDMockiOS.podspec b/DDMockiOS.podspec index 1799c2d..82b218a 100644 --- a/DDMockiOS.podspec +++ b/DDMockiOS.podspec @@ -2,38 +2,31 @@ # 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 # 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. Runtime configurable mocking library with highly flexible usage. Integrated tooling for delivery and testing teams.' 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 } + 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.source_files = 'Sources' spec.preserve_paths = [ - 'init-mocks.py', - ] + 'Generate/ddmock.py', + 'Resources/general.json', + 'Resources/root.json', + 'Resources/endpoint.json', + ] spec.swift_version = '5' spec.static_framework = true diff --git a/DDMockiOS.xcodeproj/project.pbxproj b/DDMockiOS.xcodeproj/project.pbxproj index 5b202da..bc8e394 100644 --- a/DDMockiOS.xcodeproj/project.pbxproj +++ b/DDMockiOS.xcodeproj/project.pbxproj @@ -10,10 +10,16 @@ 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 */; }; - 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMock.swift */; }; + 9118D8C0223F1B4C00195DC1 /* DDMockURLProtocolClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */; }; + 9118D8C1223F1B4C00195DC1 /* DDMockiOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9118D8BD223F1B4C00195DC1 /* DDMockiOS.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 */; }; + 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 */; }; + 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 */ @@ -29,19 +35,30 @@ /* 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 = ""; }; 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 = ""; }; - 9118D8BD223F1B4C00195DC1 /* DDMock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMock.swift; sourceTree = ""; }; + 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDMockURLProtocolClass.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 /* 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 */ @@ -67,7 +84,6 @@ isa = PBXGroup; children = ( 52C27F7F2609C1A600D04B74 /* DDMockiOSTests.swift */, - 52C27F812609C1A600D04B74 /* Info.plist */, ); path = DDMockiOSTests; sourceTree = ""; @@ -75,7 +91,6 @@ 52C27F8A2609C1AE00D04B74 /* Pod */ = { isa = PBXGroup; children = ( - 52C27F902609C23700D04B74 /* init-mocks.py */, 52C27F8F2609C20400D04B74 /* DDMockiOS.podspec */, 52C27F8E2609C20400D04B74 /* LICENSE */, ); @@ -94,9 +109,12 @@ 9118D8A7223F1AE300195DC1 = { isa = PBXGroup; children = ( + 9118D8B5223F1AE300195DC1 /* Info.plist */, + C9334F98264CC11100190EB7 /* Generate */, + C9B28E972670C587007C31A9 /* Sources */, + C9334FBF264D1C9200190EB7 /* Resources */, 52C27F8B2609C1B600D04B74 /* Documentation */, 52C27F8A2609C1AE00D04B74 /* Pod */, - 9118D8B3223F1AE300195DC1 /* DDMockiOS */, 52C27F7E2609C1A600D04B74 /* DDMockiOSTests */, 9118D8B2223F1AE300195DC1 /* Products */, ); @@ -111,17 +129,83 @@ name = Products; sourceTree = ""; }; - 9118D8B3223F1AE300195DC1 /* DDMockiOS */ = { + C9334F98264CC11100190EB7 /* Generate */ = { + isa = PBXGroup; + children = ( + C9334FAE264CF8A800190EB7 /* ddmock.py */, + C9334FC0264D1CB200190EB7 /* plist.py */, + C9334FC1264D1CB200190EB7 /* swagger_to_plist.py */, + C9334FAD264CF8A800190EB7 /* tests */, + ); + path = Generate; + sourceTree = ""; + }; + C9334FAD264CF8A800190EB7 /* tests */ = { + isa = PBXGroup; + children = ( + ); + path = tests; + sourceTree = ""; + }; + C9334FBF264D1C9200190EB7 /* Resources */ = { + isa = PBXGroup; + children = ( + C9334FDC265611E100190EB7 /* mockfiles */, + C9334FBC264D1AB500190EB7 /* general.json */, + C9B28EE62670D9CE007C31A9 /* endpoint.json */, + C9815E5F2670B7B00056AC3E /* root.json */, + C9334FB3264CF90400190EB7 /* general.plist */, + ); + path = Resources; + sourceTree = ""; + }; + C9B28E972670C587007C31A9 /* Sources */ = { isa = PBXGroup; children = ( - 9118D8BD223F1B4C00195DC1 /* DDMock.swift */, - 9118D8BC223F1B4C00195DC1 /* DDMockProtocol.swift */, - 9118D8BF223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift */, - 9118D8BE223F1B4C00195DC1 /* MockEntry.swift */, 9118D8B4223F1AE300195DC1 /* DDMockiOS.h */, - 9118D8B5223F1AE300195DC1 /* Info.plist */, + C9B28FB22671C209007C31A9 /* Constants.swift */, + C9B2904426750890007C31A9 /* Public */, + C9B2904F267508FD007C31A9 /* Mock */, + C9B29049267508E1007C31A9 /* Helper */, + C9B2904C267508EA007C31A9 /* Extension */, ); - path = DDMockiOS; + path = Sources; + sourceTree = ""; + }; + C9B2904426750890007C31A9 /* Public */ = { + isa = PBXGroup; + children = ( + 9118D8BD223F1B4C00195DC1 /* DDMockiOS.swift */, + 9118D8BC223F1B4C00195DC1 /* DDMockURLProtocolClass.swift */, + ); + path = Public; + sourceTree = ""; + }; + C9B29049267508E1007C31A9 /* Helper */ = { + isa = PBXGroup; + children = ( + C9815D95267060EF0056AC3E /* ResponseHelper.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 = Mock; sourceTree = ""; }; /* End PBXGroup section */ @@ -164,6 +248,7 @@ 9118D8AD223F1AE300195DC1 /* Sources */, 9118D8AE223F1AE300195DC1 /* Frameworks */, 9118D8AF223F1AE300195DC1 /* Resources */, + C9334F9D264CCBBA00190EB7 /* DDMOCK */, ); buildRules = ( ); @@ -182,7 +267,7 @@ attributes = { LastSwiftUpdateCheck = 1240; LastUpgradeCheck = 1240; - ORGANIZATIONNAME = "Bunduwongse, Natalie (AU - Sydney)"; + ORGANIZATIONNAME = "Deloitte Digital"; TargetAttributes = { 52C27F7C2609C1A600D04B74 = { CreatedOnToolsVersion = 12.4; @@ -223,11 +308,33 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + C9334FDD265611E100190EB7 /* mockfiles 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}/Generate/ddmock.py Resources/mockfiles\n"; + }; +/* End PBXShellScriptBuildPhase section */ + /* Begin PBXSourcesBuildPhase section */ 52C27F792609C1A600D04B74 /* Sources */ = { isa = PBXSourcesBuildPhase; @@ -241,10 +348,15 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 9118D8C3223F1B4C00195DC1 /* DDMockSettingsBundleHelper.swift in Sources */, - 9118D8C0223F1B4C00195DC1 /* DDMockProtocol.swift in Sources */, - 9118D8C1223F1B4C00195DC1 /* DDMock.swift in Sources */, + 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 */, 9118D8C2223F1B4C00195DC1 /* MockEntry.swift in Sources */, + C9B28FB32671C209007C31A9 /* Constants.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -352,7 +464,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; @@ -411,7 +523,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; @@ -434,7 +546,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)", @@ -445,7 +557,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"; }; @@ -462,7 +574,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/DDMockiOS/DDMock.swift b/DDMockiOS/DDMock.swift deleted file mode 100644 index 6bf8804..0000000 --- a/DDMockiOS/DDMock.swift +++ /dev/null @@ -1,152 +0,0 @@ -import Foundation - -public class DDMock { - 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 - public var onMissingMock: (_ path: String?) -> Void = {path in - fatalError("missing stub for path: \(path ?? "")") - } - - public static let shared = DDMock() - - 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) { - 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 { - mockEntries[key] = MockEntry(path: key, files: [url.path]) - } - } - - private func mockEntry(for 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. - matchedPaths.append(path) - return entry - } - - func hasMockEntry(path: String, method: String) -> EntrySetting { - switch getMockEntry(path: path, method: method, isTest: true)?.useRealAPI() { - case .none: - return .notFound - case .some(false): - return .mocked - case .some(true): - return .useRealAPI - } - } - - func getMockEntry(path: String, method: String) -> MockEntry? { - return getMockEntry(path: path, method: method, isTest: false) - } - - 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 let match = mockEntries[key] { - matches.append(match) - } - } - } - } - guard matches.count <= 1 else { - fatalError("Fatal Error: Multiple matches for regex entry.") - } - return matches.first - } - - 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 - } - return data - } -} - -extension String { - func matches(_ regex: String) -> Bool { - return self.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 - } -} - -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 deleted file mode 100644 index e8b829d..0000000 --- a/DDMockiOS/DDMockProtocol.swift +++ /dev/null @@ -1,67 +0,0 @@ -import Foundation - -enum EntrySetting { - case notFound - case mocked - case useRealAPI -} - -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) - 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 - } - } - - 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) { - // create mock response - let data: Data? = DDMock.shared.getData(entry) - var headers = [String: String]() - 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)! - - // Simulate response time - Thread.sleep(forTimeInterval: TimeInterval(entry.getResponseTime() / 1000)) - - // send response - self.client!.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) - - // send response data if available - if let data = data { - self.client!.urlProtocol(self, didLoad: data) - } - - // finish up - self.client!.urlProtocolDidFinishLoading(self) - } - } - } - - override public func stopLoading() { - //do nothing - } -} diff --git a/DDMockiOS/DDMockSettingsBundleHelper.swift b/DDMockiOS/DDMockSettingsBundleHelper.swift deleted file mode 100644 index 7a96082..0000000 --- a/DDMockiOS/DDMockSettingsBundleHelper.swift +++ /dev/null @@ -1,45 +0,0 @@ -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 - } - - static func globalUseRealAPIs() -> Bool { - return UserDefaults.standard.object(forKey: globalUseRealApis) as? Bool ?? false - } - - private static func getSettingsBundleKey(key: String) -> String { - return key.replacingOccurrences(of: "/", with: ".") - } -} diff --git a/DDMockiOS/MockEntry.swift b/DDMockiOS/MockEntry.swift deleted file mode 100644 index 38ddfa5..0000000 --- a/DDMockiOS/MockEntry.swift +++ /dev/null @@ -1,34 +0,0 @@ -import Foundation - -struct MockEntry: Codable { - internal static let defaultResponseTime = 400 - internal static let defaultStatusCode = 200 - - let path: 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 - } - - func getSelectedFile() -> Int { - return DDMockSettingsBundleHelper.getSelectedMockFile(key: path) - } - - func getStatusCode() -> Int { - return DDMockSettingsBundleHelper.getStatusCode(key: path) - } - - func useRealAPI() -> Bool { - return DDMockSettingsBundleHelper.useRealAPI(key: path) || - DDMockSettingsBundleHelper.globalUseRealAPIs() - } - - func getResponseTime() -> Int { - return DDMockSettingsBundleHelper.getResponseTime(key: path) - } -} diff --git a/Generate/ddmock.py b/Generate/ddmock.py new file mode 100755 index 0000000..c568aa2 --- /dev/null +++ b/Generate/ddmock.py @@ -0,0 +1,306 @@ +import os +import plistlib +import pathlib +import logging +import json +import argparse +import copy + + +# 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 + # todo: define subdir - it is the subdirectory configured (?) + for current, dirs, files in os.walk(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 + + # 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 + key = current.replace(mockfiles_path, "") + + # strip the leading slash if present + 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 key in endpoint_map: + files = endpoint_map[key] + files.append(file) + else: + endpoint_map[key] = [file] + + return (endpoint_map, header_map) + + +# def generate_header_map(): + + # 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: + res = json.load(file) + return res + + +# 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 + new_item['Title'] = 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__)) + path = pathlib.Path(path) + path = path.parent.joinpath(resources_path).absolute() + 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 + new_endpoint = copy.deepcopy(endpoint) + + logging.info(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, may not be string + 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): + 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}") + + # first create the map + # this is where the directory traversal happens + print("Creating map of endpoint paths and mock files...") + (endpoint_map, header_map) = generate_map(mockfiles_path) + + # todo: lazy evaluation in logging + logging.info(f" map: {endpoint_map}") + + # start creating settings bundle + print("Creating Settings.bundle...") + + # Settings.bundle is really just a directory + # first create directory if it doesn't exist + 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")) + + # 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 ... + canonical_key = get_canonical_key(endpoint_path) + + # add endpoint to root plist + 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, 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 + canonical_key + ".plist", "wb") as fout: + plistlib.dump(new_endpoint, fout, fmt=plistlib.FMT_XML) + + # create general plist from json template + print("Load general.plist template...") + general = path.joinpath("general.json") + general = load_json_resource(general) + + # write general plist + print("Writing general.plist...") + 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(output_path + "Root.plist", "wb") as output: + plistlib.dump(root, output, fmt=plistlib.FMT_XML) + + # finished + print("Done!") + + +if __name__ == "__main__": + + # 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 + main(args.mockfiles_path, args.output_path) diff --git a/Generate/plist.py b/Generate/plist.py new file mode 100644 index 0000000..1252e6c --- /dev/null +++ b/Generate/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/Generate/swagger_to_plist.py b/Generate/swagger_to_plist.py new file mode 100644 index 0000000..9c48cf0 --- /dev/null +++ b/Generate/swagger_to_plist.py @@ -0,0 +1,4 @@ +# convert swagger api spec into ddmock plist + +def swagger_to_plist(): + print("swag") diff --git a/DDMockiOS/Info.plist b/Info.plist similarity index 100% rename from DDMockiOS/Info.plist rename to Info.plist 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 diff --git a/Resources/general.json b/Resources/general.json new file mode 100644 index 0000000..2b43178 --- /dev/null +++ b/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/Resources/general.plist b/Resources/general.plist new file mode 100644 index 0000000..c6a7323 --- /dev/null +++ b/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) + + + + + 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/body.json b/Resources/mockfiles/example/get/body.json new file mode 100644 index 0000000..9feac80 --- /dev/null +++ b/Resources/mockfiles/example/get/body.json @@ -0,0 +1,3 @@ +{ + "message": "good job" +} diff --git a/Resources/root.json b/Resources/root.json new file mode 100644 index 0000000..1d0016f --- /dev/null +++ b/Resources/root.json @@ -0,0 +1,21 @@ +{ + "PreferenceSpecifiers": [ + { + "DefaultValue": true, + "Key": "use_real_apis", + "Title": "Use real APIs", + "Type": "PSToggleSwitchSpecifier" + }, + { + "File": "general", + "Title": "General", + "Type": "PSChildPaneSpecifier" + }, + { + "Title": "MOCK", + "Type": "PSGroupSpecifier", + "FooterText": "Note that strict mode being set at build time will override the 'Use real APIs' setting." + } + ], + "StringsTable": "Root" +} \ No newline at end of file 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" +} diff --git a/DDMockiOS/DDMockiOS.h b/Sources/DDMockiOS.h similarity index 89% rename from DDMockiOS/DDMockiOS.h rename to Sources/DDMockiOS.h index 69ce424..23a7855 100644 --- a/DDMockiOS/DDMockiOS.h +++ b/Sources/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/Sources/Extension/String+Regex.swift b/Sources/Extension/String+Regex.swift new file mode 100644 index 0000000..aae3e2e --- /dev/null +++ b/Sources/Extension/String+Regex.swift @@ -0,0 +1,42 @@ + +// 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, + options: .regularExpression, + range: nil, + locale: nil) + return matches != nil + } + + /** + Replace regex matches + replaceWith is the template (withTemplate) + */ + 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/Sources/Helper/ResponseHelper.swift b/Sources/Helper/ResponseHelper.swift new file mode 100644 index 0000000..8b648f6 --- /dev/null +++ b/Sources/Helper/ResponseHelper.swift @@ -0,0 +1,107 @@ +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' +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 + } + + // 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, + headers: [String: String]) -> HTTPURLResponse? { + + return HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: "HTTP/1.1", + 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() + +// todo: file should just be a string of the directory (?) + let path = Bundle.main.resourcePath! + Constants.mockDirectory + + 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) + } +} diff --git a/Sources/Helper/UserDefaultsHelper.swift b/Sources/Helper/UserDefaultsHelper.swift new file mode 100644 index 0000000..f61316b --- /dev/null +++ b/Sources/Helper/UserDefaultsHelper.swift @@ -0,0 +1,48 @@ +import Foundation + +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 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) + } + + static func getObject(key: String, item: SettingsKey) -> T? { + 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 . to be consistent with other keys +private func getSettingsBundleKey(key: String) -> String { + return key.replacingOccurrences(of: "/", with: ".") +} diff --git a/Sources/Mock/MockEntry.swift b/Sources/Mock/MockEntry.swift new file mode 100644 index 0000000..cc42c01 --- /dev/null +++ b/Sources/Mock/MockEntry.swift @@ -0,0 +1,89 @@ +import Foundation + +/** + mock entry struct + */ +struct MockEntry: Codable { + // 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 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] + } + + // get status code for an entry + func getStatusCode() -> Int { + return UserDefaultsHelper.getObject( + key: path, + item: .statusCode) ?? MockEntry.defaultStatusCode + } + + // get use real api + func useRealAPI() -> Bool { + return Self.getGlobalUseRealAPIs() + || getEndpointUseRealAPI(key: path) + } + + // get response time + func getResponseTime() -> Int { + 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 { + 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/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/Public/DDMockURLProtocolClass.swift b/Sources/Public/DDMockURLProtocolClass.swift new file mode 100644 index 0000000..784fab0 --- /dev/null +++ b/Sources/Public/DDMockURLProtocolClass.swift @@ -0,0 +1,129 @@ +import Foundation + +/** + Implementation of NSURLProtocol used to intercept requests. + Needs to be inserted into the list protocal classes ... + + */ +public class DDMockURLProtocolClass: URLProtocol { + + /** + 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, + at: 0) + return protocolClasses + } + + // 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 } + + guard + let req = task.currentRequest, + let path = req.url?.path, + let method = req.httpMethod else { + + 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.hasEntry(path: path, method: method) + } + + /** + 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. + */ + public override class func canonicalRequest(for request: URLRequest) -> URLRequest { + return request + } + + // todo: move logic to correct lifecycle point + /** + this is where everything happens + */ + public override func startLoading() { + + // fetch item + guard + let path = request.url?.path, + let method = request.httpMethod, + let url = request.url else { + + return + } + + // note: remove singleton could just mean restrict its usage to + // within the public interface boundary or make it more explicit + + // todo: remove singleton + guard let entry = DDMock.shared.getEntry( + path: path, + method: method) else { + + return + } + + // get response data + // todo: check in what case could this be nil + let data: Data? = ResponseHelper.getData(entry) + + // header dictionary + 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() + + // create response + guard let response = ResponseHelper.createMockResponse( + url: url, + statusCode: statusCode, + headers: headers) else { + + return + } + + // simulate response time + let time = TimeInterval(Float(entry.getResponseTime()) / 1000.0) + + // just use regular timer to async return the response + // todo: this isn't working correctly + 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) + }) + } + + /// Required override of abstract prototype, does nothing. + public override func stopLoading() { + // nothing actually loading + } +} diff --git a/Sources/Public/DDMockiOS.swift b/Sources/Public/DDMockiOS.swift new file mode 100644 index 0000000..1dcf1d1 --- /dev/null +++ b/Sources/Public/DDMockiOS.swift @@ -0,0 +1,87 @@ +import Foundation + +/** + This is the main DDMock entry point. + + */ +public final class DDMock { + + /// enforces mocks only and no API fall-through + internal var strict: Bool = false + + // todo: this should be thread safe + // and have a max size + /// chronological order of paths + private(set) var matchedPaths: [String] = [] + + /// needed for singleton + private init() {} + + /** + 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 + public static let shared = DDMock() + + /// repository for storing mocks + private var repository: MockRepository! + + /** + 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: more consistent configuration + self.strict = strict + + // todo: resource path + let path = Bundle.main.resourcePath! + Constants.mockDirectory + + // load the files in the mock directory + repository = MockRepository(path: path, fm: FileManager.default) + } + + /** + reset the history + */ + public func clearHistory() { + matchedPaths.removeAll() + } + + /** + Check if an entry exists for a given path + */ + 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 + let entry = repository.getEntry( + path: path, + method: method, + strict: strict, + onMissing: onMissingMock) else { + + return nil + } + + // add to history + matchedPaths.append(path) + + // return the entry + return entry + } +} 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!"