Skip to content
Aleksey Garbarev edited this page Jun 30, 2015 · 2 revisions

Quick Start

This chapter aimed to get started with TRC as soon as possible. You'll meet all features starting from simplest ones.

Let's build simple REST client to Redmine issue tracker in this sample.

1. Configuration

First of all, we have to create instance of TyphoonRestClient and setup network connection.

_restClient = [TyphoonRestClient new];

id<TRCConnection> connection = [[TRCConnectionAFNetworking alloc] initWithBaseUrl:[NSURL URLWithString:@"http://www.redmine.org"]];

//Uncomment to add logger into connection chain
//connection = [[TRCConnectionLogger alloc] initWithConnection:connection];

_restClient.connection = connection;

2. API Call

Then we have to configure our API call. Our first API call would be request to get all issues. Let's create new class named RequestToGetIssues.

RequestToGetIssues.h

#import "TRCRequest.h"

@interface RequestToGetIssues : NSObject <TRCRequest>

@end

RequestToGetIssues.m

#import "RequestToGetIssues.h"

@implementation RequestToGetIssues

- (NSString *)path
{
    return @"issues.json";
}

- (TRCRequestMethod)method
{
    return TRCRequestMethodGet;
}

@end

3. Invoke API Call

That's all, now you can call API method, like this:

RequestToGetIssues *request = [RequestToGetIssues new];
    
[_restClient sendRequest:request completion:^(id result, NSError *error) {
   // ... 
}];

In the completion block, you have two arguments: id result and NSError *error. Currently, result is NSDictionary, parsed from JSON text response (because default response serialization is JSON) and error is nil, since everything is correct.

4. Validation Scheme

Ok, it's still boring. Let's add complexity and some fun. First of all, we may want to be sure that response from server match API.

Let's add empty file named RequestToGetIssues.response.json to the project. This is schema of response structure we want to validate.

Writing schema files from scratch is too boring. Easiest way to get starting point is copy-paste of server response. Let's call http://www.redmine.org/issues.json?limit=1 for that and save result into created file:

RequestToGetIssues.response.json

{
    "issues": [
        {
            "id": 19448,
            "project": 
            {
                "id": 1,
                "name": "Redmine"
            },
            "tracker": 
            {
                "id": 2,
                "name": "Feature"
            },
            "status": 
            {
                "id": 1,
                "name": "New"
            },
            "priority": 
            {
                "id": 4,
                "name": "Normal"
            },
            "author": 
            {
                "id": 124165,
                "name": "Reginaldo P. Fernandes Ribeiro Reginaldo"
            },
            "category": 
            {
                "id": 24,
                "name": "Documentation"
            },
            "subject": "Mapa do SIte",
            "description": "Cria\u00e7\u00e3o do mapa do site do sistema para intera\u00e7\u00e3o com o usu\u00e1rio.",
            "done_ratio": 0,
            "custom_fields": [
                {
                    "id": 2,
                    "name": "Resolution",
                    "value": ""
                }
            ],
            "created_on": "2015-03-22T10:53:24Z",
            "updated_on": "2015-03-22T10:53:24Z"
        }
    ],
    "total_count": 4479,
    "offset": 0,
    "limit": 1
}

Let's invoke API call again and see the result.. For me result is error: "Can't find value for key 'category' in 'root.issues[1]' dictionary"

RequestToGetIssues.response.json catched automatically, because name of file match Request class, and extension is 'response.json'. This is default naming rule. You can use any filename for response schema, but you have to return that name by implementation of - (NSString *)responseBodyValidationSchemaName; in TRCRequest.

By default, TyphoonRestClient validate structure of response: checks that all keys exists in dictionaries, and values has correct types (string, number, array, dictionary). That means that all keys are required by default, but redmine API has optional keys too. Let's change that in scheme to avoid validation error. For that we can add {?} suffix to key name, like that:

{
....
    "author{?}": 
    {
        ...
    },
    "category{?}": 
    {
        ...
    },
    "custom_fields{?}": [
        ...
    ],   
....     
}

Wow! Now response passes validation! And if validation error occured again, we can solve that easily by:

  • Change scheme, make more fields optionals
  • Call backend team and discuss API protocol

So, that help you to be consistent with API

5. Value Transformers

Still boring? Validation not needed for you? Ok, let's try to see what else we can do with existing scheme. As you could notice before, we getting NSDictionary as result, but this dictionary contains only NSNumber and NSString. What about "created_on" and "updated_on" fields? They are dates, and they are useless until they are NSString.

Of course, we can parse them using NSDateFormatting, while parsing NSDictionary result, but this solution hard to reuse and probably not so clear. Let's try TRCValueTransformer and see how this works.

For that we'll create new class:

