Skip to content

Commit

Permalink
Merge pull request #22 from RedMadRobot/documentation_api
Browse files Browse the repository at this point in the history
Documentation API
  • Loading branch information
Alexander-Ignition authored Jun 23, 2020
2 parents 312bd24 + b72d969 commit ed88b43
Show file tree
Hide file tree
Showing 14 changed files with 171 additions and 59 deletions.
99 changes: 99 additions & 0 deletions Documentation/API.md
Original file line number Diff line number Diff line change
@@ -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
43 changes: 28 additions & 15 deletions Sources/CatbirdAPI/CatbirdAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -60,38 +63,48 @@ 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)
}
}

public func encode(to encoder: Encoder) throws {
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
}
}
4 changes: 2 additions & 2 deletions Sources/CatbirdAPI/RequestPattern.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down
4 changes: 2 additions & 2 deletions Sources/CatbirdApp/Models/Pattern+Match.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import struct CatbirdAPI.Pattern
import struct CatbirdAPI.PatternMatch

extension Pattern {
extension PatternMatch {

func match(_ string: String) -> Bool {
let pattern = value
Expand Down
2 changes: 1 addition & 1 deletion Sources/CatbirdApp/Stores/FileResponseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class FileResponseStore: ResponseStore {

func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture<Response> {
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)
Expand Down
4 changes: 2 additions & 2 deletions Sources/CatbirdApp/Stores/InMemoryResponseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,15 @@ final class InMemoryResponseStore: ResponseStore {
func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture<Response> {
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
} else {
_items.append(item)
}
return .created
case .update(let pattern, .none):
case .remove(let pattern):
_items.removeAll(where: { $0.pattern == pattern })
return .noContent
case .removeAll:
Expand Down
4 changes: 2 additions & 2 deletions Sources/CatbirdApp/Stores/LoggedResponseStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ final class LoggedResponseStore: ResponseStore {

func perform(_ action: CatbirdAction, for request: Request) -> EventLoopFuture<Response> {
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")
Expand Down
2 changes: 1 addition & 1 deletion Sources/CatbirdApp/View Model/HeaderItemViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ struct HeaderItemViewModel: Encodable, Comparable {
let key: String
let value: String

init(_ item: Dictionary<String, Pattern>.Element) {
init(_ item: Dictionary<String, PatternMatch>.Element) {
key = item.key
value = item.value.value
}
Expand Down
2 changes: 1 addition & 1 deletion Tests/CatbirdAPITests/CatbirdActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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"}"#
Expand All @@ -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)
}
}
2 changes: 1 addition & 1 deletion Tests/CatbirdAppTests/App/AppTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit ed88b43

Please sign in to comment.