ActiveResourceKit  v1.2 (498.0)
 All Classes Files Functions Variables Typedefs Enumerator Properties Macros Pages
ARService.m
Go to the documentation of this file.
1 // ActiveResourceKit ARService.m
2 //
3 // Copyright © 2011, 2012, Roy Ratcliffe, Pioneering Software, United Kingdom
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the “Software”), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included in
13 // all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED “AS IS,” WITHOUT WARRANTY OF ANY KIND, EITHER
16 // EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO
18 // EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
19 // OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
20 // ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 // DEALINGS IN THE SOFTWARE.
22 //
23 //------------------------------------------------------------------------------
24 
25 #import "ARService.h"
26 #import "ARService+Private.h"
27 
28 #import "ARURLConnection.h"
29 #import "ARHTTPResponse.h"
30 #import "ARResource.h"
31 #import "ARErrors.h"
32 
33 #import <ActiveSupportKit/ActiveSupportKit.h>
34 
36 
37 @implementation ARService
38 
39 // Should this method exist at class-scope or instance-scope?
41 {
43  {
45  }
47 }
48 
49 + (void)setDefaultConnectionClass:(Class)aClass
50 {
52 }
53 
54 // designated initialiser
55 - (id)init
56 {
57  self = [super init];
58  if (self)
59  {
60  [self setTimeout:60.0];
61  }
62  return self;
63 }
64 
65 // The following initialisers are not designated initialisers. Note the messages
66 // to -[self init] rather than -[super init], a small but important
67 // difference. These are just convenience initialisers: a way to initialise and
68 // assign the site URL at one and the same time, or site plus element name.
69 
70 - (id)initWithSite:(NSURL *)site
71 {
72  self = [self init];
73  if (self)
74  {
75  [self setSite:site];
76  }
77  return self;
78 }
79 
80 - (id)initWithSite:(NSURL *)site elementName:(NSString *)elementName
81 {
82  self = [self initWithSite:site];
83  if (self)
84  {
85  [self setElementName:elementName];
86  }
87  return self;
88 }
89 
90 - (ARService *)serviceForSubelementNamed:(NSString *)elementName
91 {
92  ARService *service = [[ARService alloc] initWithSite:[self siteWithPrefixParameter] elementName:elementName];
93  if (_connection)
94  {
95  [service setConnection:[[[_connection class] alloc] init]];
96  }
97  return service;
98 }
99 
100 //------------------------------------------------------------------------------
101 #pragma mark Schema and Known Attributes
102 //------------------------------------------------------------------------------
103 
104 @synthesize schema = _schema;
105 
106 - (NSArray *)knownAttributes
107 {
108  return [[self schema] allKeys];
109 }
110 
111 //------------------------------------------------------------------------------
112 #pragma mark Site
113 //------------------------------------------------------------------------------
114 
115 @synthesize site = _site;
116 
118 {
119  return [NSURL URLWithString:[NSString stringWithFormat:@"%@/:%@", [self collectionNameLazily], [self foreignKey]] relativeToURL:[self site]];
120 }
121 
122 //------------------------------------------------------------------------------
123 #pragma mark Format
124 //------------------------------------------------------------------------------
125 
126 @synthesize format = _format;
127 
128 // lazy getter
130 {
131  id<ARFormat> format = [self format];
132  if (format == nil)
133  {
134  [self setFormat:format = [self defaultFormat]];
135  }
136  return format;
137 }
138 
139 //------------------------------------------------------------------------------
140 #pragma mark Timeout
141 //------------------------------------------------------------------------------
142 
143 @synthesize timeout = _timeout;
144 
145 //------------------------------------------------------------------------------
146 #pragma mark Connection
147 //------------------------------------------------------------------------------
148 
149 // Lazily constructs a connection using the default connection class.
151 {
152  if (_connection == nil)
153  {
154  [self setConnection:[[[[self class] defaultConnectionClass] alloc] init]];
155  }
156  return _connection;
157 }
158 
159 - (void)setConnection:(ARConnection *)connection
160 {
161  [connection setSite:[self site]];
162  [connection setFormat:[self formatLazily]];
163  [connection setTimeout:[self timeout]];
164  _connection = connection;
165 }
166 
167 //------------------------------------------------------------------------------
168 #pragma mark Headers
169 //------------------------------------------------------------------------------
170 
171 @synthesize headers = _headers;
172 
173 - (NSMutableDictionary *)headersLazily
174 {
175  NSMutableDictionary *headers = [self headers];
176  if (headers == nil)
177  {
178  [self setHeaders:headers = [NSMutableDictionary dictionary]];
179  }
180  return headers;
181 }
182 
183 //------------------------------------------------------------------------------
184 #pragma mark Element and Collection Names
185 //------------------------------------------------------------------------------
186 
187 @synthesize elementName = _elementName;
188 
189 @synthesize collectionName = _collectionName;
190 
191 // lazy getter
192 - (NSString *)elementNameLazily
193 {
194  NSString *elementName = [self elementName];
195  if (elementName == nil)
196  {
197  [self setElementName:elementName = [self defaultElementName]];
198  }
199  return elementName;
200 }
201 
202 // lazy getter
203 - (NSString *)collectionNameLazily
204 {
205  NSString *collectionName = [self collectionName];
206  if (collectionName == nil)
207  {
208  [self setCollectionName:collectionName = [self defaultCollectionName]];
209  }
210  return collectionName;
211 }
212 
213 //------------------------------------------------------------------------------
214 #pragma mark Primary and Foreign Key
215 //------------------------------------------------------------------------------
216 
217 @synthesize primaryKey = _primaryKey;
218 
219 - (NSString *)primaryKeyLazily
220 {
221  NSString *primaryKey = [self primaryKey];
222  if (primaryKey == nil)
223  {
224  [self setPrimaryKey:primaryKey = [self defaultPrimaryKey]];
225  }
226  return primaryKey;
227 }
228 
229 - (NSString *)foreignKey
230 {
231  return [[ASInflector defaultInflector] foreignKey:[self elementNameLazily] separateClassNameAndIDWithUnderscore:YES];
232 }
233 
234 //------------------------------------------------------------------------------
235 #pragma mark Prefix
236 //------------------------------------------------------------------------------
237 
238 @synthesize prefixSource = _prefixSource;
239 
240 // lazy getter
241 - (NSString *)prefixSourceLazily
242 {
243  NSString *prefixSource = [self prefixSource];
244  if (prefixSource == nil)
245  {
247 
248  // Automatically append a trailing slash, but if and only if the prefix
249  // source does not already terminate with a slash.
250  if ([prefixSource length] == 0 || ![[prefixSource substringFromIndex:[prefixSource length] - 1] isEqualToString:@"/"])
251  {
252  prefixSource = [prefixSource stringByAppendingString:@"/"];
253  }
254 
255  [self setPrefixSource:prefixSource];
256  }
257  return prefixSource;
258 }
259 
260 - (NSString *)prefixWithOptions:(NSDictionary *)options
261 {
262  // The following implementation duplicates some of the functionality
263  // concerning extraction of prefix parameters from the prefix. See the
264  // -prefixParameters method. Nevertheless, the replace-in-place approach
265  // makes the string operations more convenient. The implementation does not
266  // need to cut apart the colon from its parameter word. The regular
267  // expression identifies the substitution on our behalf, making it easier to
268  // remove the colon, access the prefix parameter minus its colon and replace
269  // both; and all at the same time.
270  if (options == nil)
271  {
272  return [self prefixSourceLazily];
273  }
274  return [[NSRegularExpression regularExpressionWithPattern:@":(\\w+)" options:0 error:NULL] stringByReplacingMatchesInString:[self prefixSourceLazily] replacementStringForResult:^NSString *(NSTextCheckingResult *result, NSString *inString, NSInteger offset) {
275  return [[[options objectForKey:[[result regularExpression] replacementStringForResult:result inString:inString offset:offset template:@"$1"]] description] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
276  }];
277 }
278 
279 //------------------------------------------------------------------------------
280 #pragma mark Paths
281 //------------------------------------------------------------------------------
282 
283 - (NSString *)elementPathForID:(NSNumber *)ID prefixOptions:(NSDictionary *)prefixOptions queryOptions:(NSDictionary *)queryOptions
284 {
285  if (queryOptions == nil)
286  {
287  [self splitOptions:prefixOptions prefixOptions:&prefixOptions queryOptions:&queryOptions];
288  }
289  NSString *IDString = [[ID stringValue] stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
290  return [NSString stringWithFormat:@"%@%@/%@.%@%@", [self prefixWithOptions:prefixOptions], [self collectionNameLazily], IDString, [[self formatLazily] extension], ARQueryStringForOptions(queryOptions)];
291 }
292 
293 // Answers the path for creating a new element. Note, the term “new” appearing
294 // at the start of the method name does not, in this case, signify a retained
295 // result.
296 - (NSString *)newElementPathWithPrefixOptions:(NSDictionary *)prefixOptions
297 {
298  return [NSString stringWithFormat:@"%@%@/new.%@", [self prefixWithOptions:prefixOptions], [self collectionNameLazily], [[self formatLazily] extension]];
299 }
300 
301 - (NSString *)collectionPathWithPrefixOptions:(NSDictionary *)prefixOptions queryOptions:(NSDictionary *)queryOptions
302 {
303  if (queryOptions == nil)
304  {
305  [self splitOptions:prefixOptions prefixOptions:&prefixOptions queryOptions:&queryOptions];
306  }
307  return [NSString stringWithFormat:@"%@%@.%@%@", [self prefixWithOptions:prefixOptions], [self collectionNameLazily], [[self formatLazily] extension], ARQueryStringForOptions(queryOptions)];
308 }
309 
310 //------------------------------------------------------------------------------
311 #pragma mark RESTful Services
312 //------------------------------------------------------------------------------
313 
314 // Building with attributes. Should this be a class or instance method? Rails
315 // implements this as a class method, or to be more specific, a singleton
316 // method. Objective-C does not provide the singleton class
317 // paradigm. ActiveResourceKit folds the Rails singleton methods to instance
318 // methods.
319 - (void)buildWithAttributes:(NSDictionary *)attributes completionHandler:(ARResourceCompletionHandler)completionHandler
320 {
321  // Use the new element path. Construct the request URL using this path but
322  // make it relative to the site URL. The NSURL class combines the new
323  // element path with the site, using the site's scheme, host and port.
324  NSString *path = [self newElementPathWithPrefixOptions:nil];
325  // The response body decodes successfully but it does not// decode to a dictionary. It must be something else, either// an array, a string or some other primitive type. In which// case, building with attributes must fail even though// ostensibly the operation has succeeded. Set up an error.[self get:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
326  if (object && error == nil)
327  {
328  if ([object isKindOfClass:[NSDictionary class]])
329  {
330  NSMutableDictionary *attrs = [NSMutableDictionary dictionaryWithDictionary:object];
331  [attrs addEntriesFromDictionary:attributes];
332  completionHandler(response, [[ARResource alloc] initWithService:self attributes:attrs], nil);
333  }
334  else
335  {
336 
337 
338 
339 
340 
341  completionHandler(response, nil, [NSError errorWithDomain:ARErrorDomain code:ARUnsupportedRootObjectTypeError userInfo:nil]);
342  }
343  }
344  else
345  {
346  completionHandler(response, object, error);
347  }
348  }];
349 }
350 
351 - (void)createWithAttributes:(NSDictionary *)attributes completionHandler:(ARResourceCompletionHandler)completionHandler
352 {
353  ARResource *resource = [[ARResource alloc] initWithService:self attributes:attributes];
354  [resource saveWithCompletionHandler:^(ARHTTPResponse *response, NSError *error) {
355  completionHandler(response, error == nil ? resource : nil, error);
356  }];
357 }
358 
359 - (void)findAllWithOptions:(NSDictionary *)options completionHandler:(ARResourcesCompletionHandler)completionHandler
360 {
361  return [self findEveryWithOptions:options completionHandler:completionHandler];
362 }
363 
364 - (void)findFirstWithOptions:(NSDictionary *)options completionHandler:(ARResourceCompletionHandler)completionHandler
365 {
366  return [self findEveryWithOptions:options completionHandler:^(ARHTTPResponse *response, NSArray *resources, NSError *error) {
367  completionHandler(response, resources && [resources count] ? [resources objectAtIndex:0] : nil, error);
368  }];
369 }
370 
371 - (void)findLastWithOptions:(NSDictionary *)options completionHandler:(ARResourceCompletionHandler)completionHandler
372 {
373  return [self findEveryWithOptions:options completionHandler:^(ARHTTPResponse *response, NSArray *resources, NSError *error) {
374  completionHandler(response, resources && [resources count] ? [resources lastObject] : nil, error);
375  }];
376 }
377 
378 - (void)findSingleWithID:(NSNumber *)ID options:(NSDictionary *)options completionHandler:(ARResourceCompletionHandler)completionHandler
379 {
380  NSDictionary *prefixOptions = nil;
381  NSDictionary *queryOptions = nil;
382  [self splitOptions:options prefixOptions:&prefixOptions queryOptions:&queryOptions];
383  NSString *path = [self elementPathForID:ID prefixOptions:prefixOptions queryOptions:queryOptions];
384  [self get:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
385  if (object && error == nil)
386  {
387  if ([object isKindOfClass:[NSDictionary class]])
388  {
389  completionHandler(response, [self instantiateRecordWithAttributes:object prefixOptions:prefixOptions], nil);
390  }
391  else
392  {
393  completionHandler(response, nil, [NSError errorWithDomain:ARErrorDomain code:ARUnsupportedRootObjectTypeError userInfo:nil]);
394  }
395  }
396  else
397  {
398  completionHandler(response, object, error);
399  }
400  }];
401 }
402 
403 - (void)findOneWithOptions:(NSDictionary *)options completionHandler:(ARResourceCompletionHandler)completionHandler
404 {
405  NSString *from = [options objectForKey:ARFromKey];
406  if (from && [from isKindOfClass:[NSString class]])
407  {
408  NSString *path = [NSString stringWithFormat:@"%@%@", from, ARQueryStringForOptions([options objectForKey:ARParamsKey])];
409  [self get:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
410  if (object && error == nil)
411  {
412  if ([object isKindOfClass:[NSDictionary class]])
413  {
414  completionHandler(response, [self instantiateRecordWithAttributes:object prefixOptions:nil], nil);
415  }
416  else
417  {
418  completionHandler(response, nil, [NSError errorWithDomain:ARErrorDomain code:ARUnsupportedRootObjectTypeError userInfo:nil]);
419  }
420  }
421  else
422  {
423  completionHandler(response, object, error);
424  }
425  }];
426  }
427 }
428 
429 - (void)deleteWithID:(NSNumber *)ID options:(NSDictionary *)options completionHandler:(void (^)(ARHTTPResponse *response, NSError *error))completionHandler
430 {
431  NSString *path = [self elementPathForID:ID prefixOptions:options queryOptions:nil];
432  [self delete:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
433  completionHandler(response, error);
434  }];
435 }
436 
437 - (void)existsWithID:(NSNumber *)ID options:(NSDictionary *)options completionHandler:(void (^)(ARHTTPResponse *response, BOOL exists, NSError *error))completionHandler
438 {
439  // This implementation looks a little strange. Why would you pass an ID of
440  // nil? However, it fairly accurately mirrors the Rails implementation, to
441  // the extent possible at least. ID is nil when the resource is new.
442  if (ID)
443  {
444  NSDictionary *prefixOptions = nil;
445  NSDictionary *queryOptions = nil;
446  [self splitOptions:options prefixOptions:&prefixOptions queryOptions:&queryOptions];
447  NSString *path = [self elementPathForID:ID prefixOptions:prefixOptions queryOptions:queryOptions];
448  [self head:path completionHandler:^(ARHTTPResponse *response, id object, NSError *error) {
449  completionHandler(response, [response code] == 200, error);
450  }];
451  }
452  else
453  {
454  completionHandler(nil, NO, nil);
455  }
456 }
457 
458 @end