Latest 1.2.2
Homepage https://github.com/Blue-Rocket/WebApiClient
License Apache License, Version 2.0
Platforms ios 8.0, requires ARC
Authors

WebApiClient is an application framework aimed at making RESTful web service
requests in a standardized way. It works by configuring named routes for each web
service endpoint so your application focuses on consuming the logical web service
API, not HTTP implementation details.

The project is divided into modules, staring with a Core module and then
branching off into various supporting modules that add functionality like object
mapping
, caching, and UI support.

Module: Core

The Core module provides a HTTP client framework based on routes with support
for object mapping for transforming requests and responses between native objects
and serialized forms, such as JSON. This module provides just a protocol based API
and some scaffolding classes to support the API, but does not provide an actual full
implementation itself, so that different HTTP back-ends can be used as needed. The
AFNetworking module provides a full implementation of the API.

The
WebApiClient protocol defines the main HTTP client entry point for
applications to use. The API is purposefully simple and based on asynchronous block
callbacks:

- (void)requestAPI:(NSString *)name
 withPathVariables:(id)pathVariables
        parameters:(id)parameters
              data:(id<WebApiResource>)data
          finished:(void (^)(id<WebApiResponse> response, NSError *error))callback;

An example invocation of this API might look like this:

// make a GET request to /documents/123
[client requestAPI:@"doc" withPathVariables:@{@"uniqueId" : @123 } parameters:nil data:nil
          finished:^(id<WebApiResponse> response, NSError *error) {
    if ( !error ) {
        MyDocument *doc = response.responseObject;
    } else if ( response.statusCode == 422 ) {
        // handle 422 (validation) errors here...
    }
}];

Background callback support

By default the callback block is called on the main thread (queue). If you prefer to
have the callabck on a specific queue, you can use an alternate method that accepts a
dispatch queue as a parameter. In that case, the passed in queue will be used for the
callback:

- (void)requestAPI:(NSString *)name
 withPathVariables:(id)pathVariables
        parameters:(id)parameters
              data:(id<WebApiResource>)data
             queue:(dispatch_queue_t)callbackQueue
          progress:(nullable WebApiClientRequestProgressBlock)progressCallback
          finished:(void (^)(id<WebApiResponse> response, NSError *error))callback;

Progress callback support

The same method that accepts an explicit callback block shown in the previous section
also accepts an optional WebApiClientRequestProgressBlock, which is defined as this:

typedef void (^WebApiClientRequestProgressBlock)(NSString *routeName,
                                                 NSProgress * _Nullable uploadProgress,
                                                 NSProgress * _Nullable downloadProgress);

By passing in this type of block to the progress parameter, you can monitor both
the upload and download progress of the HTTP request. In addition the
WebApiClientRequestDidProgressNotification and
WebApiClientResponseDidProgressNotification notifications can be used to listen for
progress updates as well. The WebApiClientProgressNotificationKey notification user
info key will contain the relevant NSProgress object.

Synchronous request support

Sometimes it can be useful to make a blocking, synchronous request to get a HTTP
resource. WebApiClient supports that as well:

- (id<WebApiResponse>)blockingRequestAPI:(NSString *)name
                       withPathVariables:(id)pathVariables
                              parameters:(id)parameters
                                    data:(id<WebApiResource>)data
                             maximumWait:(NSTimeInterval)maximumWait
                                   error:(NSError **)error;

Calling this method will block the calling thread until either the response is
available or maximumWait seconds have elapsed.

Routing

The WebApiRoute protocol defines a single API endpoint definition,
assigned a unique name. Routes are typically configured when an application starts
up. Each route defines some standardized properties, such as a HTTP method and URL
path. For convenience, routes support arbitrary property access via Objective-C’s
keyed subscript support, so the following is possible:

id<WebApiRoute> myRoute = ...;

// access the path property
NSString *path1 = myRoute.path;

// access the path property using keyed subscript notation
NSString *path2 = myRoute[@"path"];

// access some arbitrary property not defined in WebApiRoute specifically
id something = myRoute[@"extendedProperty"];

For even more convenience, WebApiRoute provides
extensions to NSDictionary and NSMutableDictionary so
that they conform to WebApiRoute and MutableWebApiRoute, respectively. That means
you can use dictionaries directly as routes, like this:

// define a route
id<WebApiRoute> myRoute = @{ @"name" : "login", @"path" : @"user/login", @"method" : @"POST" };

// create a mutable copy and extend
id<MutableWebApiRoute> mutableRoute = [myRoute mutableCopy];
mutableRoute[@"extendedProperty"] = @"special";

Object mapping

The WebApiDataMapper protocol defines an API for encoding
native objects into HTTP requests and mapping HTTP responses into native objects.
Routes can be configured with a dataMapper property to support this feature. The
API is also pretty simple:

