Skip to content

Commit

Permalink
Allow nil returns for optional return type (#50)
Browse files Browse the repository at this point in the history
Added an additional decoding method for endpoints with optional return types.
An endpoint with an optional return type will return nil in case the response body is nil or empty.
  • Loading branch information
lutes1 authored Mar 23, 2024
1 parent 86f4e43 commit 598ae3c
Show file tree
Hide file tree
Showing 3 changed files with 219 additions and 7 deletions.
8 changes: 8 additions & 0 deletions PapyrusCore/Sources/Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ extension Response {
return body
}

public func decode<D: Decodable>(_ type: D?.Type = D?.self, using decoder: ResponseDecoder) throws -> D? {
guard let body, !body.isEmpty else {
return nil
}

return try decoder.decode(type, from: body)
}

public func decode<D: Decodable>(_ type: D.Type = D.self, using decoder: ResponseDecoder) throws -> D {
guard let body else {
throw makePapyrusError(with: "Unable to decode `\(Self.self)` from a `Response`; body was nil.")
Expand Down
144 changes: 143 additions & 1 deletion PapyrusCore/Tests/APITests.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,150 @@
import XCTest
import Papyrus
@testable import PapyrusCore

final class APITests: XCTestCase {
func testAPIGeneration() {
func testApiEndpointReturnsNilForOptionalReturnType_forNilBody() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .nil)))

// Act
let person = try await sut.getOptional()

// Assert
XCTAssertNil(person)
}

func testApiEndpointThrowsForNonOptionalReturnType_forNilBody() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .nil)))

// Act
let expectation = expectation(description: "The endpoint with the non-optional return type should throw an error for an invalid body.")
do {
let _ = try await sut.get()
} catch {
expectation.fulfill()
}

// Assert
await fulfillment(of: [expectation], timeout: 1)
}

func testApiEndpointReturnsNilForOptionalReturnType_forEmptyBody() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .empty)))

// Act
let person = try await sut.getOptional()

// Assert
XCTAssertNil(person)
}

func testApiEndpointThrowsForNonOptionalReturnType_forEmptyBody() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .empty)))

// Act
let expectation = expectation(description: "The endpoint with the non-optional return type should throw an error for an invalid body.")
do {
let _ = try await sut.get()
} catch {
expectation.fulfill()
}

// Assert
await fulfillment(of: [expectation], timeout: 1)
}

func testApiEndpointReturnsValidObjectForOptionalReturnType() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .person)))

// Act
let person = try await sut.getOptional()

// Assert
XCTAssertNotNil(person)
XCTAssertEqual(person?.name, "Petru")
}

func testApiEndpointReturnsValidObjectForNonOptionalReturnType() async throws {
// Arrange
let sut = _PeopleAPI(provider: .init(baseURL: "", http: _HTTPServiceMock(responseType: .person)))

// Act
let person = try await sut.get()

// Assert
XCTAssertNotNil(person)
XCTAssertEqual(person.name, "Petru")
}
}

@API()
fileprivate protocol _People {

@GET("")
func getOptional() async throws -> _Person?

@GET("")
func get() async throws -> _Person
}

fileprivate struct _Person: Decodable {
let name: String
}

fileprivate class _HTTPServiceMock: HTTPService {

enum ResponseType {
case `nil`
case empty
case person

var value: String? {
switch self {
case .nil:
nil
case .empty:
""
case .person:
"{\"name\": \"Petru\"}"
}
}
}

private let _responseType: ResponseType

init(responseType: ResponseType) {
_responseType = responseType
}

func build(method: String, url: URL, headers: [String : String], body: Data?) -> Request {
_Request(method: "", headers: [:])
}

func request(_ req: PapyrusCore.Request) async -> PapyrusCore.Response {
_Response(body: _responseType.value?.data(using: .utf8), statusCode: 200)
}

func request(_ req: PapyrusCore.Request, completionHandler: @escaping (PapyrusCore.Response) -> Void) {
completionHandler(_Response(body: "".data(using: .utf8)))
}
}

fileprivate struct _Request: Request {
var url: URL?
var method: String
var headers: [String : String]
var body: Data?
}

fileprivate struct _Response: Response {
var request: PapyrusCore.Request?
var body: Data?
var headers: [String : String]?
var statusCode: Int?
var error: Error?
}
74 changes: 68 additions & 6 deletions PapyrusCore/Tests/ResponseDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,77 @@ final class ResponseDecoderTests: XCTestCase {
func testWithKeyMappingDoesntMutate() throws {
let decoder = JSONDecoder()
let snakeDecoder = decoder.with(keyMapping: .snakeCase)

switch decoder.keyDecodingStrategy {
case .useDefaultKeys: break
default: XCTFail("Should be default keys")
case .useDefaultKeys: break
default: XCTFail("Should be default keys")
}

switch snakeDecoder.keyDecodingStrategy {
case .convertFromSnakeCase: break
default: XCTFail("Should be snake_case keys")
case .convertFromSnakeCase: break
default: XCTFail("Should be snake_case keys")
}
}

func testResponseWithOptionalTypeAndNilBody() throws {
// Arrange
let response = _Response()
response.body = nil

// Act
let decoded = try response.decode(_Person?.self, using: JSONDecoder())

//Assert
XCTAssertNil(decoded)
}

func testResponseWithOptionalTypeAndEmptyBody() throws {
// Arrange
let response = _Response()
response.body = "".data(using: .utf8)

// Act
let decoded = try response.decode(_Person?.self, using: JSONDecoder())

//Assert
XCTAssertNil(decoded)
}

func testResponseWithOptionalTypeAndNonNilBody() throws {
// Arrange
let response = _Response()
response.body = "{ \"name\": \"Petru\" }".data(using: .utf8)

// Act
let decoded = try response.decode(_Person?.self, using: JSONDecoder())

//Assert
XCTAssertNotNil(decoded)
XCTAssertEqual(decoded?.name, "Petru")
}

func testResponseWithNonOptionalTypeAndNonNilBody() throws {
// Arrange
let response = _Response()
response.body = "{ \"name\": \"Petru\" }".data(using: .utf8)

// Act
let decoded = try response.decode(_Person.self, using: JSONDecoder())

//Assert
XCTAssertNotNil(decoded)
XCTAssertEqual(decoded.name, "Petru")
}
}

fileprivate struct _Person: Decodable {
let name: String
}

fileprivate class _Response : Response {
var request: PapyrusCore.Request?
var body: Data?
var headers: [String : String]?
var statusCode: Int?
var error: Error?
}

0 comments on commit 598ae3c

Please sign in to comment.