Resource

@objc(BOSResource)
public final class Resource : NSObject
extension Resource: ConfigurationPatternConvertible
extension Resource: TypedContentAccessors

An in-memory cache of a RESTful resource, plus information about the status of network requests related to it.

This class answers three basic questions about a resource:

  • What is the latest data for the resource this device has retrieved, if any?
  • Did the last attempt to load it result in an error?
  • Is there a request in progress?

…and allows multiple observer to register to be notified whenever the answers to any of these questions changes.

Essentials

  • The API to which this resource belongs. Provides configuration defaults and instance uniqueness.

    Declaration

    Swift

    @objc
    public let service: Service
  • url

    The canoncial URL of this resource.

    Declaration

    Swift

    @objc
    public let url: URL

Configuration

Resource state

  • The latest valid data we have for this resource. May come from a server response, a cache, or a local override.

    Note that this property represents the full state of the resource. It therefore only holds entities fetched with load() and loadIfNeeded(), not any of the various flavors of request(...).

    Note that latestData will be present as long as there has ever been a succesful request since the resource was created or wiped. If an error occurs, latestData will still hold the latest (now stale) valid data.

    Declaration

    Swift

    public private(set) var latestData: Entity<Any>? { get set }
  • Details if the last attempt to load this resource resulted in an error. Becomes nil as soon as a request is successful.

    Note that this only reports error from load() and loadIfNeeded(), not any of the various flavors of request(...).

    Declaration

    Swift

    public private(set) var latestError: RequestError? { get set }
  • The time of the most recent update to either latestData or latestError.

    Declaration

    Swift

    @objc
    public var timestamp: TimeInterval { get }

Request management

  • True if any load requests (i.e. from calls to load(...) and loadIfNeeded()) for this resource are in progress.

    Declaration

    Swift

    @objc
    public var isLoading: Bool { get }
  • True if any requests for this resource are in progress.

    Declaration

    Swift

    @objc
    public var isRequesting: Bool { get }
  • All load requests in progress, in the order they were initiated.

    Declaration

    Swift

    public private(set) var loadRequests: [Request] { get }
  • All requests in progress related to this resource, in the order they were initiated.

    Declaration

    Swift

    public private(set) var allRequests: [Request] { get }