@protocol WebApiDataMapper <NSObject>

// Map a source data object into some domain object.
- (id)performMappingWithSourceObject:(id)sourceObject route:(id<WebApiRoute>)route error:(NSError *__autoreleasing *)error;

// Encode a domain object into an encoded form, such as @c NSDictionary or @c NSData.
- (id)performEncodingWithObject:(id)domainObject route:(id<WebApiRoute>)route error:(NSError *__autoreleasing *)error;

@end

Module: AFNetworking

The AFNetworking module provides a full implementation of the WebApiClient API,
based on AFNetworking and NSURLSession.

Route configuration

Routes can be configured in code via the registerRoute:forName: method, but more
conveniently they can be configured via BREnvironment. The webservice.api
key will be inspected by default, and can be a dictionary representing all the routes
that should be registered for the application. For example, the following JSON would
register three routes, login, register, and absolute:

{
  "App_webservice_protocol" : "https",
  "App_webservice_host" : "example.com",
  "App_webservice_port" : 443,
  "webservice" : {
    "api" : {
      "register" : {
        "method" : "POST",
        "path" : "user/register",
      },
      "login" : {
        "method" : "POST",
        "path" : "user/login",
      },
      "absolute" : {
        "method" : "GET",
        "path" : "https://example.com/something"
      }
    }
  }
}

You’ll notice that the register and login routes have relative paths. All
webservice URLs are constructed as relative to a configurable baseApiURL property,
which by default is configured via the various App_webservice_*
BREnvironment keys you can see in the previous example JSON.

GZip compression support

The gzip property on routes is supported. When set to true any request data
will be compressed and a request HTTP header of Content-Encoding: gzip will be
added.

To support compressed response data (highly recommended!) you only need to
configure an Accept-Encoding: gzip HTTP header, either individually on routes via
the requestHeaders property or via the globalHTTPRequestHeaders property
available on AFNetworkingWebApiClient.

Here’s an example route that configures both request and response compression:

{
  "webservice" : {
    "api" : {
      "trim" : {
        "method" : "POST",
        "path" : "upload/jumbo",
        "gzip" : true,
        "requestHeaders" : {
          "Accept-Encoding" : "gzip"
        }
      }
    }
  }
}

Upload raw data

Raw data can be uploaded directly in the body of the HTTP request. This can be
useful, for example, when you need to upload images, or any other type of data, and
the URL contains sufficient information to identify the content. To perform a raw
data upload, pass a WebApiResource instance on the data parameter
in the WebApiClient API. WebApiClient provides two implementations of
WebApiResource: DataWebApiResource for in-memory data and FileWebApiResource
for file-based data. The WebApiResource instance you pass in will be sent directly
in the body of the request, and appropriate Content-Type and Content-MD5 HTTP
headers will be included. This means the parameters object is ignored. If you need
to post both parameters and a file, use the multipart/form-data upload method
described in the next section.

Upload data (multipart/form-data)

Instead of uploading raw data, you can also upload using a multipart/form-data
attachment encoding by passing a WebApiResource instance on the
data parameter in the WebApiClient API and configuring the route with a
serialization type of form, like this:

{
  "webservice" : {
    "api" : {
      "trim" : {
        "method" : "POST",
        "path" : "upload/image",
        "serializationName" : "form"
      }
    }
  }
}

The WebApiClient API parameters object, if provided, will also be included in the
request, serialized into additional parts of the request body.

Download raw data

You can configure a route to save the response data into a file, instead of the
default of loading the response in RAM, by adding a saveAsResource property with a
truthy value, like this:

{
  "webservice" : {
    "api" : {
      "download" : {
        "method" : "GET",
        "path" : "download/image",
        "saveAsResource" : true
      }
    }
  }
}

Then the responseObject returned in the WebApiResponse will be a
WebApiResource which you can then move to an appropriate location
as needed, for example:

[client requestAPI:@"download" withPathVariables:nil parameters:nil data:nil
             queue:dispatch_get_main_queue()
          progress:nil
          finished:^(id<WebApiResponse> response, NSError *error) {
    if ( !error ) {
        id<WebApiResource> resource = response.responseObject;
        NSURL *dest = [NSURL fileURLWithPath:@"/some/path"];
        [[NSFileManager defaultManager] moveItemAtURL:[resource URLValue] toURL:dest error:nil];
    }
}];

Module: Cache

The Cache module provides response caching support to the WebApiClient API by
providing the PINCacheWebApiClient proxy that can cache
result objects using PINCache. Caching support is enabled by configuring a
cacheTTL property on routes, for example:

{
  "webservice" : {
    "api" : {
      "info" : {
        "method" : "GET",
        "path" : "infrequentlyupdated/info",
        "cacheTTL" : 3600
      }
    }
  }
}

See the CachingWebApiRoute protocol for more route cache
details.

