Skip to content

Commit

Permalink
#29: Add tests for json decoder inheritance; update documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
tdeleon committed Jan 29, 2024
1 parent 32b9468 commit 42be750
Show file tree
Hide file tree
Showing 11 changed files with 249 additions and 46 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Relax supports the [Swift Package Manager](https://www.swift.org/package-manager
4. Click on **Add Package**

>Tip: `URLMock` is an additional framework provided to aid in testing by mocking responses to requests, and should be
added to your test targets only. For more information, see <doc:Relax#Testing> below.
added to your test targets only. For more information, see [Testing](#testing) below.

#### Import the framework

Expand Down Expand Up @@ -114,7 +114,8 @@ let request = Request(.post, url: URL(string: "https://example.com/users")!) {
}
```

See <doc:DefiningRequests> for more details.
See [Defining Requests](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingrequests) for more
details.

### Define a Complex API Structure

Expand Down Expand Up @@ -147,7 +148,8 @@ enum UserService: Service {
let users = try await UserService.Users.getAll.send()
```

See <doc:DefiningAPIStructure> for more details.
See [Defining an APIStructure](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingapistructure) for
more details.

## Testing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ returns the data decoded to a `Decodable` type.
```swift
let request = Request(.get, url: URL(string: "https://example.com")!)
request.send { (result: Result<User, RequestError> in
request.send { (result: Result<User, RequestError>) in
switch result {
case .success(let user):
print("User: \(user)")
case .failure(let error):
print("Request failed - \(error.localizedDescription)")
print("Request failed - \(error)")
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cancellable = request.send()
.sink(receiveCompletion: { completion in
switch completion {
case .failure(let error):
print("Request failed - \(error.localizedDescription)")
print("Request failed - \(error)")
case .finished:
break
}
Expand Down Expand Up @@ -109,7 +109,7 @@ cancellable = try request.send()
case .finished:
break
}
}, receiveValue: { response in
}, receiveValue: { (response: User) in
print("User: \(response.responseModel)")
})
```
Expand Down
41 changes: 41 additions & 0 deletions Sources/Relax/Request/Request+Send.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,52 @@ extension Request {
return task
}

/// Send a request with a completion handler, decoding the received data to a Decodable instance
/// - Parameters:
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - completion: A completion handler with a `Result` of the model type decoded from received data, or ``RequestError`` on failure.
///
///
/// Use this method when you want to decode data into a given model type, and do not need the full `HTTPURLResponse` from the server.
///
/// ```swift
/// let request = Request(.get, url: URL(string: "https://example.com")!)
/// request.send { (result: Result<User, RequestError>) in
/// switch result {
/// case .success(let user):
/// print("User: \(user)")
/// case .failure(let error):
/// print("Request failed - \(error)")
/// }
/// }
/// ```
///
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder? = nil,
session: URLSession? = nil,
completion: @escaping (_ result: Result<ResponseModel, RequestError>) -> Void
) {
send(
decoder: decoder,
session: session
) { (result: Result<Request.ResponseModel<ResponseModel>, RequestError>) in
switch result {
case .success(let success):
completion(.success(success.responseModel))
case .failure(let failure):
completion(.failure(failure))
}
}
}

/// Send a request with a completion handler, decoding the received data to a Decodable instance
/// - Parameters:
/// - decoder: When set, overrides the ``Request/decoder`` used to decode received data.
/// - session: When set, overrides the ``Request/session`` used to send the request.
/// - completion: A completion handler with the response from the server, including the decoded data as the Decodable type.
///
/// Use this method when decoding a model `Decodable` type and you also need the full `HTTPURLResponse` from the server.
public func send<ResponseModel: Decodable>(
decoder: JSONDecoder? = nil,
session: URLSession? = nil,
Expand Down
40 changes: 40 additions & 0 deletions Tests/RelaxTests/AsyncTests/AsyncRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,5 +57,45 @@ final class AsyncRequestTests: XCTestCase {
func testComplexRequest() async throws {
try await makeSuccess(request: service.ComplexRequests.complex)
}

func testOverrideSession() async throws {
let expectation = self.expectation(description: "Mock received")
let expectedSession = URLMock.session(.mock { _ in
expectation.fulfill()
})

let override = Request(.get, parent: InheritService.User.self, session: expectedSession)
try await override.send()

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

func testOverrideSessionOnSendAsync() async throws {
let expectation = self.expectation(description: "Mock received")
let expectedSession = URLMock.session(.mock { _ in
expectation.fulfill()
})
try await InheritService.User.get.send(session: expectedSession)

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

func testOverrideDecoderOnSend() async throws {
let model = InheritService.User.Response(date: .now)
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
let session = URLMock.session(.mock(model, encoder: encoder))

// send request - the defined JSONDecoder on InheritService has 8601 date decoding strategy
let _: InheritService.User.Response = try await InheritService.User.get.send(session: session)

do {
// send request, overriding the inherited decoder with default - should fail to parse
let _: InheritService.User.Response = try await InheritService.User.get.send(decoder: JSONDecoder(), session: session)
XCTFail("Should have failed")
} catch {
XCTAssertTrue(error is RequestError)
}
}
}
#endif
41 changes: 39 additions & 2 deletions Tests/RelaxTests/CombineTests/CombineCodableTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import URLMock
final class CombineCodableTests: XCTestCase {
typealias User = ExampleService.Users.User
var session: URLSession!
var cancellable: AnyCancellable?
var cancellables = Set<AnyCancellable>()

let service = ExampleService.Users.self

Expand All @@ -32,7 +32,7 @@ final class CombineCodableTests: XCTestCase {
URLMock.response = .mock(sampleModel)
let expectation = self.expectation(description: "Expect")

cancellable = service.getRequest
service.getRequest
.send(session: session)
.sink(receiveCompletion: { completion in
defer { expectation.fulfill() }
Expand All @@ -45,8 +45,45 @@ final class CombineCodableTests: XCTestCase {
}, receiveValue: { (response: [User]) in
XCTAssertEqual(response, sampleModel)
})
.store(in: &cancellables)

waitForExpectations(timeout: 1)
}

func testOverrideDecoderOnSend() throws {
let success = self.expectation(description: "Success")
let failure = self.expectation(description: "Failure")
let model = InheritService.User.Response(date: .now)

let session = URLMock.session(.mock(model, encoder: InheritService.iso8601Encoder))

InheritService.User.get.send(session: session)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
XCTFail("Should succeed")
}
}, receiveValue: { (response: InheritService.User.Response) in
success.fulfill()
})
.store(in: &cancellables)

InheritService.User.get.send(decoder: JSONDecoder(), session: session)
.sink(receiveCompletion: { completion in
switch completion {
case .finished:
break
case .failure:
failure.fulfill()
}
}, receiveValue: { (response: InheritService.User.Response) in
XCTFail()
})
.store(in: &cancellables)

waitForExpectations(timeout: 1)
}
}
#endif
29 changes: 29 additions & 0 deletions Tests/RelaxTests/CombineTests/CombineRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,35 @@ final class CombineRequestTests: XCTestCase {
makeSuccess(request: ExampleService.ComplexRequests.complex)
}

func testOverrideSession() throws {
let expectation = self.expectation(description: "Mock received")
let session = URLMock.session(.mock { _ in
expectation.fulfill()
})

let override = Request(.get, parent: InheritService.User.self, session: session)
cancellable = override.send()
.sink(receiveCompletion: { _ in
}, receiveValue: { _ in
})

waitForExpectations(timeout: 1)
}

func testOverrideSessionOnSend() throws {
let expectation = self.expectation(description: "Mock received")
let session = URLMock.session(.mock { _ in
expectation.fulfill()
})

cancellable = InheritService.User.get.send(session: session)
.sink(receiveCompletion: { _ in
}, receiveValue: { _ in
})

waitForExpectations(timeout: 1)
}

func testGetPerformance() throws {
measure {
makeSuccess(request: ExampleService.get)
Expand Down
53 changes: 53 additions & 0 deletions Tests/RelaxTests/CompletionTests/CompletionRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,59 @@ final class CompletionRequestTests: XCTestCase {
makeSuccess(request: ExampleService.ComplexRequests.complex)
}

func testOverrideDecoderOnSend() throws {
let success = self.expectation(description: "success")
let fail = self.expectation(description: "fail")
let model = InheritService.User.Response(date: .now)

let session = URLMock.session(.mock(model, encoder: InheritService.iso8601Encoder))

InheritService.User.get.send(session: session) { (result: Result<InheritService.User.Response, RequestError>) in
switch result {
case .success:
success.fulfill()
case .failure:
XCTFail("Failed")
}
}

InheritService.User.get.send(decoder: JSONDecoder(), session: session) { (result: Result<InheritService.User.Response, RequestError>) in
switch result {
case .success:
XCTFail("Should fail")
case .failure:
fail.fulfill()
}
}

waitForExpectations(timeout: 1)
}

func testOverrideSession() throws {
let expectation = self.expectation(description: "Mock received")
let expectedSession = URLMock.session(.mock { _ in
expectation.fulfill()
})

let override = Request(.get, parent: InheritService.User.self, session: expectedSession)
override.send(session: expectedSession) { _ in
}

waitForExpectations(timeout: 1)
}

func testOverrideSessionOnSend() throws {
let expectation = self.expectation(description: "Mock received")
let expectedSession = URLMock.session(.mock { _ in
expectation.fulfill()
})

InheritService.User.get.send(session: expectedSession) { _ in
}

waitForExpectations(timeout: 1)
}

func testGetPerformance() throws {
measure {
makeSuccess(request: ExampleService.get)
Expand Down
28 changes: 28 additions & 0 deletions Tests/RelaxTests/Helpers/MockServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,31 @@ struct BadURLService: Service {
}
}
}

enum InheritService: Service {
static let baseURL = URL(string: "https://example.com")!
static var configuration: Request.Configuration = Request.Configuration(allowsCellularAccess: false)
static let session: URLSession = URLSession(configuration: .ephemeral)
static let decoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()

static let iso8601Encoder: JSONEncoder = {
let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601
return encoder
}()

enum User: Endpoint {
static var path: String = "users"
typealias Parent = InheritService

static let get = Request(.get, parent: User.self)

struct Response: Codable, Hashable {
var date: Date
}
}
}
10 changes: 10 additions & 0 deletions Tests/RelaxTests/Request/RequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,14 @@ final class RequestTests: XCTestCase {
XCTAssertEqual(request._properties, properties)
}

func testHashable() {
let request1 = Request(.get, url: URL(string: "https://example.com/")!)
let request2 = request1
let request3 = Request(.delete, url: URL(string: "https://example.com/")!)
let request4 = Request(.get, url: URL(string: "https://example.org/")!)

XCTAssertEqual(request1.hashValue, request2.hashValue)
XCTAssertNotEqual(request1.hashValue, request3.hashValue)
XCTAssertNotEqual(request1.hashValue, request4.hashValue)
}
}
Loading

0 comments on commit 42be750

Please sign in to comment.