#CSMapper
In the world of mapping data to our data model, the data may change at any time, and CSMapper is the simplest solution to solve this problem. As an extremely lightweight mapping framework, CSMapper provides the flexibility for an ever changing development environment by mapping dictionaries of KVO compliant objects, to KVO compliant objects via simple plist or JSON configuration files.
#Features
- Flexible and lightweight
- Maps KVO compliant objects to
NSDictionary
objects, via plist / JSON configuration - Supports default values for missing properties
- Supports mapping inheritance
- Flexible runtime transformations
- Compound property mappings
#Basic Use
The basic concept behind CSMapper is a three steps :
- Define your model class
- Create a plist or JSON file with the same name as the model class
- Define model property to
NSDictionary
property mappings in the plist.
If custom parse time values need to be generated based on multiple NSDictionary
return values, mappers can be used to transform multiple values into a single value via CSMapper
protocol.
Let's look at a basic example below with a class definition, a NSDictionary
response, and a plist mapping file associated with the class.
Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, strong) NSNumber *height;
@end
NSDictionary
{
'person_name' : 'nameValue',
'person_age' : 28,
'person_height: 54
}
Person.plist
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<key>age</key>
<dict>
<key>key</key>
<string>person_age</string>
</dict>
<key>height</key>
<dict>
<key>key</key>
<string>person_height</string>
</dict>
</plist>
Or, alternatively:
Person.json
{
"name": {
"key": "person_name"
},
"age": {
"key": "person_age"
},
"height": {
"key": "person_height"
}
}
Result
Once the response is received it's as easy as the following line of code to map all the values accordingly to the Person
model class.
Person *newPersonInstance = [[Person alloc] init];
[newPersonInstance mapAttributesWithDictionary:dictionaryResponse];
Let's pretend that in the previous example, the NSDictionary
did not contain a value for the person_height. Once the result gets parsed, the resulting Person
instance would receiver a nil
value for the height property. CSMapper allows the developer to define default values for specific per property by setting the default key within the plist mapping.
NSDictionary
{
'person_name' : 'nameValue',
'person_age' : 28
}
Person.plist
Notice that we added the default key for the height
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<key>age</key>
<dict>
<key>key</key>
<string>person_age</string>
</dict>
<key>height</key>
<dict>
<key>key</key>
<string>person_height</string>
<key>default</key>
<string>10</string>
</dict>
</plist>
Result
Once the result is parsed, since we set the default value in the mapping, the resulting Person
instance would receiver an NSNumber
typed value of 10 for the height property. This is encouraged for all properties as of now, CSMapper does not detect the receiver of the data. So a UUID can be a string or an integer in a bit case.
While the single mapping is great for simple use cases, inheritance is always something that comes to mind with these kind of things.
CSMapper solves this problem by specifying a special key, called __parent__ which is either an NSString
or an NSArray
of Strings.
What this does is, it takes the Parent mapping and applies it before the actual Mapping takes place.
###Single Inheritance
####Example Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSNumber *age;
@end
Programmer.h
@interface Programmer : Person
@property (nonatomic, strong) NSString *programmingSkills;
@end
Person.plist
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<key>age</key>
<dict>
<key>key</key>
<string>person_age</string>
</dict>
</plist>
Programmer.plist
Notice the __parent__ key definition for the Person
class.
<plist version="1.0">
<key>__parent__</key>
<string>Person</string>
<key>programmingSkills</key>
<dict>
<key>key</key>
<string>programming_skills</string>
</plist>
Result
The Programmer object would map the properties as follows:
Programmer:
name -> person_name
age -> person_age
programmingSkills -> programming_skills
###Multiple Inheritance ####Example
Now, if __parent__ key in the property is an array, multiple inheritance is used.
Resource.h
@interface Resource : NSObject
@property (nonatomic, strong) NSString *name;
@end
Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSNumber *age;
@end
Programmer.h
@interface Programmer : Person
@property (nonatomic, strong) NSString *programmingSkills;
@end
Resource.plist
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>resource_name</string>
</dict>
</plist>
Person.plist
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<key>age</key>
<dict>
<key>key</key>
<string>person_age</string>
</dict>
</plist>
Programmer.plist
<plist version="1.0">
<key>__parent__</key>
<array>
<string>Person</string>
<string>Resource</string>
</array>
<key>programmingSkills</key>
<dict>
<key>key</key>
<string>programming_skills</string>
</plist>
Result
In this case, the order matters in which the parents appear, hence an Array property in the plist, not an unordered Set. Notice in this example that the order of the __parent__ key mapping contains Person
at the first index, then Resource
at the second index. Resource takes precedence here which implies that the name property value on the object gets the value of resource_name.
Here comes a tricky part though, internally, both get set. That means, that CSMapper sets the value for name to the NSDictionaty
value of person_name and then to the value of resource_name. If resource_name is not found in the dictionary, the value of name is preserved because nil values don't get set at the moment. This is a short coming of this approach that might change.
So, the mapping for the Programmer object will look like this:
Programmer:
yearsService -> years_service
name -> person_name # If resource_name does not exists
name -> resource_name # If resource_name exists,
else it will remain the person_name value if it exists
age -> person_age
programmingSkills -> programming_skills
By default the CSMapper is capable of detecting and mapping native datatypes such as NSString
, NSDate
, NSNumber
, NSDictionary
, and NSArray
without explicit plist configuration on the fly, yet allows the developer to override them explicitely, even newly defined to custom datatypes. Please refer to the Mappers section below in the case a BOOL
value is to be mapped.
Sometimes there is reason to store a returned NSNumber value as a string, and CSMapper allows you that flexibility. By defining the type property within the mappings we can override how the object is going to be store in our model. Let's look at a simple example.
Person.h
@interface Person : NSObject
@property (nonatomic, strong) NSString *age;
@end
NSDictionary
Notice the age property in the NSDictionary
is returned as an NSNumber
, but we want to store it as an NSString
.
{
'person_age' : 28
}
Person.plist
By defining the type key for the age property to NSString
, CSMapper will explicitely converted it while mapping.
<plist version="1.0">
<key>age</key>
<dict>
<key>key</key>
<string>person_age</string>
<key>type</key>
<string>NSString</string>
</dict>
</plist>
Result
As simple as that, CSMapper will map the results as an NSString
to the Person
object.
###Custom Types
Sometimes we defined a custom class as a property of a class. And there is a chance that you may receive a NSDictionary
which contains a dictionary for the custom custom object object property defined in your model. CSMapper gives us the flexibility to use type property within the mappings to directly map sub dictionaries to your model class. Let's look at an example
####Example
ContactInfo.h
@interface ContactInfo : NSObject
@property (nonatomic, strong) NSString *phoneNumber;
@end
Person.h
Notice the Person
class contains contactInfo of type ContactInfo
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) ContactInfo *contactInfo;
@end
NSDictionary
The NSDictionary
returned an NSDictionary
named contact_info, which should be stored as a ContactInfo
class type.
{
'person_name' : 'Bob',
'contact_info' :
{
'phone_number' : '12345678900'
}
}
ContactInfo.plist
<plist version="1.0">
<key>phoneNumber</key>
<dict>
<key>key</key>
<string>phone_number</string>
</dict>
</plist>
Person.plist
Observe the type key below for the contactInfo for the Person
is set to ContactInfo
type.
<plist version="1.0">
<key>name</key>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<key>contactInfo</key>
<dict>
<key>key</key>
<string>contact_info</string>
<key>type</key>
<string>ContactInfo</string>
</dict>
</plist>
Result
As simple as that, after mapping the attributes, the Person
object will have contactInfo set to a mapped object of type ContactInfo
.
##Mappers
When class properties need to pre-processing, or tranformation, from a single, or multiple NSDictionary
values into a custom value for storage. There are many usecases for pre-processing:
- Dealing with dates, which can be returned in many different formats from the server
- Setting binary flags on the model object based on string values returned from the server
- Appending multiple string values together based on the responce to preprocessing values displayed, (i.e scrolling table view cell with label text comprised of 3 attributes)
CSMapper gives you the ability to create an class that abides by the CSMapper
protocol to transform, return, and map a value on the fly
###Single Value Transform
Let's look at an example that can be applied to an NSDate
with a single value being transformed to a specific format returned form the server.
####Example
Person.h
@interface Employee : NSObject
@property (nonatomic, strong) NSDate *hireDate;
@end
NSDictionary
{
"hire_date": "2013-11-01T07:20:20-07:00"
}
Employee.plist
Notice that we use the mapper key to define the object that will be transforming our NSDate
<plist version="1.0">
<key>hireDate</key>
<dict>
<key>key</key>
<string>hire_date</string>
<key>mapper</key>
<string>APIDateMapper</string>
</dict>
</plist>
APIDateMapper.h
Class conforms to the CSMapper
protocol
#import <Foundation/Foundation.h>
#import "CSMapper.h"
@interface APIDateMapper : NSObject <CSMapper>
@end
APIDateMapper.m
This class creates a static instance for a and NSDateFormatter
in memory, and transforms the NSDate
value accordingly, then returns an NSDate
value. This is handy as a single point of formatting for an NSDate
returned by the server, which can modified with ease if the response changes.
#import "APIDateMapper.h"
@implementation APIDateMapper
static NSDateFormatter *dateFormatter = nil;
+ (id)transformValue:(id)inputValue {
if (dateFormatter == nil) {
dateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
dateFormatter.locale = enUSPOSIXLocale;
dateFormatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
dateFormatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:sszzzz";
}
NSDate *retval;
NSError *error;
[dateFormatter getObjectValue:&retval forString:inputValue range:nil error:&error];
return retval;
}
@end
Result
In this example, by setting the mapper key in the plist for the hireDate, CSMapper will automatically call the transformValue:(id)inputValue
method, and assign the returned value to the hireDate property of the Person
instance.
###Multi Value Transform
When there is a need to string multiple values together for display purposes or pre-processing for a specific property. By creating a class that abides by the CSMapper
protocol we can pass multiple values for a transformation.
####Example
For this example, imagine we are scrolling through a list, and the display data displays name, age, and hire date, in short form, as a single sting. If we were to attempt to create the display text while scrolling, this could cause unneccesary overhead on the device as each value would need to be processed for each cell in the list as it is about to be displayed.
Person.h
Notice the Person
class contains an NSString
that represents this custom value that we need to transform from the returned NSDictionary
@interface Person : NSObject
@property (nonatomic, strong) NSString *metaDisplayString;
@end
NSDictionary` Response
{
'person_name': 'nameValue',
'person_age' : 28,
"hire_date": "2013-11-01T07:20:20-07:00"
}
Employee.plist
Notice that we use the mapper key to define an array of objects that will undergo transformation.
<plist version="1.0">
<key>metaDisplayString</key>
<dict>
<key>mapper</key>
<string>APIMetaStringMapper</string>
<key>key</key>
<array>
<dict>
<key>key</key>
<string>person_name</string>
</dict>
<dict>
<key>key</key>
<string>person_age</string>
</dict>
<dict>
<key>key</key>
<string>ratings</string>
</dict>
</array>
</dict>
</plist>
APIMetaStringMapper.h
Class conforms to the CSMapper
protocol
#import <Foundation/Foundation.h>
#import "CSMapper.h"
@interface APIMetaStringMapper : NSObject <CSMapper>
@end
APIMetaStringMapper.m
This class receives an array as the inputValue. It is key to understand that the order of the inputValue array maps specifically to the order of the keys defined in the plist.
#import "APIMetaStringMapper.h"
@implementation APIMetaStringMapper
static NSDateFormatter *dateFormatter = nil;
+ (id)transformValue:(id)inputValue {
if (dateFormatter == nil) {
dateFormatter = [NSDateFormatter new];
dateFormatter.timeStyle = NSDateFormatterShortStyle;
dateFormatter.dateStyle = NSDateFormatterNoStyle;
}
NSMutableString *returnString = [[NSMutableString alloc] init];
NSString *slash = @" / ";
for (id object in inputValue) {
if ([object isEqual:[inputValue lastObject]]) {
[returnString appendString:[dateFormatter stringFromDate:object]];
} else {
[returnString appendString:object];
[returnString appendString:slash];
}
}
return returnString;
}
@end
Result
As simple as that, will transform the three inputvalues into a single string and assign it to the metaDisplayString property.
###Compound Attributes
When a compound attribute value is applicable, may it be a compound identifier, or comparable example, CSMapper provides a mapper class CSJoinMapper
which allows you to do just that. In the following scenario, let us pretend we need a compount value of the
####Example
Person.h
Notice the Person
class contains an NSString
typed compoundIdentifier property.
@interface Person : NSObject
@property (nonatomic, strong) NSString *compoundIdentifier;
@end
NSDictionary` Response
{
'employee_identifier': '123456789',
'employee_age' : 28
}
Employee.plist
Notice that we use the mapper key to defines an array of objects that will undergoe compound attribute transformation.
<plist version="1.0">
<key>compoundIdentifier</key>
<dict>
<key>mapper</key>
<string>CSJoinMapper</string>
<key>key</key>
<array>
<dict>
<key>key</key>
<string>employee_identifier</string>
</dict>
<dict>
<key>key</key>
<string>employee_age</string>
</dict>
</array>
</dict>
</plist>
Result
The resulting value for the compoundIdentifier will be "123456789:28"
###Boolean Attributes
As an API developer we generally can run into many different boolean value responses:
- on
- 1
- true
- TRUE
When mapping a boolean value for an object, apply the CSAPIBoolMapper
just as you would in the Single Transform Example above.
When a mapping contains a property called "groups", of type array, the property is only mapped when the requested groups are contained in the mapping groups. One exception to this is if the "groups" parameter is empty or not set.
Person.json
{
"name": {
"key": "person_name",
"groups": [
"list"
]
},
"age": {
"key": "person_age",
"groups": [
"detail",
"list"
]
},
"address": {
"key": "address",
"groups": [
"detail"
]
},
"height": {
"key": "person_height"
}
}
Person *newPersonInstance = [[Person alloc] init];
[newPersonInstance mapAttributesWithDictionary:dictionaryResponse groups:@[@"list"]];
In this instance, name
, age
& height
get mapped since the group list
is contained in name
and age
. height
doesn't define any groups.
Person *newPersonInstance = [[Person alloc] init];
[newPersonInstance mapAttributesWithDictionary:dictionaryResponse groups:@[@"list", @"detail"]];
In this instance, age
& height
get mapped since both groups list
& detail
are contained in age
. height
doesn't define any groups.
The project includes XCTestCases
Edit your podfile
edit Podfile
pod 'CSMapper', :git => 'https://github.com/marcammann/CSMapper.git'
Now you can install CSMapper
pod install
#import <NSObject+CSMapper.h>
Learn more at CocoaPods.
- MacOS X 10.7 +
- iOS 5.0 +