Routes that invalidate cached data for other routes

You can also configure a route so that it invalidates any cached data for other
routes. A good example of where this is useful is when you define a list route that
returns a list of objects, and another add route to add to that same list of
objects. We can make the latter route invalidate the cached data of the former like
this:

{
  "webservice" : {
    "api" : {
      "list" : {
        "method" : "GET",
        "path" : "stuff/list",
        "cacheTTL" : 3600
      },
      "add" : {
        "method" : "PUT",
        "path" : "stuff/:thingId",
        "invalidatesCachedRouteNames" : [ "list" ]
      }
    }
  }
}

The invalidatesCachedRouteNames is configured as an array of route names that
should be invalidated when that route is called successfully.

Ignoring route URL query parameters

By default URL query parameters will be included when calculating the cache key for
each route. Sometimes it can be useful to ignore the query parameters, however. For
example, pre-signed Amazon S3 resource URLs contain authorization query parameters
that change each time the same resource is requested. By configuring the route with
cacheIgnoreQueryParameters = YES then the query parameters for that route will be
not be included in the request’s cache key:

{
  "webservice" : {
    "api" : {
      "info" : {
        "method" : "GET",
        "path" : "https://s3.amazon.com/info/foo.txt",
        "cacheTTL" : 3600,
        "cacheIgnoreQueryParameters" : true
      }
    }
  }
}

Cache groups for multi-user support

PINCacheWebApiClient supports a keyDiscriminator property
that can be changed at runtime to support isolating all cached route data into
groups. The main use case for this is to support multi-user apps where route URLs do
not contain user-identifying parameters, so when different users are signed in they
don’t see cached data from some other user. You can assign the active user’s unique
identifier to the keyDiscriminator property and then all cached data becomes
user-specific. When a user logs out and a different user logs in, change the
keyDiscriminator to the new user’s identifier.

Checking for cached data

The CachingWebApiClient API adds a method that can be used
for testing if cached data is available:

- (void)requestCachedAPI:(NSString *)name
       withPathVariables:(nullable id)pathVariables
              parameters:(nullable id)parameters
                   queue:(dispatch_queue_t)callbackQueue
                finished:(void (^)(id<WebApiResponse> _Nullable response, NSError * _Nullable error))callback;

The callback will be passed a response only if the data was available in the cache.
Sometimes its handy to know if something is already downloaded!

Module: RestKit

The RestKit module provides an object mapping implementation for the
WebApiClient API based on the RestKit. It provides a way to transform native
objects into JSON, and vice versa. This module only makes use of the
RestKit/ObjectMapping module, so it does not conflict with AFNetworking 2. In fact,
part of the motivation for WebApiClient was to be able to use AFNetworking 2 with
RestKit’s object mapping support because RestKit’s networking layer is based on
AFNetworking 1. In some respects the WebApiClient API provides some of the same
scaffolding that the full RestKit project provides.

Mapping configuration

The RestKitWebApiDataMapper class supports a shared
singleton pattern that your application can configure when it starts up with any
required RKObjectMapping objects. You configure it like this:

RestKitWebApiDataMapper *dataMapper = [RestKitWebApiDataMapper sharedDataMapper];

// get RestKit mapper for user objects
RKObjectMapper *userObjectMapper = ...;

// register user mapper for requests and responses
[dataMapper registerRequestObjectMapping:[userObjectMapper inverseMapping] forRouteName:@"login"];
[dataMapper registerResponseObjectMapping:userObjectMapper forRouteName:@"login"];

Block-based encoding & mapping

The RestKitWebApiDataMapper class also supports
block-based mapping hooks for both request encoding and response mapping. The blocks
are executed after any configured RKObjectMapper has done its job on the request
or response data.

One useful example of this support is for wiring up parent-child relationship
properties that are implied by the data. Imagine a Person class that has an array
of child Person objects, and each child has a parent property that points to its
parent Person instance:

@interface Person : NSObject

@property (strong) NSString *name;
@property (strong) NSArray<Person *> *children;
@property (weak) Person *parent;

@end

The server returns JSON like this:

{
  "name" : "John Doe",
  "children" : [
    { "name" : "Johnny Doe" },
    { "name" : "Jane Doe" }
  ]
}

To populate each child’s parent property with the John Doe object, a response
mapping block could be configured like this:

[dataMapper registerResponseMappingBlock:^(id sourceObject, id<WebApiRoute> route, NSError * __autoreleasing *error) {
    if ( [sourceObject isKindOfClass:[Person class]] ) {
        // populate the Child -> Parent relationship
        Person *parent = sourceObject;
        for ( Person *child in parent.children ) {
            child.parent = parent;
        }
    }
    return sourceObject;
} forRouteName:@"parent-child-tree"];

Route configuration

To use RestKit-based object mapping with a route, you configure the dataMapper
property of the route with RestKitWebApiDataMapper like this:

{
  "webservice" : {
    "api" : {
      "login" : {
        "method" : "POST",
        "path" : "user/login",
        "dataMapper" : "RestKitWebApiDataMapper"
      }
    }
  }
}

Sometimes the request or response JSON needs to be nested in some top-level object.
For example imagine that the register endpoint expects the user object to be posted
as JSON like this:

{
  "user" : { "email" : "[email protected]", "name" : "Joe" }
}

This can be done by adding a dataMapperRequestRootKeyPath property (or
dataMapperResponseRootKeyPath for mapping responses), like this:

{
  "webservice" : {
    "api" : {
      "login" : {
        "method" : "POST",
        "path" : "user/login",
        "dataMapper" : "RestKitWebApiDataMapper",
        "dataMapperRequestRootKeyPath" : "user"
      }
    }
  }
}

Module: UI

The UI module provides some UI utilities, such as the
WebApiClientActivitySupport class that listens to
route requests and for those that specify preventUserInteraction with a truthy
value will throw up a full-screen "request taking too long" view to let the user of
the app know it’s waiting for a response. For example, a route can be configured like:

{
  "webservice" : {
    "api" : {
      "login" : {
        "method" : "POST",
        "path" : "user/login",
        "preventUserInteraction" : true
      }
    }
  }
}

Latest podspec

{
    "name": "WebApiClient",
    "version": "1.2.2",
    "summary": "A HTTP client API based on configured routes.",
    "description": "WebApiClient provides a protocol-based HTTP client API based on configured routes withnsupport for object mapping for transforming requests and responses between native objectsnand serialized forms, such as JSON. A full implementation of the API is also provided,nbased on AFNetworking.",
    "homepage": "https://github.com/Blue-Rocket/WebApiClient",
    "license": {
        "type": "Apache License, Version 2.0",
        "file": "LICENSE"
    },
    "authors": {
        "Matt Magoffin": "[email protected]"
    },
    "social_media_url": "http://twitter.com/bluerocketinc",
    "platforms": {
        "ios": "8.0"
    },
    "source": {
        "git": "https://github.com/Blue-Rocket/WebApiClient.git",
        "tag": "1.2.2"
    },
    "requires_arc": true,
    "default_subspecs": "All",
    "subspecs": [
        {
            "name": "All",
            "dependencies": {
                "WebApiClient/Core": [],
                "WebApiClient/AFNetworking": [],
                "WebApiClient/Cache": [],
                "WebApiClient/RestKit": [],
                "WebApiClient/UI": []
            }
        },
        {
            "name": "Core",
            "source_files": [
                "WebApiClient/Code/WebApiClient-Core.h",
                "WebApiClient/Code/WebApiClient"
            ],
            "dependencies": {
                "BRCocoaLumberjack": [
                    "~> 2.0"
                ],
                "BREnvironment": [
                    "~> 1.1"
                ],
                "BRLocalize/Core": [],
                "MAObjCRuntime": [
                    "~> 0.0.1"
                ],
                "SOCKit": [
                    "~> 1.1"
                ]
            }
        },
        {
            "name": "AFNetworking",
            "source_files": [
                "WebApiClient/Code/WebApiClient-AFNetworking.h",
                "WebApiClient/Code/WebApiClient-AFNetworking"
            ],
            "dependencies": {
                "WebApiClient/Core": [],
                "AFNetworking/NSURLSession": [
                    "~> 2.5"
                ],
                "AFgzipRequestSerializer": [
                    "~> 0.0.2"
                ]
            }
        },
        {
            "name": "Cache",
            "source_files": [
                "WebApiClient/Code/WebApiClient-Cache.h",
                "WebApiClient/Code/WebApiClient-Cache"
            ],
            "dependencies": {
                "WebApiClient/Core": [],
                "PINCache": [
                    "~> 2.0"
                ]
            }
        },
        {
            "name": "RestKit",
            "source_files": [
                "WebApiClient/Code/WebApiClient-RestKit.h",
                "WebApiClient/Code/WebApiClient-RestKit"
            ],
            "dependencies": {
                "WebApiClient/Core": [],
                "RestKit/ObjectMapping": [
                    "~> 0.27"
                ],
                "TransformerKit/String": [
                    "~> 0.5"
                ]
            }
        },
        {
            "name": "UI",
            "source_files": [
                "WebApiClient/Code/WebApiClient-UI.h",
                "WebApiClient/Code/WebApiClient-UI"
            ],
            "dependencies": {
                "WebApiClient/Core": [],
                "ImageEffects": [
                    "~> 1.0"
                ],
                "MAObjCRuntime": [
                    "~> 0.0.1"
                ],
                "Masonry": [
                    "~> 0.6"
                ]
            }
        }
    ]
}

Pin It on Pinterest

Share This