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.
-
The API to which this resource belongs. Provides configuration defaults and instance uniqueness.
Declaration
Swift
@objc public let service: Service
-
The canoncial URL of this resource.
Declaration
Swift
@objc public let url: URL
-
Configuration when there is no request method.
Declaration
Swift
public var configuration: Configuration { get }
-
Configuration for requests with the given request method.
Declaration
Swift
public func configuration(for method: RequestMethod) -> Configuration
-
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()
andloadIfNeeded()
, not any of the various flavors ofrequest(...)
.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.See also
TypedContentAccessors
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()
andloadIfNeeded()
, not any of the various flavors ofrequest(...)
.Declaration
Swift
public private(set) var latestError: RequestError? { get set }
-
The time of the most recent update to either
latestData
orlatestError
.Declaration
Swift
@objc public var timestamp: TimeInterval { get }
-
True if any load requests (i.e. from calls to
load(...)
andloadIfNeeded()
) 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 }
-
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 asResource.request(_:json:contentType:requestMutation:)
will encode common request body types for you. Custom mutation is the “full control” option for cases when:- you need to alter the request in ways Siesta doesn’t provide helpers for, or
- you want to alter one individual request instead of configuring all requests for a resource.
The
RequestMutation
receives aURLRequest
after Siesta has already applied all of its normal configuration. TheURLRequest
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? BecauseRequest.repeated()
does not repeat the original request verbatim; instead, it recomputes the request headers using the latest configuration, then reapplies yourRequestMutation
.See also
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.
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 thanexpirationTime
seconds ago,
…or:
- the last request failed (i.e.
latestError
is not nil), and - the timestamp on
latestError
is more recent thanretryTime
seconds ago.
Declaration
Swift
@objc public var isUpToDate: Bool { get }
- the resource has data (i.e.
-
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:
- This resource’s
isLoading
property becomes true, and remains true until the request either succeeds or fails. Observers immedately receiveResourceEvent.requested
. - If the request is cancelled before completion, observers receive
ResourceEvent.requestCancelled
. - If the server returns a success response, that goes in
latestData
, andlatestError
becomes nil. Observers receiveResourceEvent.newData
. - If the server returns a 304,
latestData
’s timestamp is updated but the entity is otherwise untouched.latestError
becomes nil. Observers receiveResourceEvent.notModified
. - If the request fails for any reason, whether client-, server-, or network-related, observers receive
ResourceEvent.error
. Note thatlatestData
does not become nil; the last valid response always sticks around until another valid response arrives.
Declaration
Swift
@discardableResult public func load() -> Request
- This resource’s
-
Updates the state of this resource using the result of the given request. Use this method when you want a request to update
latestData
orlatestError
and notify observers just asload()
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]))
-
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 = {})
-
Directly updates
latestData
without touching the network. ClearslatestError
and broadcastsResourceEvent.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 aDictionary
, notData
containing encoded JSON.See also
overrideLocalContent(with:)
Declaration
Swift
public func overrideLocalData(with entity: Entity<Any>)
-
Convenience method to replace the
content
oflatestData
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 oflatestData
andlatestError
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
orlatestError
— including a call tooverrideLocalData(...)
oroverrideLocalContent(...)
— 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()
- Sets
-
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 tolatestData?.content
.Declaration
Swift
public var entityForTypedContentAccessors: Entity<Any>? { get }
-
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.
-
Creates (but does not start) a new request using custom request logic.
This method allows you to make a request using custom / external logic that does not use the service’s normal network provider. For example, you could use this to wrap a third-party OAUth library as a Siesta request, then use
Configuration.decorateRequests(...)
andRequest.chained(...)
to wait for OAuth before proceeding with the normal Siesta request.See also
RequestDelegate
See also
Request.start()
Declaration
Swift
public static func prepareRequest(using delegate: RequestDelegate) -> Request
-
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:- any content-type set in
Configuration.mutateRequests(...)
overrides - any content-type set in
requestMutation
, which overrides - this parameter, which overrides
- any content-type set with
Configuration.headers
.
requestMutation
Allows you to override details fo the HTTP request before it is sent. See
request(_:requestMutation:)
. - any content-type set in
-
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 theonFailure(...)
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
-
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. SeeResourcePathsSpec
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 anhref
attribute in an HTML document. Refer toResourcePathsSpec
for details.See also
Declaration
Swift
@objc public func relative(_ href: String) -> Resource
-
Returns
relative(href)
ifhref
is present, and nil ifhref
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 torelative(_:)
instead of using this method. For example:resource.relative("?foo=1&foo=2")
Note
Service
gives out uniqueResource
instances according to the full URL in string form, and thus considers query string parameter order significant. Therefore, to ensure that you get the sameResource
instance no matter the order in which you specify parameters,withParam(_:_:)
sorts all parameters by name, including existing ones. Note that onlywithParam(_:_:)
andwithParams(_:)
do this sorting; if you use other methods to create query strings, it is up to you to canonicalize your parameter order.See also
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
-
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
UIViewController
s 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 ofaddObserver
, 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 implementResourceObserver.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 asowner
still exists.Note
Unlike theaddObserver(_:)
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?)