forked from royratcliffe/ActiveResourceKit
/
ARService+Private.m
275 lines (250 loc) · 10.5 KB
/
ARService+Private.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
// ActiveResourceKit ARService+Private.m
//
// Copyright © 2011, 2012, Roy Ratcliffe, Pioneering Software, United Kingdom
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EITHER
// EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO
// EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
// OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//
//------------------------------------------------------------------------------
#import "ARService+Private.h"
#import "ARConnection.h"
#import <ActiveResourceKit/ActiveResourceKit.h>
#import <ActiveModelKit/ActiveModelKit.h>
#import <ActiveSupportKit/ActiveSupportKit.h>
NSString *const ARFromKey = @"from";
NSString *const ARParamsKey = @"params";
NSString *ARQueryStringForOptions(NSDictionary *options)
{
return options == nil || [options count] == 0 ? @"" : [NSString stringWithFormat:@"?%@", [options toQueryWithNamespace:nil]];
}
@implementation ARService(Private)
- (id<ARFormat>)defaultFormat
{
return [ARJSONFormat JSONFormat];
}
- (NSString *)defaultElementName
{
return [[[AMName alloc] initWithClass:[self class]] element];
}
- (NSString *)defaultCollectionName
{
return [[ASInflector defaultInflector] pluralize:[self elementNameLazily]];
}
- (NSString *)defaultPrimaryKey
{
return @"id";
}
- (NSString *)defaultPrefixSource
{
// Asking for the site path answers only the non-base path as the path, even
// if the URL comprises relative path components based on a base URL which
// itself includes a path. The base URL's path disappears from the
// equation. Is this a bug in Foundation? Work around it by computing the
// prefix source through concatenation of the two paths.
NSURL *site = [self site];
NSURL *baseURL = [site baseURL];
NSString *path = [site path];
return baseURL ? [[baseURL path] stringByAppendingPathComponent:path] : path;
}
- (void)findEveryWithOptions:(NSDictionary *)options completionHandler:(void (^)(ARHTTPResponse *response, NSArray *resources, NSError *error))completionHandler
{
NSString *path;
NSDictionary *prefixOptions;
NSString *from = [options objectForKey:ARFromKey];
if (from && [from isKindOfClass:[NSString class]])
{
prefixOptions = nil;
path = [NSString stringWithFormat:@"%@%@", from, ARQueryStringForOptions([options objectForKey:ARParamsKey])];
}
else
{
NSDictionary *queryOptions = nil;
[self splitOptions:options prefixOptions:&prefixOptions queryOptions:&queryOptions];
path = [self collectionPathWithPrefixOptions:prefixOptions queryOptions:queryOptions];
}
[self get:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
if ([object isKindOfClass:[NSArray class]])
{
completionHandler(response, [self instantiateCollection:object prefixOptions:prefixOptions], nil);
}
else
{
completionHandler(response, nil, [NSError errorWithDomain:ARErrorDomain code:ARUnsupportedRootObjectTypeError userInfo:nil]);
}
}];
}
- (NSArray *)instantiateCollection:(NSArray *)collection prefixOptions:(NSDictionary *)prefixOptions
{
NSMutableArray *resources = [NSMutableArray array];
for (NSDictionary *attributes in collection)
{
[resources addObject:[self instantiateRecordWithAttributes:attributes prefixOptions:prefixOptions]];
}
return [resources copy];
}
- (ARResource *)instantiateRecordWithAttributes:(NSDictionary *)attributes prefixOptions:(NSDictionary *)prefixOptions
{
Class aClass = NSClassFromString(ASInflectorCamelize([self elementNameLazily], YES));
if (aClass == nil || ![aClass isSubclassOfClass:[ARResource class]])
{
aClass = [ARResource class];
}
ARResource *resource = [[aClass alloc] initWithService:self attributes:attributes persisted:YES];
[resource setPrefixOptions:prefixOptions];
return resource;
}
- (NSSet *)prefixParameters
{
NSMutableSet *parameters = [NSMutableSet set];
NSString *prefixSource = [self prefixSourceLazily];
for (NSTextCheckingResult *result in [[NSRegularExpression regularExpressionWithPattern:@":\\w+" options:0 error:NULL] matchesInString:prefixSource options:0 range:NSMakeRange(0, [prefixSource length])])
{
[parameters addObject:[[prefixSource substringWithRange:[result range]] substringFromIndex:1]];
}
return [parameters copy];
}
- (void)splitOptions:(NSDictionary *)options prefixOptions:(NSDictionary **)outPrefixOptions queryOptions:(NSDictionary **)outQueryOptions
{
NSSet *prefixParameters = [self prefixParameters];
NSMutableDictionary *prefixOptions = [NSMutableDictionary dictionary];
NSMutableDictionary *queryOptions = [NSMutableDictionary dictionary];
[options enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
[[prefixParameters member:key] != nil ? prefixOptions : queryOptions setObject:obj forKey:key];
}];
if (outPrefixOptions)
{
*outPrefixOptions = [prefixOptions copy];
}
if (outQueryOptions)
{
*outQueryOptions = [queryOptions copy];
}
}
//------------------------------------------------------------------------------
#pragma mark HTTP Requests
//------------------------------------------------------------------------------
// Under Rails, the HTTP request methods belong to a separate connection class,
// ActiveResource::Connection. That class acts as a shim in-between active
// resources and the Net::HTTP library, handling authentication and encryption
// if the connection requires such features. The Objective-C implementation
// obviates the connection class by delegating to the underlying Apple
// NSURLConnection implementation for the handling of authentication and
// encryption.
- (void)get:(NSString *)path completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
[self requestHTTPMethod:ARHTTPGetMethod path:path body:nil completionHandler:completionHandler];
}
- (void)delete:(NSString *)path completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
[self requestHTTPMethod:ARHTTPDeleteMethod path:path body:nil completionHandler:completionHandler];
}
- (void)put:(NSString *)path body:(NSData *)data completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
[self requestHTTPMethod:ARHTTPPutMethod path:path body:data completionHandler:completionHandler];
}
- (void)post:(NSString *)path body:(NSData *)data completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
[self requestHTTPMethod:ARHTTPPostMethod path:path body:data completionHandler:completionHandler];
}
- (void)head:(NSString *)path completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
[self requestHTTPMethod:ARHTTPHeadMethod path:path body:nil completionHandler:completionHandler];
}
- (void)requestHTTPMethod:(NSString *)HTTPMethod path:(NSString *)path body:(NSData *)data completionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
ARConnection *connection = [self connectionLazily];
NSMutableURLRequest *request = [connection requestForHTTPMethod:HTTPMethod path:path headers:[[self headers] copy]];
if (data)
{
[request setHTTPBody:data];
}
[connection sendRequest:request completionHandler:[self decodeHandlerWithCompletionHandler:completionHandler]];
}
- (ARConnectionCompletionHandler)decodeHandlerWithCompletionHandler:(ARServiceRequestCompletionHandler)completionHandler
{
// This response handler exists for two main purposes: (1) to cast the
// generalised URL response to a protocol-specific HTTP response; (2) to
// decode the body. Purpose number two presumes that all responses arriving
// here require format-specific decoding. If this is not true, do not use
// this handler. Instead, roll your own.
//
// Sends -copy to the block thereby transferring the block from the local
// stack to the heap. Copy for use after the destruction of this
// scope. Compiling for iOS raises a warning unless you copy the block, but
// not so for OS X.
return [^(ARHTTPResponse *response, NSError *error) {
if (response)
{
if ([response body])
{
error = [ARConnection errorForResponse:response];
if (error == nil)
{
// What if the response body proves empty? This happens for
// responses to DELETE requests. Never attempt to decode a
// zero-length data body. Zero-length data is valid for some
// operations. It does not necessarily signal an
// error. Instead, pass the result up through to the
// higher-level software layers as success. Let higher-level
// software make a decision about validity.
//
// Rails answers a single space for DELETE responses, in
// fact for all responses where your action controller uses
// the "head" method. Explicitly, the action controller head
// method assigns a single space for the response
// body. Unfortunately, this fails to successfully
// decode. White space does not correctly decode a JSON
// object. Instead it produces an error. Work around is
// issue by trimming white space.
NSData *data = [response body];
NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (string)
{
data = [[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] dataUsingEncoding:NSUTF8StringEncoding];
}
id object = data && [data length] ? [[self formatLazily] decode:data error:&error] : nil;
if (error == nil)
{
completionHandler(response, object, nil);
}
else
{
// decoding error
completionHandler(response, nil, error);
}
}
else
{
// response error
completionHandler(response, nil, error);
}
}
else
{
// connection error
completionHandler(response, nil, error);
}
}
else
{
// type error
completionHandler(nil, nil, error ? error : [NSError errorWithDomain:ARErrorDomain code:ARResponseIsNotHTTPError userInfo:nil]);
}
} copy];
}
@end