TRCValueTransformerDateISO8601.h

#import "TRCValueTransformer.h"

@interface TRCValueTransformerDateISO8601 : NSObject <TRCValueTransformer>

@end

TRCValueTransformerDateISO8601.m

#import "TRCValueTransformerDateISO8601.h"
#import "TRCUtils.h"

@implementation TRCValueTransformerDateISO8601

+ (NSDateFormatter *)sharedDateFormatter
{
    static NSDateFormatter *dateFormatter = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"];
        NSLocale *posix = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        [dateFormatter setLocale:posix];
    });
    return dateFormatter;
}

- (NSDate *)objectFromResponseValue:(NSString *)responseValue error:(NSError **)error
{
    NSDateFormatter *dateFormatter = [[self class] sharedDateFormatter];
    
    NSDate *date = [dateFormatter dateFromString:responseValue];
    if (!date && error) {
        *error = NSErrorWithFormat(@"Can't create NSDate from string '%@'", responseValue);
    }
    return date;
}

- (NSString *)requestValueFromObject:(id)object error:(NSError **)error
{
    if (![object isKindOfClass:[NSDate class]]) {
        if (error) {
            *error = NSErrorWithFormat(@"Can't convert '%@' into NSString using %@", [object class], [self class]);
        }
        return nil;
    }
    NSDateFormatter *dateFormatter = [[self class] sharedDateFormatter];
    
    NSString *string = [dateFormatter stringFromDate:object];
    
    if (!string && error) {
        *error = NSErrorWithFormat(@"Can't convert NSDate '%@' into NSStrign", object);
    }
    
    return string;
}

@end

then we need to register that transformer for special tag, while configuring TyphoonRestClient:

[_restClient registerValueTransformer:[TRCValueTransformerDateISO8601 new] forTag:@"{date_iso8601}"];

And finally, just use that tag in scheme:

{
...
   "created_on": "{date_iso8601}",
   "updated_on": "{date_iso8601}"
...
}

6. Response handling

Right now, we getting NSDictionary as result, but getting NSArray of Issue model objects would be much nicer, right?

I'll create model object, like that:

Issue.h

@interface Issue : NSObject

@property (nonatomic, strong) NSNumber *identifier;
@property (nonatomic, strong) NSString *projectName;
@property (nonatomic, strong) NSString *authorName;
@property (nonatomic, strong) NSString *statusText;

@property (nonatomic, strong) NSString *subject;
@property (nonatomic, strong) NSString *descriptionText;

@property (nonatomic, strong) NSDate *created;
@property (nonatomic, strong) NSDate *updated;

@property (nonatomic, strong) NSNumber *doneRatio;

@end

Issue.m

#import "Issue.h"

@implementation Issue

@end

Then we should change our RequestToGetIssues.m by adding method:

- (id)responseProcessedFromBody:(NSDictionary *)bodyObject headers:(NSDictionary *)responseHeaders status:(TRCHttpStatusCode)statusCode error:(NSError **)parseError
{
    NSArray *issuesDicts = bodyObject[@"issues"];
    NSMutableArray *result = [[NSMutableArray alloc] initWithCapacity:[issuesDicts count]];
    
    for (NSDictionary *dict in issuesDicts) {
        Issue *issue = [Issue new];
        issue.identifier = dict[@"id"];
        issue.projectName = dict[@"project"][@"name"];
        issue.authorName = dict[@"author"][@"name"];
        issue.statusText = dict[@"status"][@"name"];
        issue.subject = dict[@"subject"];
        issue.descriptionText = dict[@"description"];
        issue.updated = dict[@"created_on"];
        issue.created = dict[@"updated_on"];
        issue.doneRatio = dict[@"done_ratio"];
        [result addObject:issue];
    }
    
    return result;
}

Now, when we calling sendRequest:completion: in TyphoonRestClient, we'll get NSArray of Issue objects.

Notice, that we can avoid any checking here (like 'if value for key exists and has correct type') because it's already done by TyphoonRestClient validation using your scheme. Only optional values can be nil here, so you can check for that, if nil not acceptable. Also note, that values for keys created_on and updated_on already transformed and has NSDate type. So we only map dictionary into model object here.

7. Object Mappers

Looks ok, but how to reuse that mapping code, if same Issue object comes from another API Call? TRCObjectMapper aimd to solve that issue.

TRCObjectMapper works similar to TRCValueTranformer, but maps dictionary into model object.

Let's try to refactor our working application to use TRCObjectMapper, for that we'll create mappers class:

TRCObjectMapperIssue.h

#import "TRCObjectMapper.h"

@interface TRCObjectMapperIssue : NSObject <TRCObjectMapper>

@end

TRCObjectMapperIssue.m

