diff --git a/Documentation/API.md b/Documentation/API.md new file mode 100644 index 0000000..c6296ff --- /dev/null +++ b/Documentation/API.md @@ -0,0 +1,99 @@ +# Catbird API + +- [Actions](#Actions) + - [Update](#Update) + - [Remove All](#remove-all) +- [Objects](#Objects) + - [CatbirdAction](#CatbirdAction) + - [RequestPattern](#RequestPattern) + - [ResponseMock](#ResponseMock) + - [PatternMatch](#PatternMatch) + +## Actions + +### Update + +Add or update `ResponseMock` for `RequestPattern` + +```json +POST /catbird/api/mocks +{ + "type": "update", + "pattern": { + "method": "POST", + "url": { + "kind": "equal", + "value": "/api/login" + } + }, + "response": { + "status": 200 + } +} +``` + +`RequestPattern` acts as a unique identifier or key for `ResponseMock`, so that it can be used to remove mock + +Remove `ResponseMock` for `RequestPattern` + +```json +POST /catbird/api/mocks +{ + "type": "remove", + "pattern": { + "method": "POST", + "url": { + "kind": "equal", + "value": "/api/login" + } + } +} +``` + +### Remove All + +```json +POST /catbird/api/mocks +{ + "type": "removeAll" +} +``` + +## Objects + +### CatbirdAction + +Name | Required | Type +---------|----------|------- +type | true | String enum (update, remove, removeAll) +pattern | false | RequestPattern +response | false | ResponseMock + +### RequestPattern + +`RequestPattern` is a description of the requests to be intercepted and to which the mock should be returned. + +Name | Required | Type +--------|----------|------- +method | true | String +url | true | PatternMatch +headers | true | [String: PatternMatch] + +### ResponseMock + +`ResponseMock` is a description of the http response. + +Name | Required | Type +--------|----------|------- +status | true | Int +headers | true | [String: String] +body | false | Base 64 encoded data +limit | false | Int +delay | false | Int + +### PatternMatch + +Name | Required | Type +------|----------|------- +kind | true | String enum (equal, wildcard, regexp) +value | true | String diff --git a/Sources/CatbirdAPI/CatbirdAction.swift b/Sources/CatbirdAPI/CatbirdAction.swift index cf2ce0c..45e5298 100644 --- a/Sources/CatbirdAPI/CatbirdAction.swift +++ b/Sources/CatbirdAPI/CatbirdAction.swift @@ -2,8 +2,11 @@ import Foundation /// Catbird API action. public enum CatbirdAction: Equatable { - /// Add, update or remove `ResponseMock` for `RequestPattern`. - case update(RequestPattern, ResponseMock?) + /// Add, or insert `ResponseMock` for `RequestPattern`. + case update(RequestPattern, ResponseMock) + + /// Remove `ResponseMock` for `RequestPattern`. + case remove(RequestPattern) /// Remove all mocks. case removeAll @@ -35,7 +38,7 @@ extension CatbirdAction { /// - Parameter mock: Mock representation. /// - Returns: A new `CatbirdAction`. public static func remove(_ mock: CatbirdMockConvertible) -> CatbirdAction { - CatbirdAction.update(mock.pattern, nil) + CatbirdAction.remove(mock.pattern) } } @@ -60,27 +63,33 @@ extension CatbirdAction { // MARK: - Codable +enum CatbirdActionType: String, Codable { + case update + case remove + case removeAll +} + extension CatbirdAction: Codable { enum CondingKeys: String, CodingKey { + case type case pattern case response } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CondingKeys.self) - let pattern = try container.decodeIfPresent(RequestPattern.self, forKey: .pattern) - let response = try container.decodeIfPresent(ResponseMock.self, forKey: .response) + let type = try container.decode(CatbirdActionType.self, forKey: .type) - switch (pattern, response) { - case (let pattern?, let response): + switch type { + case .update: + let pattern = try container.decode(RequestPattern.self, forKey: .pattern) + let response = try container.decode(ResponseMock.self, forKey: .response) self = .update(pattern, response) - case (.none, .none): + case .remove: + let pattern = try container.decode(RequestPattern.self, forKey: .pattern) + self = .remove(pattern) + case .removeAll: self = .removeAll - case (.none, .some): - let context = DecodingError.Context( - codingPath: [CondingKeys.pattern], - debugDescription: "Not found `RequestPattern` for `ResponseMock`") - throw DecodingError.valueNotFound(RequestPattern.self, context) } } @@ -88,10 +97,14 @@ extension CatbirdAction: Codable { var container = encoder.container(keyedBy: CondingKeys.self) switch self { case .update(let pattern, let response): + try container.encode(CatbirdActionType.update, forKey: .type) + try container.encode(pattern, forKey: .pattern) + try container.encode(response, forKey: .response) + case .remove(let pattern): + try container.encode(CatbirdActionType.remove, forKey: .type) try container.encode(pattern, forKey: .pattern) - try container.encodeIfPresent(response, forKey: .response) case .removeAll: - break + try container.encode(CatbirdActionType.removeAll, forKey: .type) } } } diff --git a/Sources/CatbirdAPI/Pattern.swift b/Sources/CatbirdAPI/PatternMatch.swift similarity index 61% rename from Sources/CatbirdAPI/Pattern.swift rename to Sources/CatbirdAPI/PatternMatch.swift index b90ecad..21cfc19 100644 --- a/Sources/CatbirdAPI/Pattern.swift +++ b/Sources/CatbirdAPI/PatternMatch.swift @@ -1,7 +1,7 @@ import Foundation /// The kind of pattern for matching request fields such as url and headers -public struct Pattern: Codable, Hashable { +public struct PatternMatch: Codable, Hashable { // MARK: - Public types @@ -26,39 +26,39 @@ public struct Pattern: Codable, Hashable { self.value = value } - public static func equal(_ value: String) -> Pattern { - return Pattern(kind: .equal, value: value) + public static func equal(_ value: String) -> PatternMatch { + return PatternMatch(kind: .equal, value: value) } - public static func wildcard(_ value: String) -> Pattern { - return Pattern(kind: .wildcard, value: value) + public static func wildcard(_ value: String) -> PatternMatch { + return PatternMatch(kind: .wildcard, value: value) } - public static func regexp(_ value: String) -> Pattern { - return Pattern(kind: .regexp, value: value) + public static func regexp(_ value: String) -> PatternMatch { + return PatternMatch(kind: .regexp, value: value) } } /// Protocol for converting common types to Pattern public protocol PatternRepresentable { - var pattern: Pattern { get } + var pattern: PatternMatch { get } } -extension Pattern: PatternRepresentable { - public var pattern: Pattern { +extension PatternMatch: PatternRepresentable { + public var pattern: PatternMatch { return self } } extension String: PatternRepresentable { - public var pattern: Pattern { + public var pattern: PatternMatch { return .equal(self) } } extension URL: PatternRepresentable { - public var pattern: Pattern { + public var pattern: PatternMatch { return .equal(self.absoluteString) } } diff --git a/Sources/CatbirdAPI/RequestPattern.swift b/Sources/CatbirdAPI/RequestPattern.swift index c97ce41..7f98030 100644 --- a/Sources/CatbirdAPI/RequestPattern.swift +++ b/Sources/CatbirdAPI/RequestPattern.swift @@ -9,10 +9,10 @@ public struct RequestPattern: Codable, Hashable { public var method: HTTPMethod /// Request URL. - public var url: Pattern + public var url: PatternMatch /// Request required headers. - public var headers: [String: Pattern] + public var headers: [String: PatternMatch] /// A new request pattern. /// diff --git a/Sources/CatbirdApp/Models/Pattern+Match.swift b/Sources/CatbirdApp/Models/Pattern+Match.swift index 5297349..bde3c23 100644 --- a/Sources/CatbirdApp/Models/Pattern+Match.swift +++ b/Sources/CatbirdApp/Models/Pattern+Match.swift @@ -1,6 +1,6 @@ -import struct CatbirdAPI.Pattern +import struct CatbirdAPI.PatternMatch -extension Pattern { +extension PatternMatch { func match(_ string: String) -> Bool { let pattern = value diff --git a/Sources/CatbirdApp/Stores/FileResponseStore.swift b/Sources/CatbirdApp/Stores/FileResponseStore.swift index b7176b0..f6cbdc8 100644 --- a/Sources/CatbirdApp/Stores/FileResponseStore.swift +++ b/Sources/CatbirdApp/Stores/FileResponseStore.swift @@ -27,7 +27,7 @@ final class FileResponseStore: ResponseStore { func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture { let eventLoop = request.eventLoop - guard case .update(_, let response?) = action, let body = response.body, !body.isEmpty else { + guard case .update(_, let response) = action, let body = response.body, !body.isEmpty else { return eventLoop.makeSucceededFuture(Response(status: .badRequest)) } let path = filePath(for: request) diff --git a/Sources/CatbirdApp/Stores/InMemoryResponseStore.swift b/Sources/CatbirdApp/Stores/InMemoryResponseStore.swift index 5766d43..93127fb 100644 --- a/Sources/CatbirdApp/Stores/InMemoryResponseStore.swift +++ b/Sources/CatbirdApp/Stores/InMemoryResponseStore.swift @@ -36,7 +36,7 @@ final class InMemoryResponseStore: ResponseStore { func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture { let status = _queue.sync { () -> HTTPStatus in switch action { - case .update(let pattern, let mock?): + case .update(let pattern, let mock): let item = ResponseStoreItem(pattern: pattern, mock: mock) if let index = _items.firstIndex(where: { $0.pattern == pattern }) { _items[index] = item @@ -44,7 +44,7 @@ final class InMemoryResponseStore: ResponseStore { _items.append(item) } return .created - case .update(let pattern, .none): + case .remove(let pattern): _items.removeAll(where: { $0.pattern == pattern }) return .noContent case .removeAll: diff --git a/Sources/CatbirdApp/Stores/LoggedResponseStore.swift b/Sources/CatbirdApp/Stores/LoggedResponseStore.swift index 742fa50..e1392b2 100644 --- a/Sources/CatbirdApp/Stores/LoggedResponseStore.swift +++ b/Sources/CatbirdApp/Stores/LoggedResponseStore.swift @@ -21,9 +21,9 @@ final class LoggedResponseStore: ResponseStore { func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture { switch action { - case .update(let pattern, .some): + case .update(let pattern, _): logger.info("write at url: \(pattern.url.value)") - case .update(let pattern, .none): + case .remove(let pattern): logger.info("remove at url: \(pattern.url.value)") case .removeAll: logger.info("remove all responses") diff --git a/Sources/CatbirdApp/View Model/HeaderItemViewModel.swift b/Sources/CatbirdApp/View Model/HeaderItemViewModel.swift index b274959..dfaf18c 100644 --- a/Sources/CatbirdApp/View Model/HeaderItemViewModel.swift +++ b/Sources/CatbirdApp/View Model/HeaderItemViewModel.swift @@ -5,7 +5,7 @@ struct HeaderItemViewModel: Encodable, Comparable { let key: String let value: String - init(_ item: Dictionary.Element) { + init(_ item: Dictionary.Element) { key = item.key value = item.value.value } diff --git a/Tests/CatbirdAPITests/CatbirdActionTests.swift b/Tests/CatbirdAPITests/CatbirdActionTests.swift index 981e5e9..d0b80a1 100644 --- a/Tests/CatbirdAPITests/CatbirdActionTests.swift +++ b/Tests/CatbirdAPITests/CatbirdActionTests.swift @@ -28,7 +28,7 @@ final class CatbirdActionTests: XCTestCase { func testRemove() throws { // Given let pattern = RequestPattern(method: .GET, url: "/about") - let action = CatbirdAction.update(pattern, nil) + let action = CatbirdAction.remove(pattern) // When let request = try XCTUnwrap(try action.makeRequest(to: baseURL)) diff --git a/Tests/CatbirdAPITests/PatternTests.swift b/Tests/CatbirdAPITests/PatternMatchTests.swift similarity index 66% rename from Tests/CatbirdAPITests/PatternTests.swift rename to Tests/CatbirdAPITests/PatternMatchTests.swift index aaa4470..210f78a 100644 --- a/Tests/CatbirdAPITests/PatternTests.swift +++ b/Tests/CatbirdAPITests/PatternMatchTests.swift @@ -1,7 +1,7 @@ -import struct CatbirdAPI.Pattern +import struct CatbirdAPI.PatternMatch import XCTest -final class PatternTests: XCTestCase { +final class PatternMatchTests: XCTestCase { private enum JSONs: String { case equal = #"{"kind":"equal","value":"some"}"# @@ -12,41 +12,41 @@ final class PatternTests: XCTestCase { } func testEncodingEqual() throws { - let pattern = Pattern.equal("some") + let pattern = PatternMatch.equal("some") let data = try JSONEncoder().encode(pattern) XCTAssertEqual(String(data: data, encoding: .utf8), JSONs.equal.rawValue) } func testEncodingWildcard() throws { - let pattern = Pattern.wildcard("some*") + let pattern = PatternMatch.wildcard("some*") let data = try JSONEncoder().encode(pattern) XCTAssertEqual(String(data: data, encoding: .utf8), JSONs.wildcard.rawValue) } func testEncodingRegexp() throws { - let pattern = Pattern.regexp("^some$") + let pattern = PatternMatch.regexp("^some$") let data = try JSONEncoder().encode(pattern) XCTAssertEqual(String(data: data, encoding: .utf8), JSONs.regexp.rawValue) } func testDecodingEqual() throws { let data = JSONs.equal.data - let pattern = try JSONDecoder().decode(Pattern.self, from: data) - let reference = Pattern.equal("some") + let pattern = try JSONDecoder().decode(PatternMatch.self, from: data) + let reference = PatternMatch.equal("some") XCTAssertEqual(pattern, reference) } func testDecodingWildcard() throws { let data = JSONs.wildcard.data - let pattern = try JSONDecoder().decode(Pattern.self, from: data) - let reference = Pattern.wildcard("some*") + let pattern = try JSONDecoder().decode(PatternMatch.self, from: data) + let reference = PatternMatch.wildcard("some*") XCTAssertEqual(pattern, reference) } func testDecodingRegexp() throws { let data = JSONs.regexp.data - let pattern = try JSONDecoder().decode(Pattern.self, from: data) - let reference = Pattern.regexp("^some$") + let pattern = try JSONDecoder().decode(PatternMatch.self, from: data) + let reference = PatternMatch.regexp("^some$") XCTAssertEqual(pattern, reference) } } diff --git a/Tests/CatbirdAppTests/App/AppTests.swift b/Tests/CatbirdAppTests/App/AppTests.swift index 65f3144..0911392 100644 --- a/Tests/CatbirdAppTests/App/AppTests.swift +++ b/Tests/CatbirdAppTests/App/AppTests.swift @@ -109,7 +109,7 @@ final class AppTests: AppTestCase { try app.perform(.update(pattern, mock)) // When - try app.perform(.update(pattern, nil)) + try app.perform(.remove(pattern)) // Then try app.test(.POST, "api/users/1") { response in diff --git a/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift b/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift index 645dd38..9da9c75 100644 --- a/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift +++ b/Tests/CatbirdAppTests/Helpers/Application+CatbirdAction.swift @@ -26,9 +26,9 @@ extension Application { extension CatbirdAction { var expectedStatus: HTTPResponseStatus { switch self { - case .update(_, .some): + case .update: return .created - case .update, .removeAll: + case .remove, .removeAll: return .noContent } } diff --git a/Tests/CatbirdAppTests/Model/MatchPatternTests.swift b/Tests/CatbirdAppTests/Model/PatternMatchTests.swift similarity index 72% rename from Tests/CatbirdAppTests/Model/MatchPatternTests.swift rename to Tests/CatbirdAppTests/Model/PatternMatchTests.swift index fa69035..f5d9dd4 100644 --- a/Tests/CatbirdAppTests/Model/MatchPatternTests.swift +++ b/Tests/CatbirdAppTests/Model/PatternMatchTests.swift @@ -2,35 +2,35 @@ import CatbirdAPI import XCTest -final class MatchPatternTests: XCTestCase { +final class PatternMatchTests: XCTestCase { func testMatchEqual() { - let pattern1 = Pattern.equal("some string") + let pattern1 = PatternMatch.equal("some string") XCTAssertTrue(pattern1.match("some string")) - let pattern2 = Pattern.equal("some*string") + let pattern2 = PatternMatch.equal("some*string") XCTAssertTrue(pattern2.match("some*string")) - let pattern3 = Pattern.equal("^some.string$") + let pattern3 = PatternMatch.equal("^some.string$") XCTAssertTrue(pattern3.match("^some.string$")) } func testMatchWildcard() { - let pattern1 = Pattern.wildcard("some?string") + let pattern1 = PatternMatch.wildcard("some?string") XCTAssertTrue(pattern1.match("some string")) XCTAssertTrue(pattern1.match("some_string")) XCTAssertTrue(pattern1.match("some-string")) XCTAssertFalse(pattern1.match("somestring")) XCTAssertFalse(pattern1.match("something")) - let pattern2 = Pattern.wildcard("foo{bar,baz}") + let pattern2 = PatternMatch.wildcard("foo{bar,baz}") XCTAssertTrue(pattern2.match("foobar")) XCTAssertTrue(pattern2.match("foobaz")) XCTAssertFalse(pattern2.match("foobuz")) } func testMatchRegexp() { - let pattern1 = Pattern.regexp(#"^some[\w\d-_]{1}string"#) + let pattern1 = PatternMatch.regexp(#"^some[\w\d-_]{1}string"#) XCTAssertTrue(pattern1.match("some-string")) XCTAssertTrue(pattern1.match("some_string")) XCTAssertTrue(pattern1.match("some1string"))