Requests

  • Allows callers to arbitrarily alter the HTTP details of a request before it is sent. For example:

    resource.request(.post) {
      $0.httpBody = imageData
      $0.addValue("image/png", forHTTPHeaderField: "Content-Type")
    }
    

    Siesta provides helpers that make this custom RequestMutation unnecessary in many common cases. Configuration lets you set request headers, and helpers such as Resource.request(_:json:contentType:requestMutation:) will encode common request body types for you. Custom mutation is the “full control” option for cases when:

    1. you need to alter the request in ways Siesta doesn’t provide helpers for, or
    2. you want to alter one individual request instead of configuring all requests for a resource.

    The RequestMutation receives a URLRequest after Siesta has already applied all of its normal configuration. The URLRequest is mutable, and any changes it makes are the last stop before the request is sent to the network. What you return is what Siesta sends.

    Note

    Why is RequestMutation marked @escaping everywhere it’s used? Because Request.repeated() does not repeat the original request verbatim; instead, it recomputes the request headers using the latest configuration, then reapplies your RequestMutation.

    Declaration

    Swift

    public typealias RequestMutation = (inout URLRequest) -> Void
  • Initiates a network request for the given resource.

    Handle the result of the request by attaching response handlers:

    resource.request(.get)
        .onSuccess { ... }
        .onFailure { ... }
    

    See Request for a complete list of hooks.

    Note that, unlike load() and loadIfNeeded(), this method does not update latestData or latestError, and does not notify resource observers about the result.

    See also

    SeeAlso:

    Declaration

    Swift

    public func request(
            _ method: RequestMethod,
            requestMutation adHocMutation: @escaping RequestMutation = { _ in })
        -> Request

    Parameters

    method

    The HTTP verb to use for the request

    requestMutation

    An optional callback to change details of the request before it is sent. Does nothing by default. Note that this is applied before any mutations configured with Configuration.mutateRequests(...). This allows configured mutations to inspect and alter the request after it is fully populated.

  • True if the resource’s local state is up to date according to staleness configuration.

    “Up to date” means that either:

    • the resource has data (i.e. latestData is not nil),
    • the last request succeeded (i.e. latestError is nil), and
    • the timestamp on latestData is more recent than expirationTime seconds ago,

    …or:

    • the last request failed (i.e. latestError is not nil), and
    • the timestamp on latestError is more recent than retryTime seconds ago.

    Declaration

    Swift

    @objc
    public var isUpToDate: Bool { get }
  • Ensures that there is a load request in progress for this resource, unless the resource is already up to date.

    If the resource is not up to date and there is no load request already in progress, this method calls load().

    See also

    Declaration

    Swift

    @discardableResult
    public func loadIfNeeded() -> Request?
  • Initiates a GET request to update the state of this resource. This method forces a new request even if there is already one in progress. (See loadIfNeeded() for comparison.) This is the method to call if you want to force a check for new data — in response to a manual refresh, for example, or because you know that the data changed on the server.

    Sequence of events:

    1. This resource’s isLoading property becomes true, and remains true until the request either succeeds or fails. Observers immedately receive ResourceEvent.requested.
    2. If the request is cancelled before completion, observers receive ResourceEvent.requestCancelled.
    3. If the server returns a success response, that goes in latestData, and latestError becomes nil. Observers receive ResourceEvent.newData.
    4. If the server returns a 304, latestData’s timestamp is updated but the entity is otherwise untouched. latestError becomes nil. Observers receive ResourceEvent.notModified.
    5. If the request fails for any reason, whether client-, server-, or network-related, observers receive ResourceEvent.error. Note that latestData does not become nil; the last valid response always sticks around until another valid response arrives.

    Declaration

    Swift

    @discardableResult
    public func load() -> Request
  • Updates the state of this resource using the result of the given request. Use this method when you want a request to update latestData or latestError and notify observers just as load() would, but:

    • you need to use a request method other than GET,
    • you need to set headers or other request options, but just for this one request (so that Service.configure(...) won’t work), or
    • for some arcane reason, you want a request for a different resource to update the state of this one.

    For example, an authentication resource might return its state only in response to a POST:

    let auth = MyAPI.authentication
    auth.load(using:
      auth.request(
          .post, json: ["user": user, "password": pass]))
    

    Declaration

    Swift

    @discardableResult
    public func load(using req: Request) -> Request
  • If this resource has no observers, cancels all loadRequests.

    Declaration

    Swift

    @objc
    public func cancelLoadIfUnobserved()
  • Convenience to call cancelLoadIfUnobserved() after a delay. Useful for situations such as table view scrolling where views are being rapidly discarded and recreated, and you no longer need the resource, but want to give other views a chance to express interest in it before canceling any requests.

    The callback is called after the given delay, regardless of whether the request was cancelled.

    Declaration

    Swift

    @objc
    public func cancelLoadIfUnobserved(afterDelay delay: TimeInterval, then callback: @escaping () -> Void = {})

Local state changes

  • Directly updates latestData without touching the network. Clears latestError and broadcasts ResourceEvent.newData to observers.

    This method is useful for incremental and optimistic updates.

    You may send a request which does not return the complete state of the resource in the response body, but which still changes the state of the resource. You could handle this by initiating a refresh immedately after success:

    resource.request(.post, json: ["name": "Fred"])
      .onSuccess { _ in resource.load() }
    

    However, if you already know the resulting state of the resource given a success response, you can avoid the second network call by updating the entity yourself:

    resource.request(.post, json: ["name": "Fred"])
      .onSuccess {
          partialEntity in
    
          // Make a mutable copy of the current content
          guard resource.latestData != nil else {
              resource.load()  // No existing entity to update, so refresh
              return
          }
    
          // Do the incremental update
          var updatedContent = resource.jsonDict
          updatedContent["name"] = partialEntity.jsonDict["newName"]
    
          // Make that the resource’s new entity
          resource.overrideLocalContent(with: updatedContent)
      }
    

    Use this technique with caution!

    Note that the data you pass does not go through the standard ResponseTransformer chain. You should pass data as if it was already parsed, not in its raw form as the server would return it. For example, in the code above, updatedContent is a Dictionary, not Data containing encoded JSON.

    Declaration

    Swift

    public func overrideLocalData(with entity: Entity<Any>)
  • Convenience method to replace the content of latestData without altering the content type or other headers.

    If this resource has no content, this method sets the content type to application/binary.

    Declaration

    Swift

    @objc
    public func overrideLocalContent(with content: Any)
  • Forces the next call to loadIfNeeded() to trigger a request, even if the current content is fresh. Leaves the current values of latestData and latestError intact (including their timestamps).

    Use this if you know the current content is stale, but don’t want to trigger a network request right away.

    Any update to latestData or latestError — including a call to overrideLocalData(...) or overrideLocalContent(...) — clears the invalidation.

    See also

    wipe()

    Declaration

    Swift

    @objc
    public func invalidate()
  • Resets this resource to its pristine state, as if newly created.

    • Sets latestData to nil.
    • Sets latestError to nil.
    • Cancels all resource requests in progress.
    • Triggers a cache fetch if there is a persistent cache configured for this resource.

    Observers receive a newData(.wipe) event. Requests in progress call completion hooks with a cancellation error.

    See also

    invalidate()

    Declaration

    Swift

    @objc
    public func wipe()
  • Matches this specific resource when passed as a pattern to Service.configure(...).

    Declaration

    Swift

    public func configurationPattern(for service: Service) -> (URL) -> Bool
  • Typed content accessors such as .text and .jsonDict apply to latestData?.content.

    Declaration

    Swift

    public var entityForTypedContentAccessors: Entity<Any>? { get }

