-
Notifications
You must be signed in to change notification settings - Fork 5
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.
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;
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
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.
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
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}"
...
}
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.
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.
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.
You can get sourcecode of this Demo here