From 07796df25322fcfb1b3ed8480e5c0266cd08ac8d Mon Sep 17 00:00:00 2001 From: Blake Watters Date: Fri, 21 Jan 2011 00:15:02 -0500 Subject: [PATCH] Refactored the Rails router to respect properties specified by the model objects. Introduced new support to allow models to modify RESTful object loaders just before they are sent. Sample app completely works. Really fucking happy with how this is turning out! --- Code/CoreData/RKManagedObject.m | 10 +---- Code/ObjectMapping/RKDynamicRouter.m | 2 +- Code/ObjectMapping/RKObject.m | 10 +---- Code/ObjectMapping/RKObjectLoader.m | 19 ++++++++++ Code/ObjectMapping/RKObjectManager.m | 5 +-- Code/ObjectMapping/RKObjectMappable.h | 38 +++++++++++++++---- Code/ObjectMapping/RKObjectMappable.m | 37 ++++++++++++++++++ Code/ObjectMapping/RKRailsRouter.m | 15 +------- .../Controllers/DBPostTableViewController.m | 4 +- .../DiscussionBoard/Code/Models/DBPost.m | 30 +++++++-------- RestKit.xcodeproj/project.pbxproj | 4 ++ Specs/RKObjectSpec.m | 4 +- 12 files changed, 117 insertions(+), 61 deletions(-) create mode 100644 Code/ObjectMapping/RKObjectMappable.m diff --git a/Code/CoreData/RKManagedObject.m b/Code/CoreData/RKManagedObject.m index 0e4bbf1c..bad308e8 100644 --- a/Code/CoreData/RKManagedObject.m +++ b/Code/CoreData/RKManagedObject.m @@ -157,14 +157,8 @@ return [self valueForKey:[[self class] primaryKeyProperty]]; } -- (NSObject*)paramsForSerialization { - NSMutableDictionary* params = [NSMutableDictionary dictionary]; - for (NSString* elementName in [[self class] elementToPropertyMappings]) { - NSString* propertyName = [[[self class] elementToPropertyMappings] objectForKey:elementName]; - [params setValue:[self valueForKey:propertyName] forKey:elementName]; - } - - return [NSDictionary dictionaryWithDictionary:params]; +- (NSDictionary*)propertiesForSerialization { + return RKObjectMappableGetPropertiesByElement(self); } - (BOOL)isNew { diff --git a/Code/ObjectMapping/RKDynamicRouter.m b/Code/ObjectMapping/RKDynamicRouter.m index 1b935642..1aa50c62 100644 --- a/Code/ObjectMapping/RKDynamicRouter.m +++ b/Code/ObjectMapping/RKDynamicRouter.m @@ -95,7 +95,7 @@ - (NSObject*)serializationForObject:(NSObject*)object method:(RKRequestMethod)method { // By default return a form encoded serializable dictionary - return [object paramsForSerialization]; + return [object propertiesForSerialization]; } @end diff --git a/Code/ObjectMapping/RKObject.m b/Code/ObjectMapping/RKObject.m index 3602283c..c99ed2a6 100644 --- a/Code/ObjectMapping/RKObject.m +++ b/Code/ObjectMapping/RKObject.m @@ -23,14 +23,8 @@ return [[self new] autorelease]; } -- (NSObject*)paramsForSerialization { - NSMutableDictionary* params = [NSMutableDictionary dictionary]; - for (NSString* elementName in [[self class] elementToPropertyMappings]) { - NSString* propertyName = [[[self class] elementToPropertyMappings] objectForKey:elementName]; - [params setValue:[self valueForKey:propertyName] forKey:elementName]; - } - - return [NSDictionary dictionaryWithDictionary:params]; +- (NSDictionary*)propertiesForSerialization { + return RKObjectMappableGetPropertiesByElement(self); } @end diff --git a/Code/ObjectMapping/RKObjectLoader.m b/Code/ObjectMapping/RKObjectLoader.m index 5f45ebef..3169c26c 100644 --- a/Code/ObjectMapping/RKObjectLoader.m +++ b/Code/ObjectMapping/RKObjectLoader.m @@ -289,4 +289,23 @@ } } +// Give the target object a chance to modify the request +- (void)triggerWillSendForTargetObject { + if (self.targetObject) { + if ([self.targetObject respondsToSelector:@selector(willSendWithObjectLoader:)]) { + [self.targetObject willSendWithObjectLoader:self]; + } + } +} + +- (void)send { + [self triggerWillSendForTargetObject]; + [super send]; +} + +- (RKResponse*)sendSynchronously { + [self triggerWillSendForTargetObject]; + return [super sendSynchronously]; +} + @end diff --git a/Code/ObjectMapping/RKObjectManager.m b/Code/ObjectMapping/RKObjectManager.m index b7146e00..cf125e20 100644 --- a/Code/ObjectMapping/RKObjectManager.m +++ b/Code/ObjectMapping/RKObjectManager.m @@ -194,10 +194,7 @@ static RKObjectManager* sharedManager = nil; return loader; } -// TODO: Need to factor core data stuff out of here... -// TODO: Use notifications for this??? -// RKObjectManagerWillGETObject / RKObjectManagerWillPOSTObject / RKObjectManagerWillPUTObject / RKObjectManagerWillDELETEObject -// RKObjectManagerDidGETObject / RKObjectManagerDidPOSTObject / RKObjectManagerDidPUTObject / RKObjectManagerDidDELETEObject +// TODO: Need to factor core data stuff out of here... Use notifications (probably at RKObjectLoader level) to trigger save of the object store - (void)saveObjectStore { if (self.objectStore) { NSError* error = [self.objectStore save]; diff --git a/Code/ObjectMapping/RKObjectMappable.h b/Code/ObjectMapping/RKObjectMappable.h index b4fef810..a6616548 100644 --- a/Code/ObjectMapping/RKObjectMappable.h +++ b/Code/ObjectMapping/RKObjectMappable.h @@ -8,6 +8,7 @@ */ @protocol RKRequestSerializable; +@class RKObjectLoader; /** * Must be implemented by all classes utilizing the RKModelMapper to map REST @@ -40,17 +41,40 @@ @optional -/** - * Return a dictionary of values to be serialized for submission to a remote resource. The router - * will encode these parameters into a serialization format (form encoded, JSON, etc). This is - * required to use putObject: and postObject: for updating and creating remote object representations. - */ -- (NSObject*)paramsForSerialization; - /** * Must return a new autoreleased instance of the model class ready for mapping. Used to initialize the model * via any method other than alloc & init. */ + (id)object; +/** + * Return a dictionary of values to be serialized for submission to a remote resource. The router + * will encode these parameters into a serialization format (form encoded, JSON, etc). This is + * required to use putObject: and postObject: for updating and creating remote object representations. + */ +- (NSDictionary*)propertiesForSerialization; + +/** + * Invoked before the mappable object is sent with an Object Loader. This + * can be used to completely customize the behavior of an object loader at the + * model level before sending the request. Note that this is invoked after the + * router has processed and just before the object loader is sent. + * + * If you want to customize the behavior of the parameters sent with the request + * this is the right place to do so. + */ +- (void)willSendWithObjectLoader:(RKObjectLoader*)objectLoader; + @end + +/** + * Returns a dictionary containing all the mappable properties + * and their values for a given mappable object. + */ +NSDictionary* RKObjectMappableGetProperties(NSObject*object); + +/** + * Returns a dictionary containing all the mappable properties + * and their values keyed by the element name. + */ +NSDictionary* RKObjectMappableGetPropertiesByElement(NSObject*object); diff --git a/Code/ObjectMapping/RKObjectMappable.m b/Code/ObjectMapping/RKObjectMappable.m new file mode 100644 index 00000000..4b56c4f4 --- /dev/null +++ b/Code/ObjectMapping/RKObjectMappable.m @@ -0,0 +1,37 @@ +// +// RKObjectMappable.m +// RestKit +// +// Created by Blake Watters on 1/20/11. +// Copyright 2011 Two Toasters. All rights reserved. +// + +#import "RKObjectMappable.h" + +// Return all the mapped properties of object in a dictionary +NSDictionary* RKObjectMappableGetProperties(NSObject*object) { + NSDictionary* mappings = [[object class] elementToPropertyMappings]; + NSMutableDictionary* propertyNamesAndValues = [NSMutableDictionary dictionaryWithCapacity:[mappings count]]; + // Return all the properties of this model in a dictionary under their element names + for (NSString* elementName in mappings) { + NSString* propertyName = [mappings valueForKey:elementName]; + id propertyValue = [object valueForKey:propertyName]; + [propertyNamesAndValues setValue:propertyValue forKey:propertyName]; + } + + return [NSDictionary dictionaryWithDictionary:propertyNamesAndValues]; +} + +// Return all the mapped properties of object in a dictionary under their element names +NSDictionary* RKObjectMappableGetPropertiesByElement(NSObject*object) { + NSDictionary* mappings = [[object class] elementToPropertyMappings]; + NSMutableDictionary* elementsAndPropertyValues = [NSMutableDictionary dictionaryWithCapacity:[mappings count]]; + + for (NSString* elementName in mappings) { + NSString* propertyName = [mappings valueForKey:elementName]; + id propertyValue = [object valueForKey:propertyName]; + [elementsAndPropertyValues setValue:propertyValue forKey:elementName]; + } + + return [NSDictionary dictionaryWithDictionary:elementsAndPropertyValues]; +} diff --git a/Code/ObjectMapping/RKRailsRouter.m b/Code/ObjectMapping/RKRailsRouter.m index 0278fdf2..6c0a2a65 100644 --- a/Code/ObjectMapping/RKRailsRouter.m +++ b/Code/ObjectMapping/RKRailsRouter.m @@ -28,19 +28,6 @@ [_classToModelMappings setObject:modelName forKey:class]; } -- (NSDictionary*)elementNamesAndPropertyValuesForObject:(NSObject*)object { - NSDictionary* mappings = [[object class] elementToPropertyMappings]; - NSMutableDictionary* elementsAndPropertyValues = [NSMutableDictionary dictionaryWithCapacity:[mappings count]]; - // Return all the properties of this model in a dictionary under their element names - for (NSString* elementName in mappings) { - NSString* propertyName = [mappings valueForKey:elementName]; - id propertyValue = [object valueForKey:propertyName]; - [elementsAndPropertyValues setValue:propertyValue forKey:elementName]; - } - - return (NSDictionary*) elementsAndPropertyValues; -} - #pragma mark RKRouter - (NSObject*)serializationForObject:(NSObject*)object method:(RKRequestMethod)method { @@ -49,7 +36,7 @@ return nil; } - NSDictionary* elementsAndProperties = [self elementNamesAndPropertyValuesForObject:object]; + NSDictionary* elementsAndProperties = [object propertiesForSerialization]; NSMutableDictionary* resourceParams = [NSMutableDictionary dictionaryWithCapacity:[elementsAndProperties count]]; NSString* modelName = [_classToModelMappings objectForKey:[object class]]; if (nil == modelName) { diff --git a/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Controllers/DBPostTableViewController.m b/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Controllers/DBPostTableViewController.m index e2eb5c9e..06f06416 100644 --- a/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Controllers/DBPostTableViewController.m +++ b/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Controllers/DBPostTableViewController.m @@ -86,8 +86,8 @@ } } else { [items addObject:[TTTableLongTextItem itemWithText:_post.body]]; - NSString* url = _post.attachmentPath; - [items addObject:[TTTableImageItem itemWithText:@"" imageURL:url URL:nil]]; + NSString* imageURL = _post.attachmentPath; + [items addObject:[TTTableImageItem itemWithText:@"" imageURL:imageURL URL:nil]]; } if ([self.post isNewRecord]) { diff --git a/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Models/DBPost.m b/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Models/DBPost.m index 528bed52..203fef95 100644 --- a/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Models/DBPost.m +++ b/Examples/RKDiscussionBoardExample/DiscussionBoard/Code/Models/DBPost.m @@ -75,29 +75,29 @@ } /** - * Return a serializable representation of this object's properties. This - * serialization will be encoded by the router into a request body and - * sent to the remote service. - * - * A default implementation of paramsForSerialization is provided by the - * RKObject/RKManagedObject base classes, but can be overloaded in the subclass - * for customization. This is useful for including things like transient properties - * in your payloads. + * Invoked just before this Post is sent in an object loader request via + * getObject, postObject, putObject or deleteObject. Here we can manipulate + * the request at will. + * + * The router only has the ability to work with simple dictionaries, so to + * support uploading the attachment we are going to supply our own params + * for the request. */ -- (NSObject*)paramsForSerialization { - // TODO: This is broken! - // TODO: The Rails router does not respect paramsForSerialization. Need to fix that! +- (void)willSendWithObjectLoader:(RKObjectLoader *)objectLoader { RKParams* params = [RKParams params]; - [params setValue:self.body forParam:@"body"]; + + // NOTE - Since we have side-stepped the router, we need to + // nest the param names under the model name ourselves + [params setValue:self.body forParam:@"post[body]"]; NSLog(@"Self Body: %@", self.body); if (_newAttachment) { NSData* data = UIImagePNGRepresentation(_newAttachment); NSLog(@"Data Size: %d", [data length]); - RKParamsAttachment* attachment = [params setData:data MIMEType:@"application/octet-stream" forParam:@"attachment"]; + RKParamsAttachment* attachment = [params setData:data MIMEType:@"application/octet-stream" forParam:@"post[attachment]"]; attachment.fileName = @"image.png"; } - - return params; + + objectLoader.params = params; } - (BOOL)hasAttachment { diff --git a/RestKit.xcodeproj/project.pbxproj b/RestKit.xcodeproj/project.pbxproj index 95067972..f9565b74 100644 --- a/RestKit.xcodeproj/project.pbxproj +++ b/RestKit.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 253A0933125525F100976E89 /* RKRequestFilterableTTModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A089F12551D8D00976E89 /* RKRequestFilterableTTModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 253A09E612552B5300976E89 /* ObjectMapping.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A09E512552B5300976E89 /* ObjectMapping.h */; settings = {ATTRIBUTES = (Public, ); }; }; 253A09F612552BDC00976E89 /* Support.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A09F512552BDC00976E89 /* Support.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 253E1B1112E9450700F3E4B0 /* RKObjectMappable.m in Sources */ = {isa = PBXBuildFile; fileRef = 253E1B1012E9450700F3E4B0 /* RKObjectMappable.m */; }; 25431EBB1255640800A315CF /* CoreData.h in Headers */ = {isa = PBXBuildFile; fileRef = 25431EBA1255640800A315CF /* CoreData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2543201C1256179900A315CF /* RKObjectSeeder.h in Headers */ = {isa = PBXBuildFile; fileRef = 253A088812551D8D00976E89 /* RKObjectSeeder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2543201D1256179900A315CF /* RKObjectSeeder.m in Sources */ = {isa = PBXBuildFile; fileRef = 253A088912551D8D00976E89 /* RKObjectSeeder.m */; }; @@ -481,6 +482,7 @@ 253A09E512552B5300976E89 /* ObjectMapping.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjectMapping.h; sourceTree = ""; }; 253A09F512552BDC00976E89 /* Support.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Support.h; sourceTree = ""; }; 253A0A8E1255300000976E89 /* Protect.command */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Protect.command; sourceTree = ""; }; + 253E1B1012E9450700F3E4B0 /* RKObjectMappable.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKObjectMappable.m; sourceTree = ""; }; 25431EBA1255640800A315CF /* CoreData.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = CoreData.h; sourceTree = ""; }; 25432040125618F000A315CF /* RKParser.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RKParser.h; sourceTree = ""; }; 255DE03010FF9BDF00A85891 /* RKManagedObjectSpec.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RKManagedObjectSpec.m; sourceTree = ""; }; @@ -880,6 +882,7 @@ 259562E3126D3B36004BAC4C /* RKDynamicRouter.m */, 259562E6126D3B43004BAC4C /* RKRailsRouter.h */, 259562E7126D3B43004BAC4C /* RKRailsRouter.m */, + 253E1B1012E9450700F3E4B0 /* RKObjectMappable.m */, ); path = ObjectMapping; sourceTree = ""; @@ -1700,6 +1703,7 @@ 253A09011255246900976E89 /* RKObjectPropertyInspector.m in Sources */, 259562E5126D3B36004BAC4C /* RKDynamicRouter.m in Sources */, 259562E9126D3B43004BAC4C /* RKRailsRouter.m in Sources */, + 253E1B1112E9450700F3E4B0 /* RKObjectMappable.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Specs/RKObjectSpec.m b/Specs/RKObjectSpec.m index cacb44d6..63a95369 100644 --- a/Specs/RKObjectSpec.m +++ b/Specs/RKObjectSpec.m @@ -33,7 +33,7 @@ nil]; } -- (void)itShouldReturnTheColorParamsForSerialization { +- (void)itShouldReturnTheColorPropertiesForSerialization { self.age = [NSNumber numberWithInt:10]; self.favoriteColor = @"blue"; @@ -41,7 +41,7 @@ @"myFavoriteColor", @"blue", @"myAge", [NSNumber numberWithInt:10], nil]; - [expectThat([self paramsForSerialization]) should:be(expectedParams)]; + [expectThat([self propertiesForSerialization]) should:be(expectedParams)]; } @end