Requests with Hard-Wired Responses

  • Returns a request that immedately fails, without ever touching the network or applying the transformer pipeline.

    This is useful for performing pre-request validation: if you know a request is invalid before you even send it, you can return an immediate error response that looks just like any other Siesta error.

    Declaration

    Swift

    public static func failedRequest(returning error: RequestError) -> Request
  • Returns a request that immediately and always returns the given response, without ever touching the network or applying the transformer pipeline.

    Declaration

    Swift

    public static func hardWiredRequest(returning response: Response) -> Request

Requests with Custom Logic

Request Creation Conveniences

  • Convenience method to initiate a request with a body containing arbitrary data.

    Declaration

    Swift

    public func request(
            _ method:        RequestMethod,
            data:            Data,
            contentType:     String,
            requestMutation: @escaping RequestMutation = { _ in })
        -> Request

    Parameters

    method

    The HTTP method of the request.

    data

    The body of the request.

    contentType

    The value for the request’s Content-Type header. The priority order is as follows:

    requestMutation

    Allows you to override details fo the HTTP request before it is sent. See request(_:requestMutation:).

  • Convenience method to initiate a request with a text body.

    If the string cannot be encoded using the given encoding, this methods triggers the onFailure(...) request hook immediately, without touching the network.

    Declaration

    Swift

    public func request(
            _ method:        RequestMethod,
            text:            String,
            contentType:     String = "text/plain",
            encoding:        String.Encoding = String.Encoding.utf8,
            requestMutation: @escaping RequestMutation = { _ in })
        -> Request

    Parameters

    contentType

    text/plain by default.

    encoding

    UTF-8 (NSUTF8StringEncoding) by default.

  • Convenience method to initiate a request with a JSON body.

    If the json cannot be encoded as JSON, e.g. if it is a dictionary with non-JSON-convertible data, this methods triggers the onFailure(...) request hook immediately, without touching the network.

    Declaration

    Swift

    public func request(
            _ method:        RequestMethod,
            json:            JSONConvertible,
            contentType:     String = "application/json",
            requestMutation: @escaping RequestMutation = { _ in })
        -> Request

    Parameters

    contentType

    application/json by default.

  • Convenience method to initiate a request with URL-encoded parameters in the meesage body.

    This method performs all necessary escaping, and has full Unicode support in both keys and values.

    The content type is application/x-www-form-urlencoded.

    Declaration

    Swift

    public func request(
            _ method:          RequestMethod,
            urlEncoded params: [String:String],
            requestMutation:   @escaping RequestMutation = { _ in })
        -> Request