#import "TRCObjectMapperIssue.h"
#import "Issue.h"

@implementation TRCObjectMapperIssue

- (id)objectFromResponseObject:(NSDictionary *)dict error:(NSError **)error
{
    Issue *issue = [Issue new];
    issue.identifier = dict[@"id"];
    issue.projectName = dict[@"project"][@"name"];
    issue.authorName = dict[@"author"][@"name"];
    issue.statusText = dict[@"status"][@"name"];
    issue.subject = dict[@"subject"];
    issue.descriptionText = dict[@"description"];
    issue.updated = dict[@"created_on"];
    issue.created = dict[@"updated_on"];
    issue.doneRatio = dict[@"done_ratio"];
    return issue; //You allowed to return nil. If items in the array, this item would be skipped
}

@end

and copy Issue schema into file named TRCObjectMapperIssue.response.json

TRCObjectMapperIssue.response.json

{
    "id": 19448,
    "project":
    {
        "id": 1,
        "name": "Redmine"
    },
    "tracker":
    {
        "id": 2,
        "name": "Feature"
    },
    "status":
    {
        "id": 1,
        "name": "New"
    },
    "priority":
    {
        "id": 4,
        "name": "Normal"
    },
    "author{?}":
    {
        "id": 124165,
        "name": "Reginaldo P. Fernandes Ribeiro Reginaldo"
    },
    "category{?}":
    {
        "id": 24,
        "name": "Documentation"
    },
    "subject": "Mapa do SIte",
    "description": "Cria\u00e7\u00e3o do mapa do site do sistema para intera\u00e7\u00e3o com o usu\u00e1rio.",
    "done_ratio": 0,
    "custom_fields{?}": [
    {
        "id": 2,
        "name": "Resolution",
        "value": ""
    }
    ],
    "created_on": "{date_iso8601}",
    "updated_on": "{date_iso8601}"
}

TyphoonRestClient will validate and tranform values in response dictioanry using that scheme.

Then we have to register that object mapper for special tag:

[_restClient registerObjectMapper:[TRCObjectMapperIssue new] forTag:@"{issue}"];

and modify RequestToGetIssues.response.json and RequestToGetIssues.m:

RequestToGetIssues.response.json

{
    "issues": [
        "{issue}"
    ],
    "total_count": 4479,
    "offset": 0,
    "limit": 1
}

RequestToGetIssues.m

#import "RequestToGetIssues.h"

@implementation RequestToGetIssues

- (NSString *)path
{
    return @"issues.json";
}

- (TRCRequestMethod)method
{
    return TRCRequestMethodGet;
}

- (id)responseProcessedFromBody:(NSDictionary *)bodyObject headers:(NSDictionary *)responseHeaders status:(TRCHttpStatusCode)statusCode error:(NSError **)parseError
{
    return bodyObject[@"issues"];
}

@end

Note, that in result of our refactoring, bodyObject[@"issues"] will return NSArray of Issue objects. TyphoonRestClient automatically converts dictionaries into Issue objects using TRCObjectMapper.

7. Request parameters.

By checking redmine REST Api for issues: http://www.redmine.org/projects/redmine/wiki/Rest_Issues we can see, that it has special parameters for request, like pagination, project_id, etc

Let's implement some of them in our example. It's simple:

RequestToGetIssues.h

@interface RequestToGetIssues : NSObject <TRCRequest>

@property (nonatomic) NSRange range;
@property (nonatomic) NSNumber *projectId;

@end

RequestToGetIssues.m

@implementation RequestToGetIssues

- (NSString *)path
{
    return @"issues.json";
}

- (NSDictionary *)pathParameters
{
    return @{
        @"project_id": ValueOrNull(self.projectId),
        @"offset": @(self.range.location),
        @"limit": self.range.length > 0 ? @(self.range.length) : @10
    };
}
....

In that example pathParameters transformed into GET url arguemnts. Note that ValueOrNull macros inserts value itself or [NSNull null] if value is nil. TyphoonRestClient always skip NSNull values to make your code clean and avoid that checking on your side.

Another example of pathParameters usage is dynamic part of URL path, like this redmine API:

GET /issues/[id].[format]

Of course, we can compose that string in path method of TRCRequest, but much cleaner to do that with pathParameters:

- (NSString *)path
{
    return @"issues/{issue_id}.json";
}

- (NSDictionary *)pathParameters
{
    NSParameterAssert(_issueId);
    return @{ @"issue_id" : _issueId };
}

TRC replaces any arguments inside curly braces in path with values from pathParameters. You also can mix GET parameters with path arguments, in that case TRC inserts path arguments first, then use other as GET parameters.

Demo

You can get sourcecode of this Demo here