Skip to content

Declaring Web Services

Andrew Wagner edited this page Jul 23, 2019 · 14 revisions

You will most likely only have to declare one WebService per project unless your project communicates with multiple services.

Summary

This is the entire WebService protocol.

public protocol WebService {
    associatedtype BasicResponse: Decodable
    associatedtype ErrorResponse: AnyErrorResponse

    // MARK: Core

    static var shared: Self {get}
    var baseURL: URL {get}
    var sessionOverride: Session? {get} // Optional

    // MARK: Authorization

    var authorization: Authorization {get}

    // MARK: Configuration

    func configure<E: Endpoint>(_ request: inout URLRequest, for endpoint: E) throws // Optional
    func configure<E: Endpoint>(_ encoder: inout JSONEncoder, for endpoint: E) throws // Optional
    func configure<E: Endpoint>(_ decoder: inout JSONDecoder, for endpoint: E) throws // Optional
    func configure<E: Endpoint>(_ encoder: inout XMLEncoder, for endpoint: E) throws // Optional
    func configure<E: Endpoint>(_ decoder: inout XMLDecoder, for endpoint: E) throws // Optional

    // MARK: Validation

    func validate<E: Endpoint>(_ response: URLResponse, for endpoint: E) throws // Optional
    func validate<E: Endpoint>(_ response: BasicResponse, for endpoint: E) throws // Optional

    // MARK: Error Handling

    func handle<E: Endpoint>(_ error: ErrorKind, response: URLResponse, from endpoint: E) -> ErrorHandling // Optional
}

Let's break down our options for implementing each piece.

BasicResponse

This is perhaps the most complex part to customize. This is available if all successful responses from the service include the same field(s). For example, maybe every response includes a "status" property.

If this is not the case, you can simply set it to NoBasicResponse:

struct MyService: WebService {
    typealias BasicResponse = NoBasicResponse

    // ...
}

If instead, there are universal properties, you can define a type for it:

struct MyService: WebService {
    struct BasicResponse {
        let status: String
    }

    // ...
}

Here, every response is required to have a "status" property to be considered successful.

Note: You can also perform extra validation on these basic responses

ErrorResponse

This allows you to define what error responses look like. If there is a parsing or validation error, the request will attempt to parse this type from the response to allow the service to provide a better error message.

If the service doesn't have a standard error format, you can just set it to NoErrorResponse.

struct MyService: WebService {
    typealias ErrorResponse = NoErrorResponse

    // ...
}

However, if there is a standard error format, you can declare that type:

struct MyService: WebService {
    struct ErrorResponse: AnyErrorResponse {
        let message: String
    }

    // ...
}

The ErrorResponse must implement the AnyErrorResponse protocol which only requires a message getter.

In the example above, it is expected that errors returned from the server include a message property. However, if they don't, the request will fallback to its default error.

Shared

This property simply defines an instance that should be used by default for all requests. Each request can specify a different instance, but this one will be used by default. Usually, this will just be a simple initialization of the type.

struct MyService: WebService {
    let shared = MyService()

    // ...
}

Base URL

This is probably the most critical piece. The final URL for a request will be formed by adding that endpoint's path to this base URL.

struct MyService: WebService {
    let baseURL = URL(string: "https://example.com/api/v1")!

    // ...
}

Notice that the base URL can still include a base path.

Authorization

This defines the authorization to include with each request based on the auth requirement of its endpoint

This is expected to change as the client is authenticated and de-authenticated.

You will probably want to declare it as a variable defaulting to none.

var authorization = Authorization.none

Then, once you have authenticated, you would set it.

MyService.shared.authorization = .basic(username: "user", password: "secret")

There are multiple types of authorization:

  • Basic: Username and password
  • Bearer: Base 64 token
  • Custom: Custom header key and value

SessionOverride

The session override lets you specify a URLSession to use. If you don't specify one or return nil, URLSession.shared will be used.

Configuration

There are 3 optional ways you can configure a request as they are being created.

URLRequest

If want to modify a request before it goes, out you can implement this method.

struct MyService: WebService {
    func configure<E: Endpoint>(_ request: inout URLRequest, for endpoint: E) throws {
        request.addValue("SOME VALUE", forHTTPHeaderField: "KEY")
    }

    // ...
}

Encoders and Decoders

With these methods, you can configure the encoders and decoders for the input formats. Most notably, you might want to customize the date formats:

struct MyService: WebService {
    func configure<E: Endpoint>(_ encoder: inout JSONEncoder, for endpoint: E) throws {
        encoder.dateEncodingStrategy = .secondsSince1970
    }

    func configure<E: Endpoint>(_ decoder: inout JSONDecoder, for endpoint: E) throws {
        decoder.dateDecodingStrategy = .secondsSince1970
    }

    func configure<E: Endpoint>(_ encoder: inout XMLEncoder, for endpoint: E) throws {
        encoder.dateEncodingStrategy = .secondsSince1970
    }

    func configure<E: Endpoint>(_ decoder: inout XMLDecoder, for endpoint: E) throws {
        decoder.dateDecodingStrategy = .secondsSince1970
    }

    // ...
}

However, just like with the URLRequest configuration, these methods are optional.

Validation

The final available customization is validation. You can validate two different parts of a response. Validation methods should throw an error if there is a problem, otherwise, they should simply return without incident.

URLResponse

This is your basic response from the underlying Foundation networking. For example, You may want to validate that the HTTPS response code is within the 200 range.

struct MyService: WebService {
    func validate<E>(_ response: URLResponse, for endpoint: E) throws where E : Endpoint {
        let statusCode = (response as? HTTPURLResponse)?.statusCode ?? -1
        guard statusCode != 201 else {
            throw RequestError.custom("Shouldn't have created anything")
        }
    }

    // ...
}

However, you are also free to not implement this.

BasicResponse

This method is only called if you supply a custom basic response (as opposed to NoBasicResponse).

It is called after successfully parsing the BasicResponse from the response body. You can do whatever validation you want, for example, maybe you want to make sure the status is correct.

struct MyService: WebService {
    func validate<E>(_ response: BasicResponse, for endpoint: E) throws where E : Endpoint {}
        guard response.status == "Success" else {
            throw RequestError.custom("Bad status: \(response.status)")
        }
    }

    // ...
}

Again, this method is optional.

Error Handling

If an error occurs, you will have an opportunity to handle it automatically and transparently to the requester. To do that, you must implement the handle method.

func handle<E: Endpoint>(_ error: ErrorKind, response: URLResponse, from endpoint: E) -> ErrorHandling {
    switch error {
    case .plain:
        return .redirect(to: URL(string: "https://example.com/redirected")!)
    default:
        return .none
    }
}

The method gets passed a few things to help you determine if/how to handle the error. If you don't want to handle the error, simply return .none.

Your only other option currently is to return a redirect command with a custom URL. If you return this, the same exact request will be repeated to the new URL and the result will go through the entire pipeline again. All of this will be entirely transparent to the requesting code so that it appears as if only one request was made.

You can’t perform that action at this time.