URL navigation

  • Returns the resource with the given string appended to the path of this resource’s URL, with a joining slash inserted if necessary.

    Use this method for hierarchical resource navigation. The typical use case is constructing a resource URL from path components and IDs:

    let resource = service.resource("/widgets")
    resource.child("123").child("details")
      //→ /widgets/123/details
    

    This method always returns a subpath of the receiving resource. It does not apply any special interpretation to strings such ./, // or ? that have significance in other URL-related situations. Special characters are escaped when necessary, and otherwise ignored. See ResourcePathsSpec for details.

    See also

    relative(_:)

    Declaration

    Swift

    @objc
    public func child(_ subpath: String) -> Resource
  • Returns the resource with the given URL, using this resource’s URL as the base if it is a relative URL.

    This method interprets strings such as ., .., and a leading / or // as relative URLs. It resolves its parameter much like an href attribute in an HTML document. Refer to ResourcePathsSpec for details.

    Declaration

    Swift

    @objc
    public func relative(_ href: String) -> Resource
  • Returns relative(href) if href is present, and nil if href is nil.

    This convenience method is useful for resolving URLs returned as part of a JSON response body:

    let href = resource.jsonDict["owner"] as? String  // href is an optional
    if let ownerResource = resource.optionalRelative(href) {
      ...
    }
    

    Declaration

    Swift

    @objc
    public func optionalRelative(_ href: String?) -> Resource?
  • Returns this resource with the given parameter added to or changed in the query string.

    If value is an empty string, the parameter appears in the query string with no value (e.g. ?foo).

    If value is nil, however, the parameter is removed.

    There is no support for parameters with an equal sign but an empty value (e.g. ?foo=). There is also no support for repeated keys in the query string (e.g. ?foo=1&foo=2). If you need to circumvent either of these restrictions, you can create the query string yourself and pass it to relative(_:) instead of using this method. For example:

    resource.relative("?foo=1&foo=2")
    

    Note

    Service gives out unique Resource instances according to the full URL in string form, and thus considers query string parameter order significant. Therefore, to ensure that you get the same Resource instance no matter the order in which you specify parameters, withParam(_:_:) sorts all parameters by name, including existing ones. Note that only withParam(_:_:) and withParams(_:) do this sorting; if you use other methods to create query strings, it is up to you to canonicalize your parameter order.

    See also

    withParams(_:)

    Declaration

    Swift

    @objc(withParam:value:)
    public func withParam(_ name: String, _ value: String?) -> Resource
  • Returns this resource with all the entries in the given dictionary added to or changed in the query string. Equivalent to chained calls to withParam(_:_:) using each key-value pair in the dictionary.

    See withParam(_:_:) for information about the meaning of nil values and empty strings, multi-values params, and canonical parameter ordering.

    Declaration

    Swift

    public func withParams(_ params: [String : String?]) -> Resource

Observing Resources

  • Adds an self-owned observer to this resource, which will receive notifications of changes to resource state.

    The resource holds a weak reference to the observer. If there are no strong references to the observer, it is automatically removed.

    Use this method for objects such as UIViewControllers which already have a lifecycle of their own, are retained elsewhere, and also happen to act as observers.

    Note

    This method prevents duplicates; adding the same observer object a second time has no effect. This is not necessarily true of other flavors of addObserver, which accept observers that are not objects.

    Declaration

    Swift

    @discardableResult
    public func addObserver(_ observerAndOwner: ResourceObserver & AnyObject) -> Self
  • Adds an observer to this resource, holding a strong reference to it as long as owner still exists.

    The resource holds only a weak reference to owner, and as soon as the owner goes away, the observer is removed.

    The typical use for this method is for glue objects whose only purpose is to act as an observer, and which would not normally be retained by anything else.

    Note

    By default, this method prevents duplicates only if the observer is an object. If you pass a struct twice, you will receive two calls for every event. This is because only objects have a notion of identity in Swift. You can implement ResourceObserver.observerIdentity to make a struct prevent duplicates; however, it’s usually easier to ensure that you don’t make redundant calls to this method if you’re passing a struct.

    Declaration

    Swift

    @discardableResult
    public func addObserver(_ observer: ResourceObserver, owner: AnyObject) -> Self
  • Adds a closure observer to this resource.

    The resource holds a weak reference to owner, and the closure will receive events only as long as owner still exists.

    Note

    Unlike the addObserver(_:) that takes objects, this method does not prevent duplicates. If you pass a closure twice, it will be called twice for every event. It has to be this way, because Swift has no notion of closure identity: there is no such thing as “the same” closure in the language, and thus no way to detect duplicates. It is thus the caller’s responsibility to prevent redundant calls to this method.

    Declaration

    Swift

    @discardableResult
    public func addObserver(
            owner: AnyObject,
            file: String = #file,
            line: Int = #line,
            closure: @escaping ResourceObserverClosure)
        -> Self
  • Removes all observers owned by the given object.

    Declaration

    Swift

    @objc(removeObserversOwnedBy:)
    public func removeObservers(ownedBy owner: AnyObject?)