diff --git a/Documentation/Annotations.md b/Documentation/Annotations.md new file mode 100644 index 0000000..3b2b00d --- /dev/null +++ b/Documentation/Annotations.md @@ -0,0 +1,131 @@ +# Annotations + +Entities, attributes and relationships in a managed object model have an associated **user info dictionary** in which you can specify custom metadata as key-value pairs. + +Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as **annotations**. + +You can use the **Data Model inspector** in Xcode to annotate entities, attributes and relationships: + +Data Model inspector + +This document lists all the different keys you can use to annotate models, and the purpose of each. + +## Property annotations + +### `JSONKeyPath` + +Using this key you can specify how your managed object’s properties (that is, attributes and relationships) map to key paths in a JSON object. + +For example, consider this JSON modelling a famous comic book character: + +```json +{ + "id": "1699", + "name": "Batman", + "publisher": { + "id": "10", + "name": "DC Comics" + } +} +``` + +We could model this in Core Data using two related entities: `Character` and `Publisher`. + +The `Character` entity could have `identifier` and `name` attributes, and a `publisher` to-one relationship. + +The `Publisher` entity could have `identifier` and `name` attributes, and a `characters` to-many relationship. + +Each of these properties should have a `JSONKeyPath` entry in their corresponding user info dictionary: + +* `id` for the `identifier` attribute, +* `name` for the `name` attribute, +* `publisher` for the `publisher` relationship, +* etc. + +Attributes and relationships that don't have a `JSONKeyPath` entry are **not considered** for JSON serialization or deserialization. + +Note that if we were only interested in the publisher's name, we could drop the `Publisher` entity and add a `publisherName` attribute specifying `publisher.name` as the `JSONKeyPath`. + +### `JSONTransformerName` + +With this key you can specify the name of a value transformer that will be used to transform values when serializing from or into JSON. + +Consider the `id` key in the previous JSON. Some web APIs send 64-bit integers as strings to support languages that have trouble consuming large integers. + +We should store identifier values as integers instead of strings to save space. + +First we need to change the `identifier` attribute's type to `Integer 64` in both the `Character` and `Publisher` entities. + +Then we add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary with the name of the value transformer: `StringToInteger`. + +Finally we create the value transformer and give it the name we just used: + +```objc +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +} reverseTransformBlock:^id(NSNumber *value) { + return [value stringValue]; +}]; +``` + +If we were not interested in serializing characters back into JSON we could omit the reverse transformation: + +```objc +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +}]; +``` + +## Entity annotations + +### `identityAttribute` + +Use this key to specify the name of the attribute that uniquely identifies instances of an entity. + +In our example, we should add an `identityAttribute` entry to both the `Character` and `Publisher` entities user dictionaries with the value `identifier`. + +Specifying the `identityAttribute` in an entity is essential to preserve the object graph and avoid duplicate information when serializing from JSON. + +Note that specifying multiple attributes for this annotation is not currently supported. + +### `entityMapperName` + +If your model uses entity inheritance, use this key in the base entity to specify an entity mapper name. + +An entity mapper is used to determine which sub-entity is used when deserializing an object from JSON. + +For example, consider the following JSON: + +```json +{ + "messages": [ + { + "id": 1, + "type": "text", + "text": "Hello there!" + }, + { + "id": 2, + "type": "picture", + "image_url": "http://example.com/risitas.jpg" + } + ] +} +``` + +We could model this in Core Data using an abstract base entity `Message` and two concrete sub-entities `TextMessage` and `PictureMessage`. + +Then we need to add an `entityMapperName` entry to the `Message` entity's user info dictionary: `MessageMapper`. + +Finally we create the entity mapper and give it the name we just used: + +```objc +[NSValueTransformer grt_setEntityMapperWithName:@"MessageMapper" mapBlock:^NSString *(NSDictionary *JSONDictionary) { + NSDictionary *entityMapping = @{ + @"text": @"TextMessage", + @"picture": @"PictureMessage" + }; + NSString *type = JSONDictionary[@"type"]; + return entityMapping[type]; +}]; +``` diff --git a/Documentation/README.md b/Documentation/README.md new file mode 100644 index 0000000..16828fe --- /dev/null +++ b/Documentation/README.md @@ -0,0 +1 @@ +This folder contains extended documentation for using Groot. \ No newline at end of file diff --git a/Groot.podspec b/Groot.podspec index 9e09b64..63842d6 100644 --- a/Groot.podspec +++ b/Groot.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "Groot" - s.version = "0.2" + s.version = "1.0" s.summary = "From JSON to Core Data and back." s.description = <<-DESC @@ -12,16 +12,27 @@ Pod::Spec.new do |s| s.author = { "Guillermo Gonzalez" => "gonzalezreal@icloud.com" } s.social_media_url = "https://twitter.com/gonzalezreal" - - s.ios.deployment_target = '6.0' - s.osx.deployment_target = '10.8' - + s.source = { :git => "https://github.com/gonzalezreal/Groot.git", :tag => s.version.to_s } - - s.source_files = "Groot/**/*.{h,m}" - s.private_header_files = "Groot/Private/*.h" - - s.frameworks = 'Foundation', 'CoreData' - + + s.default_subspec = "Swift" + + s.subspec "Swift" do |ss| + ss.ios.deployment_target = "7.0" + ss.osx.deployment_target = "10.9" + + ss.source_files = "Groot/**/*.{swift,h,m}" + ss.private_header_files = "Groot/Private/*.h" + end + + s.subspec "ObjC" do |ss| + ss.ios.deployment_target = "6.0" + ss.osx.deployment_target = "10.8" + + ss.source_files = "Groot/**/*.{h,m}" + ss.private_header_files = "Groot/Private/*.h" + end + + s.frameworks = "Foundation", "CoreData" s.requires_arc = true end diff --git a/Groot.xcodeproj/project.pbxproj b/Groot.xcodeproj/project.pbxproj index 00c139f..2ec823a 100644 --- a/Groot.xcodeproj/project.pbxproj +++ b/Groot.xcodeproj/project.pbxproj @@ -7,31 +7,78 @@ objects = { /* Begin PBXBuildFile section */ + B408736D1B5AB6930063F150 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B40873621B5AB6930063F150 /* Groot.framework */; }; + B408737B1B5AB8350063F150 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B408737C1B5AB83D0063F150 /* Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FD30701B569F1A002392F7 /* Groot.swift */; }; + B408737D1B5AB8470063F150 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B408737E1B5AB8470063F150 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; + B408737F1B5AB8470063F150 /* GRTJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873801B5AB8470063F150 /* GRTJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */; }; + B40873811B5AB8470063F150 /* GRTManagedStore.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873821B5AB8470063F150 /* GRTManagedStore.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */; }; + B40873831B5AB8470063F150 /* NSValueTransformer+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B40873841B5AB8470063F150 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; + B40873851B5AB8470063F150 /* NSValueTransformer+Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */; }; + B40873861B5AB87F0063F150 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */; }; + B40873871B5AB87F0063F150 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */; }; + B40873881B5AB87F0063F150 /* NSPropertyDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */; }; + B40873891B5AB87F0063F150 /* NSPropertyDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */; }; + B408738A1B5AB87F0063F150 /* NSAttributeDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */; }; + B408738B1B5AB87F0063F150 /* NSAttributeDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */; }; + B408738C1B5AB87F0063F150 /* NSEntityDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */; }; + B408738D1B5AB87F0063F150 /* NSEntityDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */; }; + B408738E1B5AB87F0063F150 /* NSManagedObject+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */; }; + B408738F1B5AB87F0063F150 /* NSManagedObject+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */; }; + B40873901B5AB89C0063F150 /* NSValueTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */; }; + B40873911B5AB8A20063F150 /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; + B40873921B5AB8A60063F150 /* GRTJSONSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */; }; + B40873931B5AB8AF0063F150 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; + B40873941B5AB8AF0063F150 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; + B40873951B5AB8AF0063F150 /* container.json in Resources */ = {isa = PBXBuildFile; fileRef = B409C72F1B5979E700C47D48 /* container.json */; }; + B408739A1B5AB8DC0063F150 /* GRTModels.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */; }; + B408739B1B5AB8E20063F150 /* NSData+Resource.m in Sources */ = {isa = PBXBuildFile; fileRef = B42A1D961B56614600309637 /* NSData+Resource.m */; }; + B408739C1B5AB8E80063F150 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */; }; + B409C7301B5979E700C47D48 /* container.json in Resources */ = {isa = PBXBuildFile; fileRef = B409C72F1B5979E700C47D48 /* container.json */; }; + B42A1D931B56600000309637 /* characters.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D911B56600000309637 /* characters.json */; }; + B42A1D941B56600000309637 /* characters_update.json in Resources */ = {isa = PBXBuildFile; fileRef = B42A1D921B56600000309637 /* characters_update.json */; }; + B42A1D971B56614600309637 /* NSData+Resource.m in Sources */ = {isa = PBXBuildFile; fileRef = B42A1D961B56614600309637 /* NSData+Resource.m */; }; + B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */ = {isa = PBXBuildFile; fileRef = B46200C01B4F07E4003B3B69 /* GRTError.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */ = {isa = PBXBuildFile; fileRef = B46200C11B4F07E4003B3B69 /* GRTError.m */; }; + B475B4101B53FCE1001F29FE /* NSManagedObject+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */; }; + B475B4111B53FCE1001F29FE /* NSManagedObject+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */; }; + B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */; }; B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1AF11AA9CA5E00F67403 /* Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1AF81AA9CA5E00F67403 /* Groot.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */; }; - B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B081AA9CAC200F67403 /* GRTConstants.h */; }; - B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B091AA9CAC200F67403 /* GRTConstants.m */; }; B4DC1B1B1AA9CAC200F67403 /* GRTJSONSerialization.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */; }; B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */; }; - B4DC1B1F1AA9CAC200F67403 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; - B4DC1B201AA9CAC200F67403 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */; }; B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */; }; B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */; }; - B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */; }; - B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */; }; B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */; }; B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */; }; B4DC1B271AA9CAC200F67403 /* NSPropertyDescription+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */; }; B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */; }; B4DC1B2F1AA9CBAD00F67403 /* GRTJSONSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */; }; B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */; }; - B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */; }; B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */; }; + B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */; }; + B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */; }; + B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */ = {isa = PBXBuildFile; fileRef = B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */; }; + B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */; }; + B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */; }; + B4FD30711B569F1A002392F7 /* Groot.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FD30701B569F1A002392F7 /* Groot.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + B408736E1B5AB6930063F150 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = B4DC1AE31AA9CA5E00F67403 /* Project object */; + proxyType = 1; + remoteGlobalIDString = B40873611B5AB6930063F150; + remoteInfo = "Groot-Mac"; + }; B4DC1AF91AA9CA5E00F67403 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = B4DC1AE31AA9CA5E00F67403 /* Project object */; @@ -42,23 +89,29 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + B40873621B5AB6930063F150 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B408736C1B5AB6930063F150 /* GrootTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GrootTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + B409C72F1B5979E700C47D48 /* container.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = container.json; sourceTree = ""; }; + B42A1D911B56600000309637 /* characters.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters.json; sourceTree = ""; }; + B42A1D921B56600000309637 /* characters_update.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = characters_update.json; sourceTree = ""; }; + B42A1D951B56614600309637 /* NSData+Resource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+Resource.h"; sourceTree = ""; }; + B42A1D961B56614600309637 /* NSData+Resource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+Resource.m"; sourceTree = ""; }; + B46200C01B4F07E4003B3B69 /* GRTError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTError.h; sourceTree = ""; }; + B46200C11B4F07E4003B3B69 /* GRTError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTError.m; sourceTree = ""; }; + B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSManagedObject+Groot.h"; sourceTree = ""; }; + B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSManagedObject+Groot.m"; sourceTree = ""; }; + B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStoreTests.m; sourceTree = ""; }; B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Groot.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AF01AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; B4DC1AF11AA9CA5E00F67403 /* Groot.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Groot.h; sourceTree = ""; }; B4DC1AF71AA9CA5E00F67403 /* GrootTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = GrootTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; B4DC1AFD1AA9CA5E00F67403 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B4DC1B081AA9CAC200F67403 /* GRTConstants.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTConstants.h; sourceTree = ""; }; - B4DC1B091AA9CAC200F67403 /* GRTConstants.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTConstants.m; sourceTree = ""; }; B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTJSONSerialization.h; sourceTree = ""; }; B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTJSONSerialization.m; sourceTree = ""; }; B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTManagedStore.h; sourceTree = ""; }; B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTManagedStore.m; sourceTree = ""; }; - B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTValueTransformer.h; sourceTree = ""; }; - B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformer.m; sourceTree = ""; }; B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributeDescription+Groot.h"; sourceTree = ""; }; B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSAttributeDescription+Groot.m"; sourceTree = ""; }; - B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDictionary+Groot.h"; sourceTree = ""; }; - B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDictionary+Groot.m"; sourceTree = ""; }; B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSEntityDescription+Groot.h"; sourceTree = ""; }; B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSEntityDescription+Groot.m"; sourceTree = ""; }; B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSPropertyDescription+Groot.h"; sourceTree = ""; }; @@ -66,11 +119,32 @@ B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTJSONSerializationTests.m; sourceTree = ""; }; B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTModels.h; sourceTree = ""; }; B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTModels.m; sourceTree = ""; }; - B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformerTests.m; sourceTree = ""; }; B4DC1B2E1AA9CBAD00F67403 /* Model.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Model.xcdatamodel; sourceTree = ""; }; + B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GRTValueTransformer.h; sourceTree = ""; }; + B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GRTValueTransformer.m; sourceTree = ""; }; + B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSValueTransformer+Groot.h"; sourceTree = ""; }; + B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSValueTransformer+Groot.m"; sourceTree = ""; }; + B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSValueTransformer+Groot.swift"; sourceTree = ""; }; + B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSValueTransformerTests.swift; sourceTree = ""; }; + B4FD30701B569F1A002392F7 /* Groot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Groot.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + B408735E1B5AB6930063F150 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B40873691B5AB6930063F150 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B408736D1B5AB6930063F150 /* Groot.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE81AA9CA5E00F67403 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -89,6 +163,16 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + B42A1D901B56600000309637 /* JSON */ = { + isa = PBXGroup; + children = ( + B42A1D911B56600000309637 /* characters.json */, + B42A1D921B56600000309637 /* characters_update.json */, + B409C72F1B5979E700C47D48 /* container.json */, + ); + path = JSON; + sourceTree = ""; + }; B4DC1AE21AA9CA5E00F67403 = { isa = PBXGroup; children = ( @@ -103,6 +187,8 @@ children = ( B4DC1AEC1AA9CA5E00F67403 /* Groot.framework */, B4DC1AF71AA9CA5E00F67403 /* GrootTests.xctest */, + B40873621B5AB6930063F150 /* Groot.framework */, + B408736C1B5AB6930063F150 /* GrootTests.xctest */, ); name = Products; sourceTree = ""; @@ -111,14 +197,16 @@ isa = PBXGroup; children = ( B4DC1AF11AA9CA5E00F67403 /* Groot.h */, - B4DC1B081AA9CAC200F67403 /* GRTConstants.h */, - B4DC1B091AA9CAC200F67403 /* GRTConstants.m */, + B4FD30701B569F1A002392F7 /* Groot.swift */, + B46200C01B4F07E4003B3B69 /* GRTError.h */, + B46200C11B4F07E4003B3B69 /* GRTError.m */, B4DC1B0A1AA9CAC200F67403 /* GRTJSONSerialization.h */, B4DC1B0B1AA9CAC200F67403 /* GRTJSONSerialization.m */, B4DC1B0C1AA9CAC200F67403 /* GRTManagedStore.h */, B4DC1B0D1AA9CAC200F67403 /* GRTManagedStore.m */, - B4DC1B0E1AA9CAC200F67403 /* GRTValueTransformer.h */, - B4DC1B0F1AA9CAC200F67403 /* GRTValueTransformer.m */, + B4E72F621B4DB9B300B9EA77 /* NSValueTransformer+Groot.h */, + B4E72F631B4DB9B300B9EA77 /* NSValueTransformer+Groot.m */, + B4E72F6C1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift */, B4DC1B101AA9CAC200F67403 /* Private */, B4DC1AEF1AA9CA5E00F67403 /* Supporting Files */, ); @@ -136,11 +224,10 @@ B4DC1AFB1AA9CA5E00F67403 /* GrootTests */ = { isa = PBXGroup; children = ( + B4E72F6E1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift */, + B4BB8A0C1B4D17B600EBAADA /* GRTManagedStoreTests.m */, B4DC1B291AA9CBAD00F67403 /* GRTJSONSerializationTests.m */, - B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, - B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, - B4DC1B2C1AA9CBAD00F67403 /* GRTValueTransformerTests.m */, - B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */, + B42A1D901B56600000309637 /* JSON */, B4DC1AFC1AA9CA5E00F67403 /* Supporting Files */, ); path = GrootTests; @@ -149,6 +236,11 @@ B4DC1AFC1AA9CA5E00F67403 /* Supporting Files */ = { isa = PBXGroup; children = ( + B4DC1B2A1AA9CBAD00F67403 /* GRTModels.h */, + B4DC1B2B1AA9CBAD00F67403 /* GRTModels.m */, + B42A1D951B56614600309637 /* NSData+Resource.h */, + B42A1D961B56614600309637 /* NSData+Resource.m */, + B4DC1B2D1AA9CBAD00F67403 /* Model.xcdatamodeld */, B4DC1AFD1AA9CA5E00F67403 /* Info.plist */, ); name = "Supporting Files"; @@ -157,14 +249,16 @@ B4DC1B101AA9CAC200F67403 /* Private */ = { isa = PBXGroup; children = ( + B4E72F5A1B4DB8EF00B9EA77 /* GRTValueTransformer.h */, + B4E72F5B1B4DB8EF00B9EA77 /* GRTValueTransformer.m */, + B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */, + B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */, B4DC1B111AA9CAC200F67403 /* NSAttributeDescription+Groot.h */, B4DC1B121AA9CAC200F67403 /* NSAttributeDescription+Groot.m */, - B4DC1B131AA9CAC200F67403 /* NSDictionary+Groot.h */, - B4DC1B141AA9CAC200F67403 /* NSDictionary+Groot.m */, B4DC1B151AA9CAC200F67403 /* NSEntityDescription+Groot.h */, B4DC1B161AA9CAC200F67403 /* NSEntityDescription+Groot.m */, - B4DC1B171AA9CAC200F67403 /* NSPropertyDescription+Groot.h */, - B4DC1B181AA9CAC200F67403 /* NSPropertyDescription+Groot.m */, + B475B40E1B53FCE1001F29FE /* NSManagedObject+Groot.h */, + B475B40F1B53FCE1001F29FE /* NSManagedObject+Groot.m */, ); path = Private; sourceTree = ""; @@ -172,18 +266,36 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + B408735F1B5AB6930063F150 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + B408738C1B5AB87F0063F150 /* NSEntityDescription+Groot.h in Headers */, + B408737B1B5AB8350063F150 /* Groot.h in Headers */, + B40873831B5AB8470063F150 /* NSValueTransformer+Groot.h in Headers */, + B408737D1B5AB8470063F150 /* GRTError.h in Headers */, + B408738A1B5AB87F0063F150 /* NSAttributeDescription+Groot.h in Headers */, + B40873881B5AB87F0063F150 /* NSPropertyDescription+Groot.h in Headers */, + B408738E1B5AB87F0063F150 /* NSManagedObject+Groot.h in Headers */, + B40873811B5AB8470063F150 /* GRTManagedStore.h in Headers */, + B40873861B5AB87F0063F150 /* GRTValueTransformer.h in Headers */, + B408737F1B5AB8470063F150 /* GRTJSONSerialization.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE91AA9CA5E00F67403 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( - B4DC1B231AA9CAC200F67403 /* NSDictionary+Groot.h in Headers */, + B46200C21B4F07E4003B3B69 /* GRTError.h in Headers */, + B4E72F641B4DB9B300B9EA77 /* NSValueTransformer+Groot.h in Headers */, B4DC1B251AA9CAC200F67403 /* NSEntityDescription+Groot.h in Headers */, - B4DC1B1F1AA9CAC200F67403 /* GRTValueTransformer.h in Headers */, - B4DC1B191AA9CAC200F67403 /* GRTConstants.h in Headers */, B4DC1B271AA9CAC200F67403 /* NSPropertyDescription+Groot.h in Headers */, B4DC1AF21AA9CA5E00F67403 /* Groot.h in Headers */, B4DC1B1D1AA9CAC200F67403 /* GRTManagedStore.h in Headers */, B4DC1B1B1AA9CAC200F67403 /* GRTJSONSerialization.h in Headers */, + B475B4101B53FCE1001F29FE /* NSManagedObject+Groot.h in Headers */, + B4E72F5C1B4DB8EF00B9EA77 /* GRTValueTransformer.h in Headers */, B4DC1B211AA9CAC200F67403 /* NSAttributeDescription+Groot.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -191,6 +303,42 @@ /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ + B40873611B5AB6930063F150 /* Groot-Mac */ = { + isa = PBXNativeTarget; + buildConfigurationList = B40873791B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-Mac" */; + buildPhases = ( + B408735D1B5AB6930063F150 /* Sources */, + B408735E1B5AB6930063F150 /* Frameworks */, + B408735F1B5AB6930063F150 /* Headers */, + B40873601B5AB6930063F150 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Groot-Mac"; + productName = "Groot-Mac"; + productReference = B40873621B5AB6930063F150 /* Groot.framework */; + productType = "com.apple.product-type.framework"; + }; + B408736B1B5AB6930063F150 /* Groot-MacTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = B408737A1B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-MacTests" */; + buildPhases = ( + B40873681B5AB6930063F150 /* Sources */, + B40873691B5AB6930063F150 /* Frameworks */, + B408736A1B5AB6930063F150 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + B408736F1B5AB6930063F150 /* PBXTargetDependency */, + ); + name = "Groot-MacTests"; + productName = "Groot-MacTests"; + productReference = B408736C1B5AB6930063F150 /* GrootTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; B4DC1AEB1AA9CA5E00F67403 /* Groot */ = { isa = PBXNativeTarget; buildConfigurationList = B4DC1B021AA9CA5E00F67403 /* Build configuration list for PBXNativeTarget "Groot" */; @@ -236,6 +384,12 @@ LastUpgradeCheck = 0610; ORGANIZATIONNAME = "Guillermo Gonzalez"; TargetAttributes = { + B40873611B5AB6930063F150 = { + CreatedOnToolsVersion = 6.4; + }; + B408736B1B5AB6930063F150 = { + CreatedOnToolsVersion = 6.4; + }; B4DC1AEB1AA9CA5E00F67403 = { CreatedOnToolsVersion = 6.1.1; }; @@ -258,11 +412,30 @@ targets = ( B4DC1AEB1AA9CA5E00F67403 /* Groot */, B4DC1AF61AA9CA5E00F67403 /* GrootTests */, + B40873611B5AB6930063F150 /* Groot-Mac */, + B408736B1B5AB6930063F150 /* Groot-MacTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + B40873601B5AB6930063F150 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B408736A1B5AB6930063F150 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B40873941B5AB8AF0063F150 /* characters_update.json in Resources */, + B40873951B5AB8AF0063F150 /* container.json in Resources */, + B40873931B5AB8AF0063F150 /* characters.json in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AEA1AA9CA5E00F67403 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -274,24 +447,61 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + B42A1D941B56600000309637 /* characters_update.json in Resources */, + B42A1D931B56600000309637 /* characters.json in Resources */, + B409C7301B5979E700C47D48 /* container.json in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + B408735D1B5AB6930063F150 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B408738D1B5AB87F0063F150 /* NSEntityDescription+Groot.m in Sources */, + B40873891B5AB87F0063F150 /* NSPropertyDescription+Groot.m in Sources */, + B40873851B5AB8470063F150 /* NSValueTransformer+Groot.swift in Sources */, + B40873801B5AB8470063F150 /* GRTJSONSerialization.m in Sources */, + B40873841B5AB8470063F150 /* NSValueTransformer+Groot.m in Sources */, + B408737E1B5AB8470063F150 /* GRTError.m in Sources */, + B408737C1B5AB83D0063F150 /* Groot.swift in Sources */, + B408738B1B5AB87F0063F150 /* NSAttributeDescription+Groot.m in Sources */, + B40873871B5AB87F0063F150 /* GRTValueTransformer.m in Sources */, + B408738F1B5AB87F0063F150 /* NSManagedObject+Groot.m in Sources */, + B40873821B5AB8470063F150 /* GRTManagedStore.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B40873681B5AB6930063F150 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + B40873901B5AB89C0063F150 /* NSValueTransformerTests.swift in Sources */, + B408739B1B5AB8E20063F150 /* NSData+Resource.m in Sources */, + B40873921B5AB8A60063F150 /* GRTJSONSerializationTests.m in Sources */, + B40873911B5AB8A20063F150 /* GRTManagedStoreTests.m in Sources */, + B408739A1B5AB8DC0063F150 /* GRTModels.m in Sources */, + B408739C1B5AB8E80063F150 /* Model.xcdatamodeld in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B4DC1AE71AA9CA5E00F67403 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B4E72F6D1B4DC4D500B9EA77 /* NSValueTransformer+Groot.swift in Sources */, B4DC1B281AA9CAC200F67403 /* NSPropertyDescription+Groot.m in Sources */, + B4FD30711B569F1A002392F7 /* Groot.swift in Sources */, B4DC1B261AA9CAC200F67403 /* NSEntityDescription+Groot.m in Sources */, + B4E72F651B4DB9B300B9EA77 /* NSValueTransformer+Groot.m in Sources */, B4DC1B1C1AA9CAC200F67403 /* GRTJSONSerialization.m in Sources */, B4DC1B221AA9CAC200F67403 /* NSAttributeDescription+Groot.m in Sources */, - B4DC1B201AA9CAC200F67403 /* GRTValueTransformer.m in Sources */, - B4DC1B1A1AA9CAC200F67403 /* GRTConstants.m in Sources */, - B4DC1B241AA9CAC200F67403 /* NSDictionary+Groot.m in Sources */, + B4E72F5D1B4DB8EF00B9EA77 /* GRTValueTransformer.m in Sources */, B4DC1B1E1AA9CAC200F67403 /* GRTManagedStore.m in Sources */, + B475B4111B53FCE1001F29FE /* NSManagedObject+Groot.m in Sources */, + B46200C31B4F07E4003B3B69 /* GRTError.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -299,16 +509,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B4E72F6F1B4DC7DA00B9EA77 /* NSValueTransformerTests.swift in Sources */, B4DC1B2F1AA9CBAD00F67403 /* GRTJSONSerializationTests.m in Sources */, - B4DC1B311AA9CBAD00F67403 /* GRTValueTransformerTests.m in Sources */, + B42A1D971B56614600309637 /* NSData+Resource.m in Sources */, B4DC1B321AA9CBAD00F67403 /* Model.xcdatamodeld in Sources */, B4DC1B301AA9CBAD00F67403 /* GRTModels.m in Sources */, + B4BB8A0D1B4D17B600EBAADA /* GRTManagedStoreTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + B408736F1B5AB6930063F150 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = B40873611B5AB6930063F150 /* Groot-Mac */; + targetProxy = B408736E1B5AB6930063F150 /* PBXContainerItemProxy */; + }; B4DC1AFA1AA9CA5E00F67403 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = B4DC1AEB1AA9CA5E00F67403 /* Groot */; @@ -317,6 +534,94 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + B40873751B5AB6930063F150 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = Groot/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_NAME = Groot; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + B40873761B5AB6930063F150 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + FRAMEWORK_VERSION = A; + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = Groot/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + PRODUCT_NAME = Groot; + SDKROOT = macosx; + SKIP_INSTALL = YES; + }; + name = Release; + }; + B40873771B5AB6930063F150 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = GrootTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_NAME = GrootTests; + SDKROOT = macosx; + }; + name = Debug; + }; + B40873781B5AB6930063F150 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + FRAMEWORK_SEARCH_PATHS = ( + "$(DEVELOPER_FRAMEWORKS_DIR)", + "$(inherited)", + ); + GCC_NO_COMMON_BLOCKS = YES; + INFOPLIST_FILE = GrootTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + PRODUCT_NAME = GrootTests; + SDKROOT = macosx; + }; + name = Release; + }; B4DC1B001AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -404,6 +709,7 @@ B4DC1B031AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -413,6 +719,7 @@ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -420,6 +727,7 @@ B4DC1B041AA9CA5E00F67403 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; @@ -436,6 +744,7 @@ B4DC1B061AA9CA5E00F67403 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", @@ -447,12 +756,14 @@ INFOPLIST_FILE = GrootTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; B4DC1B071AA9CA5E00F67403 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; FRAMEWORK_SEARCH_PATHS = ( "$(SDKROOT)/Developer/Library/Frameworks", "$(inherited)", @@ -466,6 +777,22 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + B40873791B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-Mac" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B40873751B5AB6930063F150 /* Debug */, + B40873761B5AB6930063F150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; + B408737A1B5AB6930063F150 /* Build configuration list for PBXNativeTarget "Groot-MacTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B40873771B5AB6930063F150 /* Debug */, + B40873781B5AB6930063F150 /* Release */, + ); + defaultConfigurationIsVisible = 0; + }; B4DC1AE61AA9CA5E00F67403 /* Build configuration list for PBXProject "Groot" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -482,6 +809,7 @@ B4DC1B041AA9CA5E00F67403 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; B4DC1B051AA9CA5E00F67403 /* Build configuration list for PBXNativeTarget "GrootTests" */ = { isa = XCConfigurationList; @@ -490,6 +818,7 @@ B4DC1B071AA9CA5E00F67403 /* Release */, ); defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; }; /* End XCConfigurationList section */ diff --git a/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme new file mode 100644 index 0000000..c0643d6 --- /dev/null +++ b/Groot.xcodeproj/xcshareddata/xcschemes/Groot-Mac.xcscheme @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Groot/Private/NSDictionary+Groot.h b/Groot/GRTError.h similarity index 82% rename from Groot/Private/NSDictionary+Groot.h rename to Groot/GRTError.h index 4d28543..673e17d 100644 --- a/Groot/Private/NSDictionary+Groot.h +++ b/Groot/GRTError.h @@ -1,6 +1,6 @@ -// NSDictionary+Groot.h +// GRTError.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,10 +22,10 @@ #import -@class NSAttributeDescription; +extern NSString * const GRTErrorDomain; -@interface NSDictionary (Groot) - -- (id)grt_valueForAttribute:(NSAttributeDescription *)attribute; - -@end +typedef NS_ENUM(NSInteger, GRTError) { + GRTErrorEntityNotFound, + GRTErrorInvalidJSONObject, + GRTErrorIdentityNotFound +}; diff --git a/Groot/GRTConstants.h b/Groot/GRTError.m similarity index 81% rename from Groot/GRTConstants.h rename to Groot/GRTError.m index 9b6b7d9..dc5c349 100644 --- a/Groot/GRTConstants.h +++ b/Groot/GRTError.m @@ -1,6 +1,6 @@ -// GRTConstants.h -// -// Copyright (c) 2014 Guillermo Gonzalez +// GRTError.m +// +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -8,10 +8,10 @@ // 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, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -20,8 +20,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import +#import "GRTError.h" -extern NSString * const GRTJSONKeyPathKey; -extern NSString * const GRTJSONTransformerNameKey; -extern NSString * const GRTIdentityAttributeKey; +NSString * const GRTErrorDomain = @"com.groot.error"; diff --git a/Groot/GRTJSONSerialization.h b/Groot/GRTJSONSerialization.h index 4d5ba41..d666e65 100644 --- a/Groot/GRTJSONSerialization.h +++ b/Groot/GRTJSONSerialization.h @@ -1,6 +1,6 @@ // GRTJSONSerialization.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,91 +22,49 @@ #import -extern NSString * const GRTJSONSerializationErrorDomain; -extern const NSInteger GRTJSONSerializationErrorInvalidJSONObject; +NS_ASSUME_NONNULL_BEGIN /** Converts JSON dictionaries and JSON arrays to and from Managed Objects. - - The serialization process can be customized by adding certain information to the user dictionary - available in Core Data entities, attributes and relationships: - - You can specify how an attribute or a relationship is mapped to JSON with the `JSONKeyPath` key. If - this key is not present, the attribute name will be used. If `JSONKeyPath` is associated with `@"null"` - or `NSNull` then the attribute or relationship will not participate in JSON serialization. - - Use `JSONTransformerName` to specify the name of the value transformer that will be used to convert - the JSON value for an attribute. If the specified transformer is reversible, it will also be used - to convert the attribute value back to JSON. - - The **merge** methods `mergeObjectForEntityName:fromJSONDictionary:inManagedObjectContext:error:` and - `mergeObjectsForEntityName:fromJSONArray:inManagedObjectContext:error:` need to know when a managed - object already exist when converting from JSON. You can specify which attribute makes an object unique - by using the `identityAttribute` option. Note that this option must be added to the **Entity** user - dictionary. - - Note that the user dictionary can be manipulated directly in the Core Data Model Editor. */ @interface GRTJSONSerialization : NSObject /** - Creates a managed object from a JSON dictionary. - - This method converts the specified JSON dictionary into a managed object of a given entity. + Creates or updates a set of managed objects from JSON data. @param entityName The name of an entity. - @param JSONDictionary A dictionary representing JSON data. This should match the format returned - by `NSJSONSerialization`. - @param context The context into which to insert the created managed object. - @param error If an error occurs, upon return contains an NSError object that describes the problem. - - @return A managed object, or `nil` if an error occurs. - */ -+ (id)insertObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; - -/** - Creates set of managed objects from a JSON array. - - This method converts the specified JSON array into a set of managed objects of a given entity. - - @param entityName The name of an entity. - @param JSONArray An array of dictionaries representing JSON data. This should match the format - returned by `NSJSONSerialization`. - @param context The context into which to insert the created managed object. + @param data A data object containing JSON data. + @param context The context into which to fetch or insert the managed objects. @param error If an error occurs, upon return contains an NSError object that describes the problem. @return An array of managed objects, or `nil` if an error occurs. */ -+ (NSArray *)insertObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONData:(NSData *)data + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Creates or updates a managed object from a JSON dictionary. - This method will perform a fetch request for an object matching the given JSON dictionary, using - the identity attribute specified in the entity's user info. If a match is found, then it will be - updated with the given JSON dictionary, otherwise a new object will be created. - - Note that this method will throw an exception if the given entity has no identity attribute defined. + This method converts the specified JSON dictionary into a managed object of a given entity. @param entityName The name of an entity. @param JSONDictionary A dictionary representing JSON data. This should match the format returned - by `NSJSONSerialization`. - @param context The context into which to fetch or insert the managed object. + by `NSJSONSerialization`. + @param context The context into which to fetch or insert the managed objects. @param error If an error occurs, upon return contains an NSError object that describes the problem. @return A managed object, or `nil` if an error occurs. */ -+ (id)mergeObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable id)objectWithEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Creates or updates a set of managed objects from a JSON array. - This method will perform a **single** fetch request for objects matching the given JSON array, - using the identity attribute specified in the entity's user info. Matching objects will be updated, - and the rest will be created. - - Note that this method will throw an exception if the given entity has no identity attribute defined. - @param entityName The name of an entity. @param JSONArray An array representing JSON data. This should match the format returned by `NSJSONSerialization`. @@ -115,24 +73,57 @@ extern const NSInteger GRTJSONSerializationErrorInvalidJSONObject; @return An array of managed objects, or `nil` if an error occurs. */ -+ (NSArray *)mergeObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError **)error; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; /** Converts a managed object into a JSON representation. - @param managedObject The managed object to use for JSON serialization. - + @param object The managed object to use for JSON serialization. + @return A JSON dictionary. */ -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject; ++ (NSDictionary *)JSONDictionaryFromObject:(NSManagedObject *)object; /** Converts an array of managed objects into a JSON representation. - @param managedObjects The array of managed objects to use for JSON serialization. + @param objects The array of managed objects to use for JSON serialization. @return A JSON array. */ -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects; ++ (NSArray *)JSONArrayFromObjects:(NSArray *)objects; + +@end + +@interface GRTJSONSerialization (Deprecated) + ++ (nullable id)insertObjectForEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectWithEntityName:fromJSONDictionary:inContext:error:"))); + ++ (nullable NSArray *)insertObjectsForEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectsWithEntityName:fromJSONArray:inContext:error:"))); + ++ (nullable id)mergeObjectForEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectWithEntityName:fromJSONDictionary:inContext:error:"))); + ++ (nullable NSArray *)mergeObjectsForEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inManagedObjectContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error __attribute__((deprecated("Replaced by -objectsWithEntityName:fromJSONArray:inContext:error:"))); + ++ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject __attribute__((deprecated("Replaced by -JSONDictionaryFromObject:"))); + ++ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects __attribute__((deprecated("Replaced by -JSONArrayFromObjects:"))); @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTJSONSerialization.m b/Groot/GRTJSONSerialization.m index add6d32..796bba7 100644 --- a/Groot/GRTJSONSerialization.m +++ b/Groot/GRTJSONSerialization.m @@ -1,6 +1,6 @@ // GRTJSONSerialization.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,395 +22,97 @@ #import "GRTJSONSerialization.h" -#import "NSPropertyDescription+Groot.h" -#import "NSAttributeDescription+Groot.h" #import "NSEntityDescription+Groot.h" -#import "NSDictionary+Groot.h" +#import "NSManagedObject+Groot.h" -NSString * const GRTJSONSerializationErrorDomain = @"GRTJSONSerializationErrorDomain"; -const NSInteger GRTJSONSerializationErrorInvalidJSONObject = 0xcaca; +NS_ASSUME_NONNULL_BEGIN @implementation GRTJSONSerialization -+ (id)insertObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(JSONDictionary); - - return [[self insertObjectsForEntityName:entityName fromJSONArray:@[JSONDictionary] inManagedObjectContext:context error:error] firstObject]; -} - -+ (NSArray *)insertObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(entityName); - NSParameterAssert(JSONArray); - NSParameterAssert(context); - - NSError * __block tmpError = nil; - NSMutableArray * __block managedObjects = [NSMutableArray arrayWithCapacity:JSONArray.count]; - - if (JSONArray.count == 0) { - // Return early and avoid any processing in the context queue - return managedObjects; - } - - [context performBlockAndWait:^{ - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - if (![dictionary isKindOfClass:NSDictionary.class]) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize value %@. Expected a JSON dictionary.", @""), dictionary]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - tmpError = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - - break; - } - - NSManagedObject *managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if ([property isKindOfClass:NSAttributeDescription.class]) { - *stop = ![self serializeAttribute:(NSAttributeDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:NO error:&tmpError]; - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - *stop = ![self serializeRelationship:(NSRelationshipDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:NO error:&tmpError]; - } - }]; - - if (tmpError == nil) { - [managedObjects addObject:managedObject]; - } else { - [context deleteObject:managedObject]; - break; - } - } - }]; ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONData:(NSData *)data + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError *error = nil; + id parsedJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error != nil) { - *error = tmpError; + if (outError != nil) *outError = error; + return nil; } - return managedObjects; -} - -+ (id)mergeObjectForEntityName:(NSString *)entityName fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(JSONDictionary); - - return [[self mergeObjectsForEntityName:entityName fromJSONArray:@[JSONDictionary] inManagedObjectContext:context error:error] firstObject]; -} - -+ (NSArray *)mergeObjectsForEntityName:(NSString *)entityName fromJSONArray:(NSArray *)JSONArray inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSParameterAssert(entityName); - NSParameterAssert(JSONArray); - NSParameterAssert(context); - - NSError * __block tmpError = nil; - NSMutableArray * __block managedObjects = [NSMutableArray arrayWithCapacity:JSONArray.count]; - - if (JSONArray.count == 0) { - // Return early and avoid any processing in the context queue - return managedObjects; + NSArray *array = nil; + if ([parsedJSON isKindOfClass:[NSDictionary class]]) { + array = @[parsedJSON]; + } else if ([parsedJSON isKindOfClass:[NSArray class]]) { + array = parsedJSON; } - [context performBlockAndWait:^{ - NSEntityDescription *entity = [NSEntityDescription entityForName:entityName inManagedObjectContext:context]; - - NSAttributeDescription *identityAttribute = [entity grt_identityAttribute]; - NSAssert(identityAttribute != nil, @"An identity attribute must be specified in order to merge objects"); - NSAssert([identityAttribute grt_JSONKeyPath] != nil, @"The identity attribute must have an valid JSON key path"); - - NSMutableArray *identifiers = [NSMutableArray arrayWithCapacity:JSONArray.count]; - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - if (![dictionary isKindOfClass:NSDictionary.class]) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize value %@. Expected a JSON dictionary.", @""), dictionary]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - tmpError = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - return; - } - - id identifier = [dictionary grt_valueForAttribute:identityAttribute]; - if (identifier != nil) [identifiers addObject:identifier]; - } - - NSDictionary *existingObjects = [self fetchObjectsForEntity:entity withIdentifiers:identifiers inManagedObjectContext:context error:&tmpError]; - - for (NSDictionary *dictionary in JSONArray) { - if ([dictionary isEqual:NSNull.null]) { - continue; - } - - NSManagedObject *managedObject = nil; - id identifier = [dictionary grt_valueForAttribute:identityAttribute]; - - if (identifier) { - managedObject = existingObjects[identifier]; - } - - if (!managedObject) { - managedObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; - } - - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - if ([property isKindOfClass:NSAttributeDescription.class]) { - *stop = ![self serializeAttribute:(NSAttributeDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:YES error:&tmpError]; - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - *stop = ![self serializeRelationship:(NSRelationshipDescription *)property fromJSONDictionary:dictionary inManagedObject:managedObject merge:YES error:&tmpError]; - } - }]; - - if (tmpError == nil) { - [managedObjects addObject:managedObject]; - } else { - [context deleteObject:managedObject]; - break; - } - } - }]; - - if (error != nil) { - *error = tmpError; - } + NSAssert(array != nil, @"Invalid JSON. The top level object must be an NSArray or an NSDictionary"); - return managedObjects; -} - -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject { - // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships - NSMutableSet *processingRelationships = [NSMutableSet set]; - return [self JSONDictionaryFromManagedObject:managedObject processingRelationships:processingRelationships]; + return [self objectsWithEntityName:entityName fromJSONArray:array inContext:context error:outError]; } -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects { - // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships - NSMutableSet *processingRelationships = [NSMutableSet set]; - return [self JSONArrayFromManagedObjects:managedObjects processingRelationships:processingRelationships]; -} - -#pragma mark - Private - -+ (NSDictionary *)JSONDictionaryFromManagedObject:(NSManagedObject *)managedObject processingRelationships:(NSMutableSet *)processingRelationships { - NSMutableDictionary * __block JSONDictionary = nil; - NSManagedObjectContext *context = managedObject.managedObjectContext; ++ (nullable id)objectWithEntityName:(NSString *)entityName + fromJSONDictionary:(NSDictionary *)JSONDictionary + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError *error = nil; + NSEntityDescription *entity = [NSEntityDescription grt_entityForName:entityName inContext:context error:&error]; - if (!managedObject) { + if (error != nil) { + if (outError != nil) *outError = error; return nil; } - [context performBlockAndWait:^{ - NSDictionary *propertiesByName = managedObject.entity.propertiesByName; - JSONDictionary = [NSMutableDictionary dictionaryWithCapacity:propertiesByName.count]; - - [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { - NSString *JSONKeyPath = [property grt_JSONKeyPath]; - - if (JSONKeyPath == nil) { - return; - } - - id value = [managedObject valueForKey:name]; - - if ([property isKindOfClass:NSAttributeDescription.class]) { - NSAttributeDescription *attribute = (NSAttributeDescription *)property; - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - - if (transformer != nil && [transformer.class allowsReverseTransformation]) { - value = [transformer reverseTransformedValue:value]; - } - } else if ([property isKindOfClass:NSRelationshipDescription.class]) { - NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; - - if ([processingRelationships containsObject:relationship.inverseRelationship]) { - // Skip if the inverse relationship is being serialized - return; - } - - [processingRelationships addObject:relationship]; - - if ([relationship isToMany]) { - NSArray *objects = [value isKindOfClass:NSOrderedSet.class] ? [value array] : [value allObjects]; - value = [self JSONArrayFromManagedObjects:objects processingRelationships:processingRelationships]; - } else { - value = [self JSONDictionaryFromManagedObject:value processingRelationships:processingRelationships]; - } - } - - if (value == nil) { - value = NSNull.null; - } - - NSArray *components = [JSONKeyPath componentsSeparatedByString:@"."]; - - if (components.count > 1) { - // Create a dictionary for each key path component - id obj = JSONDictionary; - for (NSString *component in components) { - if ([obj valueForKey:component] == nil) { - [obj setValue:[NSMutableDictionary dictionary] forKey:component]; - } - - obj = [obj valueForKey:component]; - } - } - - [JSONDictionary setValue:value forKeyPath:JSONKeyPath]; - }]; - }]; - - return JSONDictionary; -} - -+ (NSArray *)JSONArrayFromManagedObjects:(NSArray *)managedObjects processingRelationships:(NSMutableSet *)processingRelationships { - NSMutableArray *JSONArray = [NSMutableArray arrayWithCapacity:managedObjects.count]; - - for (NSManagedObject *managedObject in managedObjects) { - NSDictionary *JSONDictionary = [self JSONDictionaryFromManagedObject:managedObject processingRelationships:processingRelationships]; - [JSONArray addObject:JSONDictionary]; - } + BOOL mergeChanges = [entity grt_identityAttribute] != nil; - return JSONArray; + return [entity grt_importJSONArray:@[JSONDictionary] + inContext:context + mergeChanges:mergeChanges + error:outError].firstObject; } -+ (BOOL)serializeAttribute:(NSAttributeDescription *)attribute fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObject:(NSManagedObject *)managedObject merge:(BOOL)merge error:(NSError *__autoreleasing *)error { - NSString *keyPath = [attribute grt_JSONKeyPath]; - - if (keyPath == nil) { - return YES; - } ++ (nullable NSArray *)objectsWithEntityName:(NSString *)entityName + fromJSONArray:(NSArray *)JSONArray + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError *error = nil; + NSEntityDescription *entity = [NSEntityDescription grt_entityForName:entityName inContext:context error:&error]; - id value = [JSONDictionary valueForKeyPath:keyPath]; - - if (merge && value == nil) { - return YES; - } - - if ([value isEqual:NSNull.null]) { - value = nil; + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } - if (value != nil) { - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - if (transformer) { - value = [transformer transformedValue:value]; - } - } + BOOL mergeChanges = [entity grt_identityAttribute] != nil; - if ([managedObject validateValue:&value forKey:attribute.name error:error]) { - [managedObject setValue:value forKey:attribute.name]; - return YES; - } - - return NO; + return [entity grt_importJSONArray:JSONArray + inContext:context + mergeChanges:mergeChanges + error:outError]; } -+ (BOOL)serializeRelationship:(NSRelationshipDescription *)relationship fromJSONDictionary:(NSDictionary *)JSONDictionary inManagedObject:(NSManagedObject *)managedObject merge:(BOOL)merge error:(NSError *__autoreleasing *)error { - NSString *keyPath = [relationship grt_JSONKeyPath]; - - if (keyPath == nil) { - return YES; - } - - id value = [JSONDictionary valueForKeyPath:keyPath]; - - if (merge && value == nil) { - return YES; - } - - if ([value isEqual:NSNull.null]) { - value = nil; - } - - if (value != nil) { - NSString *entityName = relationship.destinationEntity.name; - NSManagedObjectContext *context = managedObject.managedObjectContext; - NSError *tmpError = nil; - - if ([relationship isToMany]) { - if (![value isKindOfClass:[NSArray class]]) { - if (error) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize '%@' into a to-many relationship. Expected a JSON array.", @""), [relationship grt_JSONKeyPath]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - *error = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - } - - return NO; - } - - NSArray *objects = merge - ? [self mergeObjectsForEntityName:entityName fromJSONArray:value inManagedObjectContext:context error:&tmpError] - : [self insertObjectsForEntityName:entityName fromJSONArray:value inManagedObjectContext:context error:&tmpError]; - - value = [relationship isOrdered] ? [NSOrderedSet orderedSetWithArray:objects] : [NSSet setWithArray:objects]; - } else { - if (![value isKindOfClass:[NSDictionary class]]) { - if (error) { - NSString *message = [NSString stringWithFormat:NSLocalizedString(@"Cannot serialize '%@' into a to-one relationship. Expected a JSON dictionary.", @""), [relationship grt_JSONKeyPath]]; - NSDictionary *userInfo = @{ - NSLocalizedDescriptionKey: message - }; - - *error = [NSError errorWithDomain:GRTJSONSerializationErrorDomain code:GRTJSONSerializationErrorInvalidJSONObject userInfo:userInfo]; - } - - return NO; - } - - value = merge - ? [self mergeObjectForEntityName:entityName fromJSONDictionary:value inManagedObjectContext:context error:&tmpError] - : [self insertObjectForEntityName:entityName fromJSONDictionary:value inManagedObjectContext:context error:&tmpError]; - } - - if (tmpError != nil) { - if (error) { - *error = tmpError; - } - return NO; - } - } - - if ([managedObject validateValue:&value forKey:relationship.name error:error]) { - [managedObject setValue:value forKey:relationship.name]; - return YES; - } - - return NO; ++ (NSDictionary *)JSONDictionaryFromObject:(NSManagedObject *)object { + // Keeping track of in process relationships avoids infinite recursion when serializing inverse relationships + NSMutableSet *relationships = [NSMutableSet set]; + return [object grt_JSONDictionarySerializingRelationships:relationships]; } -+ (NSDictionary *)fetchObjectsForEntity:(NSEntityDescription *)entity withIdentifiers:(NSArray *)identifiers inManagedObjectContext:(NSManagedObjectContext *)context error:(NSError *__autoreleasing *)error { - NSString *identityKey = [[entity grt_identityAttribute] name]; ++ (NSArray *)JSONArrayFromObjects:(NSArray *)objects { + NSMutableArray *array = [NSMutableArray arrayWithCapacity:objects.count]; - NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; - fetchRequest.entity = entity; - fetchRequest.returnsObjectsAsFaults = NO; - fetchRequest.predicate = [NSPredicate predicateWithFormat:@"%K IN %@", identityKey, identifiers]; - - NSArray *objects = [context executeFetchRequest:fetchRequest error:error]; - - if (objects.count > 0) { - NSMutableDictionary *objectsByIdentifier = [NSMutableDictionary dictionaryWithCapacity:objects.count]; - - for (NSManagedObject *object in objects) { - id identifier = [object valueForKey:identityKey]; - objectsByIdentifier[identifier] = object; - } - - return objectsByIdentifier; + for (NSManagedObject *object in objects) { + NSDictionary *dictionary = [self JSONDictionaryFromObject:object]; + [array addObject:dictionary]; } - return nil; + return array; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTManagedStore.h b/Groot/GRTManagedStore.h index ff06a38..1f171c1 100644 --- a/Groot/GRTManagedStore.h +++ b/Groot/GRTManagedStore.h @@ -1,6 +1,6 @@ // GRTManagedStore.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,6 +22,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + /** Manages a Core Data stack. */ @@ -38,30 +40,69 @@ @property (strong, nonatomic, readonly) NSManagedObjectModel *managedObjectModel; /** - Creates and returns a `GRTManagedStore` that will persist its data in memory. + The URL for this managed store. */ -+ (instancetype)managedStoreWithModel:(NSManagedObjectModel *)managedObjectModel; +@property (copy, nonatomic, readonly) NSURL *URL; /** - Creates and returns a `GRTManagedStore` that will persist its data in a temporary file. + Initializes the receiver with the specified location and managed object model. + + This is the designated initializer. + + @param URL The file location of the store. If `nil` the persistent store will be created in memory. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (instancetype)temporaryManagedStore; +- (nullable instancetype)initWithURL:(nullable NSURL *)URL model:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error NS_DESIGNATED_INITIALIZER; /** - Creates and returns a `GRTManagedStore` that will persist its data in the application caches directory. + Initializes a managed store that will persist its data in a discardable cache file. - @param cacheName The file name. + @param cacheName The name of the cache file. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. */ -+ (instancetype)managedStoreWithCacheName:(NSString *)cacheName; +- (nullable instancetype)initWithCacheName:(NSString *)cacheName model:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; /** - Initializes the receiver with the specified path and managed object model. + Initializes a managed store that will persist its data in memory. - This is the designated initializer. + @param model The managed object model. + @param error If an error occurs, upon return contains an NSError object that describes the problem. + */ +- (nullable instancetype)initWithModel:(NSManagedObjectModel *)managedObjectModel error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed store that will persist its data at a given location. + + @param URL The file location of the store. + @param error If an error occurs, upon return contains an NSError object that describes the problem. + */ ++ (nullable instancetype)storeWithURL:(NSURL *)URL error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed store that will persist its data in a discardable cache file. - @param path The persistent store path. If `nil` the persistent store will be created in memory. - @param managedObjectModel The managed object model. If `nil` all models in the current bundle will be used. + @param cacheName The file name. + @param error If an error occurs, upon return contains an NSError object that describes the problem. + */ ++ (nullable instancetype)storeWithCacheName:(NSString *)cacheName error:(NSError * __nullable * __nullable)error; + +/** + Creates and returns a managed object context for this store. */ -- (id)initWithPath:(NSString *)path managedObjectModel:(NSManagedObjectModel *)managedObjectModel; +- (NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType; + +@end + +@interface GRTManagedStore (Deprecated) + ++ (instancetype)managedStoreWithModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithModel:error:"))); + ++ (instancetype)managedStoreWithCacheName:(NSString *)cacheName __attribute__((deprecated("Replaced by +storeWithCacheName:error:"))); + +- (id)initWithPath:(nullable NSString *)path managedObjectModel:(nullable NSManagedObjectModel *)managedObjectModel __attribute__((deprecated("Replaced by -initWithURL:model:error:"))); @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTManagedStore.m b/Groot/GRTManagedStore.m index fbc6c09..4d96034 100644 --- a/Groot/GRTManagedStore.m +++ b/Groot/GRTManagedStore.m @@ -1,6 +1,6 @@ // GRTManagedStore.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,108 +22,110 @@ #import "GRTManagedStore.h" -static NSString *GRTApplicationCachePath() { - static dispatch_once_t onceToken; - static NSString *path; +NS_ASSUME_NONNULL_BEGIN + +static NSURL *GRTCachesDirectoryURL(NSError **outError) { + NSError *error = nil; + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSURL *rootURL = [fileManager URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; - dispatch_once(&onceToken, ^{ - path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; - path = [path stringByAppendingPathComponent:[[NSBundle mainBundle] bundleIdentifier]]; - - NSError *error = nil; - BOOL success = [[NSFileManager defaultManager] createDirectoryAtPath:path - withIntermediateDirectories:YES - attributes:nil - error:&error]; + if (error != nil) { + if (outError != nil) *outError = error; + return nil; + } + + NSString *bundleIdentifier = [NSBundle bundleForClass:[GRTManagedStore class]].bundleIdentifier; + NSURL *cachesDirectoryURL = [rootURL URLByAppendingPathComponent:bundleIdentifier]; + + if (![fileManager fileExistsAtPath:cachesDirectoryURL.path]) { + [fileManager createDirectoryAtURL:cachesDirectoryURL withIntermediateDirectories:YES attributes:nil error:&error]; - if (!success) { - NSLog(@"%s Error creating the application cache directory: %@", __PRETTY_FUNCTION__, error); + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } - }); + } - return path; + return cachesDirectoryURL; } -@interface GRTManagedStore () - -@property (strong, nonatomic, readwrite) NSPersistentStoreCoordinator *persistentStoreCoordinator; -@property (strong, nonatomic, readwrite) NSManagedObjectModel *managedObjectModel; - -@property (copy, nonatomic) NSString *path; - -@end - @implementation GRTManagedStore -#pragma mark - Properties +- (NSManagedObjectModel *)managedObjectModel { + return self.persistentStoreCoordinator.managedObjectModel; +} -- (NSPersistentStoreCoordinator *)persistentStoreCoordinator { - if (!_persistentStoreCoordinator) { - _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel]; - - NSString *storeType = self.path ? NSSQLiteStoreType : NSInMemoryStoreType; - NSURL *storeURL = self.path ? [NSURL fileURLWithPath:self.path] : nil; +- (NSURL *)URL { + NSPersistentStore *store = self.persistentStoreCoordinator.persistentStores[0]; + return store.URL; +} + +- (nullable instancetype)initWithURL:(nullable NSURL *)URL + model:(NSManagedObjectModel *)managedObjectModel + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + self = [super init]; + + if (self) { + _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:managedObjectModel]; + NSString *storeType = (URL != nil ? NSSQLiteStoreType : NSInMemoryStoreType); NSDictionary *options = @{ - NSMigratePersistentStoresAutomaticallyOption: @YES, - NSInferMappingModelAutomaticallyOption: @YES - }; + NSMigratePersistentStoresAutomaticallyOption: @YES, + NSInferMappingModelAutomaticallyOption: @YES + }; NSError *error = nil; - NSPersistentStore *store = [_persistentStoreCoordinator addPersistentStoreWithType:storeType - configuration:nil - URL:storeURL - options:options - error:&error]; - if (!store) { - NSLog(@"%@ Error creating persistent store: %@", self, error); + [_persistentStoreCoordinator addPersistentStoreWithType:storeType + configuration:nil + URL:URL + options:options + error:&error]; + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } } - return _persistentStoreCoordinator; + return self; } -- (NSManagedObjectModel *)managedObjectModel { - if (!_managedObjectModel) { - _managedObjectModel = [NSManagedObjectModel mergedModelFromBundles:nil]; +- (nullable instancetype)initWithCacheName:(NSString *)cacheName + model:(NSManagedObjectModel *)managedObjectModel + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError *error = nil; + NSURL *cachesDirectoryURL = GRTCachesDirectoryURL(&error); + + if (error != nil) { + if (outError != nil) *outError = error; + return nil; } - return _managedObjectModel; + NSURL *storeURL = [cachesDirectoryURL URLByAppendingPathComponent:cacheName]; + return [self initWithURL:storeURL model:managedObjectModel error:outError]; } -#pragma mark - Lifecycle - -+ (instancetype)managedStoreWithModel:(NSManagedObjectModel *)managedObjectModel { - return [[self alloc] initWithPath:nil managedObjectModel:managedObjectModel]; +- (nullable instancetype)initWithModel:(NSManagedObjectModel *)managedObjectModel error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [self initWithURL:nil model:managedObjectModel error:outError]; } -+ (instancetype)temporaryManagedStore { - NSString *fileName = [[NSProcessInfo processInfo] globallyUniqueString]; - NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; - - return [[self alloc] initWithPath:path managedObjectModel:nil]; ++ (nullable instancetype)storeWithURL:(NSURL *)URL error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [[self alloc] initWithURL:URL model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -+ (instancetype)managedStoreWithCacheName:(NSString *)cacheName { - NSParameterAssert(cacheName); - - NSString *path = [GRTApplicationCachePath() stringByAppendingPathComponent:cacheName]; - return [[self alloc] initWithPath:path managedObjectModel:nil]; ++ (nullable instancetype)storeWithCacheName:(NSString *)cacheName error:(NSError *__autoreleasing __nullable * __nullable)outError { + return [[self alloc] initWithCacheName:cacheName model:[NSManagedObjectModel mergedModelFromBundles:nil] error:outError]; } -- (id)init { - return [self initWithPath:nil managedObjectModel:nil]; -} - -- (id)initWithPath:(NSString *)path managedObjectModel:(NSManagedObjectModel *)managedObjectModel { - self = [super init]; - - if (self) { - _path = [path copy]; - _managedObjectModel = managedObjectModel; - } +- (NSManagedObjectContext *)contextWithConcurrencyType:(NSManagedObjectContextConcurrencyType)concurrencyType { + NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType]; + context.persistentStoreCoordinator = self.persistentStoreCoordinator; - return self; + return context; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTValueTransformer.h b/Groot/GRTValueTransformer.h deleted file mode 100644 index 7fcd255..0000000 --- a/Groot/GRTValueTransformer.h +++ /dev/null @@ -1,49 +0,0 @@ -// GRTValueTransformer.h -// -// Copyright (c) 2014 Guillermo Gonzalez -// -// Based on Mantle's MTLValueTransformer, MIT licensed. -// -// 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, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 - -typedef id (^GRTValueTransformerBlock)(id value); - -/** - Generic block-based value transformer. - */ -@interface GRTValueTransformer : NSValueTransformer - -/** - Returns a transformer which transforms values using the given block. Reverse transformations will not be allowed. - */ -+ (instancetype)transformerWithBlock:(GRTValueTransformerBlock)block; - -/** - Returns a transformer which transforms values using the given block, for forward or reverse transformations. - */ -+ (instancetype)reversibleTransformerWithBlock:(GRTValueTransformerBlock)block; - -/** - Returns a transformer which transforms values using the given blocks. - */ -+ (instancetype)reversibleTransformerWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock; - -@end diff --git a/Groot/Groot.h b/Groot/Groot.h index e7e66d2..c9a43c2 100644 --- a/Groot/Groot.h +++ b/Groot/Groot.h @@ -20,7 +20,7 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import +#import //! Project version number for Groot. FOUNDATION_EXPORT double GrootVersionNumber; @@ -28,6 +28,7 @@ FOUNDATION_EXPORT double GrootVersionNumber; //! Project version string for Groot. FOUNDATION_EXPORT const unsigned char GrootVersionString[]; +#import #import -#import +#import #import diff --git a/Groot/Groot.swift b/Groot/Groot.swift new file mode 100644 index 0000000..4ec3035 --- /dev/null +++ b/Groot/Groot.swift @@ -0,0 +1,164 @@ +// Groot.swift +// +// Copyright (c) 2015 Guillermo Gonzalez +// +// 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, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 CoreData + +extension NSManagedObjectContext { + internal func managedObjectModel() -> NSManagedObjectModel { + if let psc = persistentStoreCoordinator { + return psc.managedObjectModel + } + + return parentContext!.managedObjectModel() + } +} + +extension NSManagedObject { + internal class func entityInManagedObjectContext(context: NSManagedObjectContext) -> NSEntityDescription { + let className = NSStringFromClass(self) + let model = context.managedObjectModel() + let entities = model.entities as! [NSEntityDescription] + + for entity in entities { + if entity.managedObjectClassName == className { + return entity + } + } + + assert(false, "Could not locate the entity for \(className).") + return NSEntityDescription() + } +} + +/** + Creates or updates a set of managed objects from JSON data. + + :param: entityName The name of an entity. + :param: fromJSONData A data object containing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsWithEntityName(name: String, fromJSONData data: NSData, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [NSManagedObject]? { + return GRTJSONSerialization.objectsWithEntityName(name, fromJSONData: data, inContext: context, error: outError) as? [NSManagedObject] +} + +/** + Creates or updates a set of managed objects from JSON data. + + :param: fromJSONData A data object containing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsFromJSONData(data: NSData, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [T]? { + let entity = T.entityInManagedObjectContext(context) + return objectsWithEntityName(entity.name!, fromJSONData: data, inContext: context, error: outError) as? [T] +} + +public typealias JSONDictionary = [String: AnyObject] + +/** + Creates or updates a managed object from a JSON dictionary. + + This method converts the specified JSON dictionary into a managed object of a given entity. + + :param: entityName The name of an entity. + :param: fromJSONDictionary A dictionary representing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: A managed object, or `nil` if an error occurs. + */ +public func objectWithEntityName(name: String, fromJSONDictionary dictionary: JSONDictionary, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> NSManagedObject? { + return GRTJSONSerialization.objectWithEntityName(name, fromJSONDictionary: dictionary, inContext: context, error: outError) as? NSManagedObject +} + +/** + Creates or updates a managed object from a JSON dictionary. + + This method converts the specified JSON dictionary into a managed object. + + :param: fromJSONDictionary A dictionary representing JSON data. + :param: inContext The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: A managed object, or `nil` if an error occurs. + */ +public func objectFromJSONDictionary(dictionary: JSONDictionary, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> T? { + let entity = T.entityInManagedObjectContext(context) + return objectWithEntityName(entity.name!, fromJSONDictionary: dictionary, inContext: context, error: outError) as? T; +} + +public typealias JSONArray = [AnyObject] + +/** + Creates or updates a set of managed objects from a JSON array. + + :param: entityName The name of an entity. + :param: fromJSONArray An array representing JSON data. + :param: context The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsWithEntityName(name: String, fromJSONArray array: JSONArray, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [NSManagedObject]? { + return GRTJSONSerialization.objectsWithEntityName(name, fromJSONArray: array, inContext: context, error: outError) as? [NSManagedObject] +} + +/** + Creates or updates a set of managed objects from a JSON array. + + :param: fromJSONArray An array representing JSON data. + :param: context The context into which to fetch or insert the managed objects. + :param: error If an error occurs, upon return contains an NSError object that describes the problem. + + :return: An array of managed objects, or `nil` if an error occurs. + */ +public func objectsFromJSONArray(array: JSONArray, inContext context: NSManagedObjectContext, error outError: NSErrorPointer) -> [T]? { + let entity = T.entityInManagedObjectContext(context) + return objectsWithEntityName(entity.name!, fromJSONArray: array, inContext: context, error: outError) as? [T] +} + +/** + Converts a managed object into a JSON representation. + + :param: object The managed object to use for JSON serialization. + + :return: A JSON dictionary. + */ +public func JSONDictionaryFromObject(object: NSManagedObject) -> JSONDictionary { + return GRTJSONSerialization.JSONDictionaryFromObject(object) as! JSONDictionary; +} + +/** + Converts an array of managed objects into a JSON representation. + + :param: objects The array of managed objects to use for JSON serialization. + + :return: A JSON array. + */ +public func JSONArrayFromObjects(objects: [NSManagedObject]) -> JSONArray { + return GRTJSONSerialization.JSONArrayFromObjects(objects) +} diff --git a/Groot/NSValueTransformer+Groot.h b/Groot/NSValueTransformer+Groot.h new file mode 100644 index 0000000..a0ee2ee --- /dev/null +++ b/Groot/NSValueTransformer+Groot.h @@ -0,0 +1,67 @@ +// NSValueTransformer+Groot.h +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// 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, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 + +NS_ASSUME_NONNULL_BEGIN + +typedef __nullable id (^GRTTransformBlock)(id value); + +@interface NSValueTransformer (Groot) + +/** + Registers a value transformer with a given name and transform block. + + @param name The name of the transformer. + @param transformBlock The block that performs the transformation. + */ ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock; + +/** + Registers a reversible value transformer with a given name and transform blocks. + + @param name The name of the transformer. + @param transformBlock The block that performs the forward transformation. + @param reverseTransformBlock The block that performs the reverse transformation. + */ ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock + reverseTransformBlock:(__nullable id (^)(id value))reverseTransformBlock; + +/** + Registers an entity mapper with a given name and map block. + + An entity mapper maps a JSON dictionary to an entity name. + + Entity mappers can be associated with abstract core data entities in the user info + dictionary by using the `entityMapperName` key. + + @param name The name of the mapper. + @param mapBlock The block that performs the mapping. + */ ++ (void)grt_setEntityMapperWithName:(NSString *)name + mapBlock:(NSString * __nullable (^)(NSDictionary *JSONDictionary))mapBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.m b/Groot/NSValueTransformer+Groot.m new file mode 100644 index 0000000..d04f715 --- /dev/null +++ b/Groot/NSValueTransformer+Groot.m @@ -0,0 +1,53 @@ +// NSValueTransformer+Groot.h +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// 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, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 "NSValueTransformer+Groot.h" +#import "GRTValueTransformer.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSValueTransformer (Groot) + ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock +{ + GRTValueTransformer *valueTransformer = [[GRTValueTransformer alloc] initWithBlock:transformBlock]; + [self setValueTransformer:valueTransformer forName:name]; +} + ++ (void)grt_setValueTransformerWithName:(NSString *)name + transformBlock:(__nullable id (^)(id value))transformBlock + reverseTransformBlock:(__nullable id (^)(id value))reverseTransformBlock +{ + GRTReversibleValueTransformer *valueTransformer = [[GRTReversibleValueTransformer alloc] initWithForwardBlock:transformBlock reverseBlock:reverseTransformBlock]; + [self setValueTransformer:valueTransformer forName:name]; +} + ++ (void)grt_setEntityMapperWithName:(NSString *)name + mapBlock:(NSString * __nullable (^)(NSDictionary *JSONDictionary))mapBlock +{ + return [self grt_setValueTransformerWithName:name transformBlock:mapBlock]; +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Groot/NSValueTransformer+Groot.swift b/Groot/NSValueTransformer+Groot.swift new file mode 100644 index 0000000..3d027da --- /dev/null +++ b/Groot/NSValueTransformer+Groot.swift @@ -0,0 +1,78 @@ +// NSValueTransformer+Groot.swift +// +// Copyright (c) 2014-2015 Guillermo Gonzalez +// +// 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, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 Foundation + +public extension NSValueTransformer { + /** + Registers a value transformer with a given name and transform function. + + :param: name The name of the transformer. + :param: transform The function that performs the transformation. + */ + class func setValueTransformerWithName(name: String, transform: (T) -> (U?)) { + grt_setValueTransformerWithName(name) { value in + (value as? T).flatMap { + transform($0) as? AnyObject + } + } + } + + /** + Registers a reversible value transformer with a given name and transform functions. + + :param: name The name of the transformer. + :param: transform The function that performs the forward transformation. + :param: reverseTransform The function that performs the reverse transformation. + */ + class func setValueTransformerWithName(name: String, transform: (T) -> (U?), reverseTransform: (U) -> (T?)) { + grt_setValueTransformerWithName(name, transformBlock: { value in + return (value as? T).flatMap { + transform($0) as? AnyObject + } + }, reverseTransformBlock: { value in + return (value as? U).flatMap { + reverseTransform($0) as? AnyObject + } + }) + } + + /** + Registers an entity mapper with a given name and map block. + + An entity mapper maps a JSON dictionary to an entity name. + + Entity mappers can be associated with abstract core data entities in the user info + dictionary by using the `entityMapperName` key. + + :param: name The name of the mapper. + :param: map The function that performs the mapping. + */ + class func setEntityMapperWithName(name: String, map: ([String: AnyObject]) -> (String?)) { + grt_setEntityMapperWithName(name) { value in + if let dictionary = value as? [String: AnyObject] { + return map(dictionary) + } + return nil + } + } +} diff --git a/Groot/GRTConstants.m b/Groot/Private/GRTValueTransformer.h similarity index 68% rename from Groot/GRTConstants.m rename to Groot/Private/GRTValueTransformer.h index e916eb7..8bc3d09 100644 --- a/Groot/GRTConstants.m +++ b/Groot/Private/GRTValueTransformer.h @@ -1,6 +1,6 @@ -// GRTConstants.m +// GRTValueTransformer.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,8 +20,21 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import "GRTConstants.h" +#import -NSString * const GRTJSONKeyPathKey = @"JSONKeyPath"; -NSString * const GRTJSONTransformerNameKey = @"JSONTransformerName"; -NSString * const GRTIdentityAttributeKey = @"identityAttribute"; +NS_ASSUME_NONNULL_BEGIN + +@interface GRTValueTransformer : NSValueTransformer + +- (instancetype)initWithBlock:(__nullable id (^)(id value))block; + +@end + +@interface GRTReversibleValueTransformer : GRTValueTransformer + +- (instancetype)initWithForwardBlock:(__nullable id (^)(id value))forwardBlock + reverseBlock:(__nullable id (^)(id value))reverseBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Groot/GRTValueTransformer.m b/Groot/Private/GRTValueTransformer.m similarity index 50% rename from Groot/GRTValueTransformer.m rename to Groot/Private/GRTValueTransformer.m index fea1fbc..0b53fdf 100644 --- a/Groot/GRTValueTransformer.m +++ b/Groot/Private/GRTValueTransformer.m @@ -1,8 +1,6 @@ // GRTValueTransformer.m // -// Copyright (c) 2014 Guillermo Gonzalez -// -// Based on Mantle's MTLValueTransformer, MIT licensed. +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -24,82 +22,78 @@ #import "GRTValueTransformer.h" -#pragma mark - GRTReversibleValueTransformer - -@interface GRTReversibleValueTransformer : GRTValueTransformer -@end +NS_ASSUME_NONNULL_BEGIN #pragma mark - GRTValueTransformer @interface GRTValueTransformer () -@property (copy, nonatomic, readonly) GRTValueTransformerBlock forwardBlock; -@property (copy, nonatomic, readonly) GRTValueTransformerBlock reverseBlock; - -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock; +@property (copy, nonatomic) __nullable id (^transformBlock)(id); @end @implementation GRTValueTransformer -+ (instancetype)transformerWithBlock:(GRTValueTransformerBlock)block { - return [[self alloc] initWithForwardBlock:block reverseBlock:nil]; -} - -+ (instancetype)reversibleTransformerWithBlock:(GRTValueTransformerBlock)block { - return [self reversibleTransformerWithForwardBlock:block reverseBlock:block]; -} - -+ (instancetype)reversibleTransformerWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - return [[GRTReversibleValueTransformer alloc] initWithForwardBlock:forwardBlock reverseBlock:reverseBlock]; -} - -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - NSParameterAssert(forwardBlock); - +- (instancetype)initWithBlock:(__nullable id (^)(id value))block { self = [super init]; - if (self) { - _forwardBlock = [forwardBlock copy]; - _reverseBlock = [reverseBlock copy]; + self.transformBlock = block; } - return self; } #pragma mark - NSValueTransformer + (BOOL)allowsReverseTransformation { - return NO; + return NO; } + (Class)transformedValueClass { - return NSObject.class; + return NSObject.class; } - (id)transformedValue:(id)value { - return self.forwardBlock(value); + if (value != nil) { + return self.transformBlock(value); + } + return nil; } @end #pragma mark - GRTReversibleValueTransformer +@interface GRTReversibleValueTransformer () + +@property (copy, nonatomic) __nullable id (^reverseTransformBlock)(id); + +@end + @implementation GRTReversibleValueTransformer -- (id)initWithForwardBlock:(GRTValueTransformerBlock)forwardBlock reverseBlock:(GRTValueTransformerBlock)reverseBlock { - NSParameterAssert(reverseBlock); - return [super initWithForwardBlock:forwardBlock reverseBlock:reverseBlock]; +- (instancetype)initWithForwardBlock:(__nullable id (^)(id value))forwardBlock + reverseBlock:(__nullable id (^)(id value))reverseBlock +{ + self = [super initWithBlock:forwardBlock]; + if (self) { + self.reverseTransformBlock = reverseBlock; + } + return self; } #pragma mark - NSValueTransformer + (BOOL)allowsReverseTransformation { - return YES; + return YES; } - (id)reverseTransformedValue:(id)value { - return self.reverseBlock(value); + if (value != nil) { + return self.reverseTransformBlock(value); + } + return nil; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSAttributeDescription+Groot.h b/Groot/Private/NSAttributeDescription+Groot.h index 952673e..321fd2e 100644 --- a/Groot/Private/NSAttributeDescription+Groot.h +++ b/Groot/Private/NSAttributeDescription+Groot.h @@ -1,6 +1,6 @@ // NSAttributeDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,25 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSAttributeDescription (Groot) -- (NSValueTransformer *)grt_JSONTransformer; +/** + The value transformer for this attribute. + */ +- (nullable NSValueTransformer *)grt_JSONTransformer; + +/** + Returns the value for this attribute in a given JSON value. + */ +- (nullable id)grt_valueForJSONValue:(id)value; + +/** + Returns all the values for this attribute in a given JSON array. + */ +- (NSArray *)grt_valuesInJSONArray:(NSArray *)array; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSAttributeDescription+Groot.m b/Groot/Private/NSAttributeDescription+Groot.m index 9bf6967..e63bee8 100644 --- a/Groot/Private/NSAttributeDescription+Groot.m +++ b/Groot/Private/NSAttributeDescription+Groot.m @@ -1,6 +1,6 @@ // NSAttributeDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,13 +21,53 @@ // THE SOFTWARE. #import "NSAttributeDescription+Groot.h" -#import "GRTConstants.h" +#import "NSPropertyDescription+Groot.h" @implementation NSAttributeDescription (Groot) -- (NSValueTransformer *)grt_JSONTransformer { - NSString *name = self.userInfo[GRTJSONTransformerNameKey]; - return name ? [NSValueTransformer valueTransformerForName:name] : nil; +- (nullable NSValueTransformer *)grt_JSONTransformer { + NSString *name = self.userInfo[@"JSONTransformerName"]; + return name != nil ? [NSValueTransformer valueTransformerForName:name] : nil; +} + +- (nullable id)grt_valueForJSONValue:(id __nonnull)JSONValue { + id value = nil; + + if ([JSONValue isKindOfClass:[NSDictionary class]]) { + value = [self grt_rawValueInJSONDictionary:JSONValue]; + } else if ([JSONValue isKindOfClass:[NSNumber class]] || [JSONValue isKindOfClass:[NSString class]]) { + value = JSONValue; + } + + if (value != nil) { + if (value == [NSNull null]) { + return nil; + } + + NSValueTransformer *transformer = [self grt_JSONTransformer]; + + if (transformer != nil) { + return [transformer transformedValue:value]; + } + + return value; + } + + return nil; +} + +- (NSArray * __nonnull)grt_valuesInJSONArray:(NSArray * __nonnull)array { + NSMutableArray *values = [NSMutableArray arrayWithCapacity:array.count]; + + for (id object in array) { + id value = [self grt_valueForJSONValue:object]; + + if (value != nil) { + [values addObject:value]; + } + } + + return values; } @end diff --git a/Groot/Private/NSEntityDescription+Groot.h b/Groot/Private/NSEntityDescription+Groot.h index 90cc947..a6a2be5 100644 --- a/Groot/Private/NSEntityDescription+Groot.h +++ b/Groot/Private/NSEntityDescription+Groot.h @@ -1,6 +1,6 @@ // NSEntityDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,21 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSEntityDescription (Groot) -- (NSAttributeDescription *)grt_identityAttribute; ++ (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName + inContext:(NSManagedObjectContext *)context + error:(NSError * __nullable * __nullable)error; + +- (nullable NSAttributeDescription *)grt_identityAttribute; + +- (nullable NSArray *)grt_importJSONArray:(NSArray *)array + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError * __nullable * __nullable)error; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSEntityDescription+Groot.m b/Groot/Private/NSEntityDescription+Groot.m index cc7f816..432e7f0 100644 --- a/Groot/Private/NSEntityDescription+Groot.m +++ b/Groot/Private/NSEntityDescription+Groot.m @@ -1,6 +1,6 @@ // NSEntityDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,25 +21,193 @@ // THE SOFTWARE. #import "NSEntityDescription+Groot.h" -#import "GRTConstants.h" +#import "NSPropertyDescription+Groot.h" +#import "NSAttributeDescription+Groot.h" +#import "NSManagedObject+Groot.h" + +#import "GRTError.h" + +NS_ASSUME_NONNULL_BEGIN @implementation NSEntityDescription (Groot) -- (NSAttributeDescription *)grt_identityAttribute { - - NSString *identityAttribute = nil; - NSEntityDescription *entityDescription = self; - - while (entityDescription && !identityAttribute) { - identityAttribute = entityDescription.userInfo[GRTIdentityAttributeKey]; - entityDescription = entityDescription.superentity; - } - - if (identityAttribute) { - return self.attributesByName[identityAttribute]; - } - - return nil; ++ (nullable NSEntityDescription *)grt_entityForName:(NSString *)entityName + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)error +{ + NSEntityDescription *entity = [self entityForName:entityName inManagedObjectContext:context]; + + if (entity == nil && error != nil) { + *error = [NSError errorWithDomain:GRTErrorDomain code:GRTErrorEntityNotFound userInfo:nil]; + } + + return entity; +} + +- (nullable NSAttributeDescription *)grt_identityAttribute { + NSString *attributeName = nil; + NSEntityDescription *entity = self; + + while (entity != nil && attributeName == nil) { + attributeName = entity.userInfo[@"identityAttribute"]; + entity = [entity superentity]; + } + + if (attributeName != nil) { + return self.attributesByName[attributeName]; + } + + return nil; +} + +- (nullable NSArray *)grt_importJSONArray:(NSArray *)array + inContext:(NSManagedObjectContext *)context + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSMutableArray * __block managedObjects = [NSMutableArray array]; + NSError * __block error = nil; + + if (array.count == 0) { + // Return early and avoid further processing + return managedObjects; + } + + [context performBlockAndWait:^{ + NSDictionary *existingObjects = nil; + + if (mergeChanges) { + existingObjects = [self grt_existingObjectsWithJSONArray:array inContext:context error:&error]; + if (error != nil) return; // exit the block + } + + for (id obj in array) { + if (obj == [NSNull null]) { + continue; + } + + NSManagedObject *managedObject = [self grt_managedObjectForJSONValue:obj inContext:context existingObjects:existingObjects]; + + if ([obj isKindOfClass:[NSDictionary class]]) { + [managedObject grt_importJSONDictionary:obj mergeChanges:mergeChanges error:&error]; + } else { + [managedObject grt_importJSONValue:obj error:&error]; + } + + if (error == nil) { + [managedObjects addObject:managedObject]; + } else { + [context deleteObject:managedObject]; + return; // exit the block + } + } + }]; + + if (error != nil) { + // Delete any objects we have created when there's an error + if (managedObjects.count > 0) { + [context performBlockAndWait:^{ + for (NSManagedObject *object in managedObjects) { + [context deleteObject:object]; + } + }]; + } + + if (outError != nil) { + *outError = error; + } + + managedObjects = nil; + } + + return managedObjects; +} + +#pragma mark - Private + +- (nullable NSValueTransformer *)grt_entityMapper { + NSString *name = self.userInfo[@"entityMapperName"]; + + if (name == nil) { + return nil; + } + + return [NSValueTransformer valueTransformerForName:name]; +} + +- (NSString *)grt_entityNameForJSONValue:(id)value { + NSString *name = nil; + + if ([value isKindOfClass:[NSDictionary class]]) { + name = [[self grt_entityMapper] transformedValue:value]; + } + + return name ? : self.name; +} + +- (nullable NSDictionary *)grt_existingObjectsWithJSONArray:(NSArray *)array + inContext:(NSManagedObjectContext *)context + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSAttributeDescription *attribute = [self grt_identityAttribute]; + + if (attribute == nil) { + if (outError) { + NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); + NSString *message = [NSString stringWithFormat:format, self.name]; + *outError = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorIdentityNotFound + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + return nil; + } + + NSArray *identifiers = [attribute grt_valuesInJSONArray:array]; + NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; + fetchRequest.entity = self; + fetchRequest.returnsObjectsAsFaults = NO; + fetchRequest.predicate = [NSPredicate predicateWithFormat:@"%K IN %@", attribute.name, identifiers]; + + NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:outError]; + + if (fetchedObjects != nil) { + NSMutableDictionary *objects = [NSMutableDictionary dictionaryWithCapacity:fetchedObjects.count]; + + for (NSManagedObject *object in fetchedObjects) { + id identifier = [object valueForKey:attribute.name]; + if (identifier != nil) { + objects[identifier] = object; + } + } + return objects; + } + + return nil; +} + +- (NSManagedObject *)grt_managedObjectForJSONValue:(id)value + inContext:(NSManagedObjectContext *)context + existingObjects:(nullable NSDictionary *)existingObjects +{ + NSManagedObject *managedObject = nil; + + if (existingObjects) { + NSAttributeDescription *identityAttribute = [self grt_identityAttribute]; + id identifier = [identityAttribute grt_valueForJSONValue:value]; + if (identifier != nil) { + managedObject = existingObjects[identifier]; + } + } + + if (managedObject == nil) { + NSString *entityName = [self grt_entityNameForJSONValue:value]; + managedObject = [[self class] insertNewObjectForEntityForName:entityName inManagedObjectContext:context]; + } + + return managedObject; } @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSDictionary+Groot.m b/Groot/Private/NSManagedObject+Groot.h similarity index 62% rename from Groot/Private/NSDictionary+Groot.m rename to Groot/Private/NSManagedObject+Groot.h index be84bb6..5bff6bd 100644 --- a/Groot/Private/NSDictionary+Groot.m +++ b/Groot/Private/NSManagedObject+Groot.h @@ -1,6 +1,6 @@ -// NSDictionary+Groot.m +// NSManagedObject+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,27 +20,20 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. -#import "NSDictionary+Groot.h" -#import "NSPropertyDescription+Groot.h" -#import "NSAttributeDescription+Groot.h" - -@implementation NSDictionary (Groot) - -- (id)grt_valueForAttribute:(NSAttributeDescription *)attribute { - id value = [self valueForKeyPath:[attribute grt_JSONKeyPath]]; - - if ([value isEqual:NSNull.null]) { - value = nil; - } - - if (value != nil) { - NSValueTransformer *transformer = [attribute grt_JSONTransformer]; - if (transformer) { - value = [transformer transformedValue:value]; - } - } - - return value; -} +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSManagedObject (Groot) + +- (void)grt_importJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)error; + +- (void)grt_importJSONValue:(id)value error:(NSError *__autoreleasing __nullable * __nullable)error; + +- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSManagedObject+Groot.m b/Groot/Private/NSManagedObject+Groot.m new file mode 100644 index 0000000..77a4a81 --- /dev/null +++ b/Groot/Private/NSManagedObject+Groot.m @@ -0,0 +1,259 @@ +// NSManagedObject+Groot.m +// +// Copyright (c) 2015 Guillermo Gonzalez +// +// 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, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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 "NSManagedObject+Groot.h" + +#import "GRTError.h" + +#import "NSPropertyDescription+Groot.h" +#import "NSAttributeDescription+Groot.h" +#import "NSEntityDescription+Groot.h" + +NS_ASSUME_NONNULL_BEGIN + +@implementation NSManagedObject (Groot) + +- (void)grt_importJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + NSError * __block error = nil; + NSDictionary *propertiesByName = self.entity.propertiesByName; + + [propertiesByName enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSPropertyDescription *property, BOOL *stop) { + if (![property grt_JSONSerializable]) { + return; // continue + } + + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + [self grt_setAttribute:attribute fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } else if ([property isKindOfClass:[NSRelationshipDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + [self grt_setRelationship:relationship fromJSONDictionary:dictionary mergeChanges:mergeChanges error:&error]; + } + + *stop = (error != nil); // break on error + }]; + + if (error != nil && outError != nil) { + *outError = error; + } +} + +- (void)grt_importJSONValue:(id)value error:(NSError *__autoreleasing __nullable * __nullable)outError { + NSAttributeDescription *attribute = [self.entity grt_identityAttribute]; + + if (attribute == nil) { + if (outError) { + NSString *format = NSLocalizedString(@"%@ has no identity attribute", @"Groot"); + NSString *message = [NSString stringWithFormat:format, self.entity.name]; + *outError = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorIdentityNotFound + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + return; + } + + id identifier = [attribute grt_valueForJSONValue:value]; + + if ([self validateValue:&identifier forKey:attribute.name error:outError]) { + [self setValue:identifier forKey:attribute.name]; + } +} + +- (NSDictionary *)grt_JSONDictionarySerializingRelationships:(NSMutableSet *)serializingRelationships { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + NSManagedObjectContext *context = self.managedObjectContext; + NSDictionary *propertiesByName = self.entity.propertiesByName; + + [context performBlockAndWait:^{ + for (NSString *name in propertiesByName) { + NSPropertyDescription *property = propertiesByName[name]; + + if (![property grt_JSONSerializable]) { + continue; + } + + NSString *keyPath = [property grt_JSONKeyPath]; + id value = [self valueForKey:name]; + + if (value == nil) { + value = [NSNull null]; + } else { + if ([property isKindOfClass:[NSAttributeDescription class]]) { + NSAttributeDescription *attribute = (NSAttributeDescription *)property; + NSValueTransformer *transformer = [attribute grt_JSONTransformer]; + + if (transformer && [[transformer class] allowsReverseTransformation]) { + value = [transformer reverseTransformedValue:value]; + } + } else if ([property isKindOfClass:[NSRelationshipDescription class]]) { + NSRelationshipDescription *relationship = (NSRelationshipDescription *)property; + NSRelationshipDescription *inverseRelationship = relationship.inverseRelationship; + + if ([serializingRelationships containsObject:inverseRelationship]) { + // Skip if the inverse relationship is being serialized + continue; + } + + [serializingRelationships addObject:relationship]; + + if (relationship.toMany) { + NSArray *managedObjects = @[]; + if ([value isKindOfClass:[NSOrderedSet class]]) { + NSOrderedSet *set = value; + managedObjects = set.array; + } else if ([value isKindOfClass:[NSSet class]]) { + NSSet *set = value; + managedObjects = set.allObjects; + } + + NSMutableArray *array = [NSMutableArray arrayWithCapacity:managedObjects.count]; + for (NSManagedObject *managedObject in managedObjects) { + NSDictionary *dictionary = [managedObject grt_JSONDictionarySerializingRelationships:serializingRelationships]; + [array addObject:dictionary]; + } + value = array; + } else { + NSManagedObject *managedObject = value; + value = [managedObject grt_JSONDictionarySerializingRelationships:serializingRelationships]; + } + } + } + + NSMutableArray *components = [[keyPath componentsSeparatedByString:@"."] mutableCopy]; + [components removeLastObject]; + + if (components.count > 0) { + // Create a dictionary for each key path component + NSMutableDictionary *tmpDictionary = dictionary; + for (NSString *component in components) { + if (tmpDictionary[component] == nil) { + tmpDictionary[component] = [NSMutableDictionary dictionary]; + } + + tmpDictionary = tmpDictionary[component]; + } + } + + [dictionary setValue:value forKeyPath:keyPath]; + } + }]; + + return [dictionary copy]; +} + +#pragma mark - Private + +- (void)grt_setAttribute:(NSAttributeDescription *)attribute + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + id value = nil; + id rawValue = [attribute grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil) { + if (rawValue != [NSNull null]) { + NSValueTransformer *transformer = [attribute grt_JSONTransformer]; + if (transformer) { + value = [transformer transformedValue:rawValue]; + } else { + value = rawValue; + } + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:attribute.name]; + [self validateValue:&value forKey:attribute.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:attribute.name error:outError]) { + [self setValue:value forKey:attribute.name]; + } +} + +- (void)grt_setRelationship:(NSRelationshipDescription *)relationship + fromJSONDictionary:(NSDictionary *)dictionary + mergeChanges:(BOOL)mergeChanges + error:(NSError *__autoreleasing __nullable * __nullable)outError +{ + id value = nil; + id rawValue = [relationship grt_rawValueInJSONDictionary:dictionary]; + + if (rawValue != nil && rawValue != [NSNull null]) { + NSError *error = nil; + NSEntityDescription *destinationEntity = relationship.destinationEntity; + BOOL isArray = [rawValue isKindOfClass:[NSArray class]]; + + if (relationship.toMany && isArray) { + NSArray *managedObjects = [destinationEntity grt_importJSONArray:rawValue + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error]; + if (managedObjects != nil) { + value = relationship.ordered ? [NSOrderedSet orderedSetWithArray:managedObjects] : [NSSet setWithArray:managedObjects]; + } + } + else if (!relationship.toMany && !isArray) { + NSManagedObject *managedObject = [destinationEntity grt_importJSONArray:@[rawValue] + inContext:self.managedObjectContext + mergeChanges:mergeChanges + error:&error].firstObject; + + if (managedObject != nil) { + value = managedObject; + } + } + else { + NSString *format = NSLocalizedString(@"Cannot serialize '%@' into relationship '%@.%@'.", @"Groot"); + NSString *message = [NSString stringWithFormat:format, rawValue, relationship.entity.name, relationship.name]; + error = [NSError errorWithDomain:GRTErrorDomain + code:GRTErrorInvalidJSONObject + userInfo:@{ NSLocalizedDescriptionKey: message }]; + } + + if (error != nil) { + if (outError != nil) *outError = error; + return; + } + } else if (mergeChanges) { + // Just validate the current value + value = [self valueForKey:relationship.name]; + [self validateValue:&value forKey:relationship.name error:outError]; + + return; + } + + if ([self validateValue:&value forKey:relationship.name error:outError]) { + [self setValue:value forKey:relationship.name]; + } +} + +@end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSPropertyDescription+Groot.h b/Groot/Private/NSPropertyDescription+Groot.h index fc3f24c..1678cdc 100644 --- a/Groot/Private/NSPropertyDescription+Groot.h +++ b/Groot/Private/NSPropertyDescription+Groot.h @@ -1,6 +1,6 @@ // NSPropertyDescription+Groot.h // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -22,8 +22,25 @@ #import +NS_ASSUME_NONNULL_BEGIN + @interface NSPropertyDescription (Groot) -- (NSString *)grt_JSONKeyPath; +/** + The JSON key path. + */ +- (nullable NSString *)grt_JSONKeyPath; + +/** + Returns `true` if this property should participate in the JSON serialization process. + */ +- (BOOL)grt_JSONSerializable; + +/** + Returns the untransformed raw value for this property in a given JSON object. + */ +- (nullable id)grt_rawValueInJSONDictionary:(NSDictionary *)object; @end + +NS_ASSUME_NONNULL_END diff --git a/Groot/Private/NSPropertyDescription+Groot.m b/Groot/Private/NSPropertyDescription+Groot.m index 5320739..a7e67b7 100644 --- a/Groot/Private/NSPropertyDescription+Groot.m +++ b/Groot/Private/NSPropertyDescription+Groot.m @@ -1,6 +1,6 @@ // NSPropertyDescription+Groot.m // -// Copyright (c) 2014 Guillermo Gonzalez +// Copyright (c) 2014-2015 Guillermo Gonzalez // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -21,22 +21,25 @@ // THE SOFTWARE. #import "NSPropertyDescription+Groot.h" -#import "GRTConstants.h" -static BOOL GRTIsNullKeyPath(NSString *keyPath) { - return [keyPath isEqual:NSNull.null] || [keyPath isEqualToString:@"null"]; +@implementation NSPropertyDescription (Groot) + +- (nullable NSString *)grt_JSONKeyPath { + return self.userInfo[@"JSONKeyPath"]; } -@implementation NSPropertyDescription (Groot) +- (BOOL)grt_JSONSerializable { + return [self grt_JSONKeyPath] != nil; +} -- (NSString *)grt_JSONKeyPath { - NSString *JSONKeyPath = self.userInfo[GRTJSONKeyPathKey]; +- (nullable id)grt_rawValueInJSONDictionary:(NSDictionary * __nonnull)dictionary { + NSString *keyPath = [self grt_JSONKeyPath]; - if (GRTIsNullKeyPath(JSONKeyPath)) { - return nil; + if (keyPath != nil) { + return [dictionary valueForKeyPath:keyPath]; } - return JSONKeyPath ? : self.name; + return nil; } @end diff --git a/GrootTests/GRTJSONSerializationTests.m b/GrootTests/GRTJSONSerializationTests.m index 64e2cfe..fd46462 100644 --- a/GrootTests/GRTJSONSerializationTests.m +++ b/GrootTests/GRTJSONSerializationTests.m @@ -9,8 +9,8 @@ #import #import +#import "NSData+Resource.h" #import "GRTModels.h" -#import "NSEntityDescription+Groot.h" @interface GRTJSONSerializationTests : XCTestCase @@ -24,29 +24,32 @@ @implementation GRTJSONSerializationTests - (void)setUp { [super setUp]; - NSBundle *bundle = [NSBundle bundleForClass:[self class]]; - NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; + self.store = [[GRTManagedStore alloc] initWithModel:[NSManagedObjectModel grt_testModel] error:nil]; + XCTAssertNotNil(self.store); - self.store = [GRTManagedStore managedStoreWithModel:model]; - self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; - self.context.persistentStoreCoordinator = self.store.persistentStoreCoordinator; + self.context = [self.store contextWithConcurrencyType:NSMainQueueConcurrencyType]; - NSValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithForwardBlock:^id(NSString *value) { - if (value) { - return @([value integerValue]); - } - - return nil; - } reverseBlock:^id(NSNumber *value) { + [NSValueTransformer grt_setValueTransformerWithName:@"GrootTests.Transformer" transformBlock:^id(NSString *value) { + return @([value integerValue]); + } reverseTransformBlock:^id(NSNumber *value) { return [value stringValue]; }]; - [NSValueTransformer setValueTransformer:transformer forName:@"GRTTestTransformer"]; + [NSValueTransformer grt_setEntityMapperWithName:@"GrootTests.Abstract" mapBlock:^NSString *(NSDictionary *JSONDictionary) { + NSDictionary *entityMapping = @{ + @"A": @"ConcreteA", + @"B": @"ConcreteB" + }; + NSString *type = JSONDictionary[@"type"]; + return entityMapping[type]; + }]; } - (void)tearDown { self.store = nil; - [NSValueTransformer setValueTransformer:nil forName:@"GRTTestTransformer"]; + self.context = nil; + + [NSValueTransformer setValueTransformer:nil forName:@"GrootTests.Transformer"]; [super tearDown]; } @@ -74,82 +77,74 @@ - (void)testInsertObject { }; NSError *error = nil; - GRTCharacter *batman = [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:batmanJSON inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; XCTAssertNil(error, @"shouldn't return an error"); - XCTAssertEqualObjects(@"Character", batman.entity.name, @"should serialize to the right entity"); - - XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@1699, batman.identifier, @"should serialize attributes"); + XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@"Bruce Wayne", batman.realName, @"should serialize attributes"); - GRTPower *power = batman.powers[0]; + XCTAssertEqual(2U, batman.powers.count, "should serialize to-many relationships"); - XCTAssertEqualObjects(@"Power", power.entity.name, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@4, power.identifier, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@"Agility", power.name, @"should serialize to-many relationships"); + GRTPower *agility = batman.powers[0]; - power = batman.powers[1]; + XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@9, power.identifier, @"should serialize to-many relationships"); - XCTAssertEqualObjects(@"Insanely Rich", power.name, @"should serialize to-many relationships"); + GRTPower *wealth = batman.powers[1]; - XCTAssertEqualObjects(@"Publisher", batman.publisher.entity.name, @"should serialize to-one relationships"); - XCTAssertEqualObjects(@10, batman.publisher.identifier, @"should serialize to-one relationships"); - XCTAssertEqualObjects(@"DC Comics", batman.publisher.name, @"should serialize to-one relationships"); -} - -- (void)testInsertInvalidJSON { - NSArray *invalidJSON = @[@1]; + XCTAssertEqualObjects(@9, wealth.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Insanely Rich", wealth.name, @"should serialize to-many relationships"); - NSError *error = nil; - [GRTJSONSerialization insertObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; + GRTPublisher *publisher = batman.publisher; - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertNotNil(publisher, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@10, publisher.identifier, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@"DC Comics", publisher.name, @"should serialize to-one relationships"); } -- (void)testInsertInvalidToManyRelationship { +- (void)testInsertInvalidToOneRelationship { NSDictionary *invalidBatman = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"powers": @{ // This should be a JSON array - @"id": @"4", - @"name": @"Agility" - } + @"publisher": @[@"DC"] // This should be a JSON dictionary }; NSError *error = nil; - [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:invalidBatman inContext:self.context error:&error]; + XCTAssertNil(batman, @"should return nil on error"); XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } -- (void)testInsertInvalidToOneRelationship { +- (void)testInsertInvalidToManyRelationship { NSDictionary *invalidBatman = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"publisher": @"DC" // This should be a JSON dictionary + @"powers": @{ // This should be a JSON array + @"id": @"4", + @"name": @"Agility" + } }; NSError *error = nil; - [GRTJSONSerialization insertObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:invalidBatman inContext:self.context error:&error]; + XCTAssertNil(batman, @"should return nil on error"); XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertEqualObjects(GRTErrorDomain, error.domain, @"should return a serialization error"); + XCTAssertEqual(GRTErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); } - (void)testMergeObject { NSDictionary *batmanJSON = @{ @"id": @"1699", @"name": @"Batman", - @"real_name": @"Bruce Wayne", + @"real_name": @"Guille Gonzalez", @"powers": @[ @{ @"id": @"4", @@ -168,13 +163,14 @@ - (void)testMergeObject { }; NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batmanJSON inManagedObjectContext:self.context error:&error]; - XCTAssertNil(error, @"shouldn't return an error"); + + [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; + XCTAssertNil(error); NSArray *updateJSON = @[ @{ @"id": @"1699", - @"real_name": NSNull.null, // Should reset Batman real name + @"real_name": @"Bruce Wayne", // Should update real name @"publisher" : @{ @"id": @"10", @"name": @"DC Comics" // Should update the publisher name @@ -197,30 +193,28 @@ - (void)testMergeObject { } ]; - [GRTJSONSerialization mergeObjectsForEntityName:@"Character" fromJSONArray:updateJSON inManagedObjectContext:self.context error:&error]; - XCTAssertNil(error, @"shouldn't return an error"); + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:updateJSON inContext:self.context error:&error]; + XCTAssertNil(error); NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"identifier" ascending:YES]; NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Character"]; fetchRequest.sortDescriptors = @[sortDescriptor]; NSArray *characters = [self.context executeFetchRequest:fetchRequest error:NULL]; - XCTAssertEqual(2U, characters.count, @"there should be 2 characters"); GRTCharacter *ironMan = characters[0]; - XCTAssertEqualObjects(@"Character", ironMan.entity.name, @"should serialize to the right entity"); XCTAssertEqualObjects(@"Iron Man", ironMan.name, @"should serialize attributes"); XCTAssertEqualObjects(@1455, ironMan.identifier, @"should serialize attributes"); XCTAssertEqualObjects(@"Tony Stark", ironMan.realName, @"should serialize attributes"); GRTPower *powerSuit = ironMan.powers[0]; - XCTAssertEqualObjects(@"Power", powerSuit.entity.name, @"should serialize to-many relationships"); XCTAssertEqualObjects(@31, powerSuit.identifier, @"should serialize to-many relationships"); XCTAssertEqualObjects(@"Power Suit", powerSuit.name, @"should serialize to-many relationships"); + GRTPower *ironManRich = ironMan.powers[1]; XCTAssertEqualObjects(@9, ironManRich.identifier, @"should serialize to-many relationships"); @@ -228,17 +222,15 @@ - (void)testMergeObject { GRTCharacter *batman = characters[1]; - XCTAssertEqualObjects(@"Character", batman.entity.name, @"should serialize to the right entity"); XCTAssertEqualObjects(@"Batman", batman.name, @"should serialize attributes"); XCTAssertEqualObjects(@1699, batman.identifier, @"should serialize attributes"); - XCTAssertNil(batman.realName, @"should update explicit null values"); + XCTAssertEqualObjects(@"Bruce Wayne", batman.realName, @"should serialize attributes"); GRTPower *agility = batman.powers[0]; - XCTAssertEqualObjects(@"Power", agility.entity.name, @"should serialize to-many relationships"); XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); - + GRTPower *batmanRich = batman.powers[1]; XCTAssertEqualObjects(@9, batmanRich.identifier, @"should serialize to-many relationships"); @@ -246,7 +238,6 @@ - (void)testMergeObject { XCTAssertEqualObjects(batmanRich, ironManRich, @"should merge relationships properly"); - XCTAssertEqualObjects(@"Publisher", batman.publisher.entity.name, @"should serialize to-one relationships"); XCTAssertEqualObjects(@10, batman.publisher.identifier, @"should serialize to-one relationships"); XCTAssertEqualObjects(@"DC Comics", batman.publisher.name, @"should serialize to-one relationships"); @@ -259,94 +250,185 @@ - (void)testMergeObject { XCTAssertEqual(1U, publisherCount, @"there should be 1 publisher objects"); } -- (void)testMergeInvalidJSON { - NSArray *invalidJSON = @[@1]; +- (void)testValidationDuringMerge { + // See https://github.com/gonzalezreal/Groot/issues/2 + + NSData *data = [NSData grt_dataWithContentsOfResource:@"characters.json"]; + XCTAssertNotNil(data, @"characters.json not found"); NSError *error = nil; - [GRTJSONSerialization mergeObjectsForEntityName:@"Character" fromJSONArray:invalidJSON inManagedObjectContext:self.context error:&error]; + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONData:data inContext:self.context error:&error]; + XCTAssertNil(error); - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + NSData *updatedData = [NSData grt_dataWithContentsOfResource:@"characters_update.json"]; + XCTAssertNotNil(updatedData, @"characters_update.json not found"); + + [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONData:updatedData inContext:self.context error:&error]; + XCTAssertNotNil(error, "should return an error"); + XCTAssertEqualObjects(NSCocoaErrorDomain, error.domain, "should return a validation error"); + XCTAssertEqual(NSValidationMissingMandatoryPropertyError, error.code, "should return a validation error"); } -- (void)testMergeInvalidToManyRelationship { - NSDictionary *invalidBatman = @{ +- (void)testMissingIdentityAttribute { + NSEntityDescription *powerEntity = self.store.managedObjectModel.entitiesByName[@"Power"]; + powerEntity.userInfo = @{}; // Remove the identity attribute name from the entity + + NSDictionary *dictionary = @{ @"id": @"1699", @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"powers": @{ // This should be a JSON array - @"id": @"4", - @"name": @"Agility" - } + @"powers": @[ + @{ + @"id": @"4", + @"name": @"Agility" + } + ] }; NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:dictionary inContext:self.context error:&error]; - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + XCTAssertNil(batman); + XCTAssertNotNil(error); + + XCTAssertEqualObjects(GRTErrorDomain, error.domain); + XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); } -- (void)testMergeInvalidToOneRelationship { - NSDictionary *invalidBatman = @{ - @"id": @"1699", +- (void)testSerializationFromIdentifiers { + NSArray *charactersJSON = @[@"1699", @"1455"]; + + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + XCTAssertEqual(2U, characters.count); + + GRTCharacter *character = characters[0]; + XCTAssertEqualObjects(@"Character", character.entity.name); + XCTAssertEqualObjects(@1699, character.identifier); + + character = characters[1]; + XCTAssertEqualObjects(@"Character", character.entity.name); + XCTAssertEqualObjects(@1455, character.identifier); +} + +- (void)testRelationshipSerializationFromIdentifiers { + NSDictionary *batmanJSON = @{ @"name": @"Batman", @"real_name": @"Bruce Wayne", - @"publisher": @"DC" // This should be a JSON dictionary + @"id": @"1699", + @"powers": @[@"4", NSNull.null, @"9"], + @"publisher": @"10" + }; + + NSArray *powersJSON = @[ + @{ + @"id": @"4", + @"name": @"Agility" + }, + @{ + @"id": @"9", + @"name": @"Insanely Rich" + } + ]; + + NSDictionary *publisherJSON = @{ + @"id": @"10", + @"name": @"DC Comics" }; NSError *error = nil; - [GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:invalidBatman inManagedObjectContext:self.context error:&error]; + GRTCharacter *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); - XCTAssertNotNil(error, @"should return an error"); - XCTAssertEqualObjects(GRTJSONSerializationErrorDomain, error.domain, @"should return a serialization error"); - XCTAssertEqual(GRTJSONSerializationErrorInvalidJSONObject, error.code, @"should return an invalid JSON error"); + [GRTJSONSerialization objectsWithEntityName:@"Power" fromJSONArray:powersJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + [GRTJSONSerialization objectWithEntityName:@"Publisher" fromJSONDictionary:publisherJSON inContext:self.context error:&error]; + XCTAssertNil(error, @"shouldn't return an error"); + + XCTAssertEqual(2U, batman.powers.count, "should serialize to-many relationships"); + + GRTPower *agility = batman.powers[0]; + + XCTAssertEqualObjects(@4, agility.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Agility", agility.name, @"should serialize to-many relationships"); + + GRTPower *wealth = batman.powers[1]; + + XCTAssertEqualObjects(@9, wealth.identifier, @"should serialize to-many relationships"); + XCTAssertEqualObjects(@"Insanely Rich", wealth.name, @"should serialize to-many relationships"); + + GRTPublisher *publisher = batman.publisher; + + XCTAssertNotNil(publisher, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@10, publisher.identifier, @"should serialize to-one relationships"); + XCTAssertEqualObjects(@"DC Comics", publisher.name, @"should serialize to-one relationships"); } -- (void)testMergeWithoutIdentityAttribute { - NSEntityDescription *powerEntity = self.store.managedObjectModel.entitiesByName[@"Power"]; - powerEntity.userInfo = @{}; // Remove the identity attribute name from the entity +- (void)testSerializationFromIdentifiersFailsWithoutIdentityAttribute { + NSEntityDescription *characterEntity = self.store.managedObjectModel.entitiesByName[@"Character"]; + characterEntity.userInfo = @{}; // Remove the identity attribute name from the entity - NSDictionary *batman = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ - @{ - @"id": @"4", - @"name": @"Agility" - } - ] - }; + NSArray *charactersJSON = @[@"1699", @"1455"]; + + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; - XCTAssertThrows([GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batman inManagedObjectContext:self.context error:NULL], @"merge should assert when the identity attribute is not specified"); + XCTAssertNil(characters); + XCTAssertNotNil(error); + + XCTAssertEqualObjects(GRTErrorDomain, error.domain); + XCTAssertEqual(GRTErrorIdentityNotFound, error.code, "should return an identity not found error"); } -- (void)testMergeWithNoIdentityAttributeJSONKeyPath { - NSEntityDescription *powerEntity = self.store.managedObjectModel.entitiesByName[@"Power"]; - NSAttributeDescription *identityAttribute = [powerEntity grt_identityAttribute]; - identityAttribute.userInfo = @{ - @"JSONKeyPath": @"null" - }; +- (void)testSerializationFromIdentifiersValidatesValues { + NSArray *charactersJSON = @[@"1699", [NSValue valueWithRange:NSMakeRange(0, 0)]]; - NSDictionary *batman = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ - @{ - @"id": @"4", - @"name": @"Agility" - } - ] + NSError *error = nil; + NSArray *characters = [GRTJSONSerialization objectsWithEntityName:@"Character" fromJSONArray:charactersJSON inContext:self.context error:&error]; + + XCTAssertNil(characters); + XCTAssertNotNil(error); + XCTAssertEqualObjects(NSCocoaErrorDomain, error.domain); + XCTAssertEqual(NSValidationMissingMandatoryPropertyError, error.code); +} + +- (void)testSerializationWithEntityInheritance { + NSData *data = [NSData grt_dataWithContentsOfResource:@"container.json"]; + XCTAssertNotNil(data, @"container.json not found"); + + NSError *error = nil; + NSArray *objects = [GRTJSONSerialization objectsWithEntityName:@"Container" fromJSONData:data inContext:self.context error:&error]; + XCTAssertNil(error); + XCTAssertEqual(1U, objects.count); + + GRTContainer *container = objects[0]; + + GRTConcreteA *concreteA = container.abstracts[0]; + XCTAssertEqualObjects(@"ConcreteA", concreteA.entity.name); + XCTAssertEqualObjects(@1, concreteA.identifier); + XCTAssertEqualObjects(@"this is A", concreteA.foo); + + GRTConcreteB *concreteB = container.abstracts[1]; + XCTAssertEqualObjects(@"ConcreteB", concreteB.entity.name); + XCTAssertEqualObjects(@2, concreteB.identifier); + XCTAssertEqualObjects(@"this is B", concreteB.bar); + + NSDictionary *updateConcreteA = @{ + @"id": @1, + @"foo": @"A has been updated" }; - XCTAssertThrows([GRTJSONSerialization mergeObjectForEntityName:@"Character" fromJSONDictionary:batman inManagedObjectContext:self.context error:NULL], @"merge should assert when the identity attribute doesn't have a JSON key path"); + concreteA = [GRTJSONSerialization objectWithEntityName:@"Abstract" fromJSONDictionary:updateConcreteA inContext:self.context error:&error]; + XCTAssertNil(error); + XCTAssertEqualObjects(@"ConcreteA", concreteA.entity.name); + XCTAssertEqualObjects(@1, concreteA.identifier); + XCTAssertEqualObjects(@"A has been updated", concreteA.foo); } -- (void)testJSONDictionaryFromManagedObject { +- (void)testSerializationToJSON { GRTPublisher *dc = [NSEntityDescription insertNewObjectForEntityForName:@"Publisher" inManagedObjectContext:self.context]; dc.identifier = @10; dc.name = @"DC Comics"; @@ -366,13 +448,17 @@ - (void)testJSONDictionaryFromManagedObject { batman.powers = [[NSOrderedSet alloc] initWithArray:@[agility, wealth]]; batman.publisher = dc; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; + GRTCharacter *ironMan = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; + ironMan.name = @"Iron Man"; + + NSArray *JSONArray = [GRTJSONSerialization JSONArrayFromObjects:@[batman, ironMan]]; - NSDictionary *expectedDictionary = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ + NSArray *expectedArray = @[ + @{ + @"id": @"1699", + @"name": @"Batman", + @"real_name": @"Bruce Wayne", + @"powers": @[ @{ @"id": @"4", @"name": @"Agility" @@ -381,49 +467,46 @@ - (void)testJSONDictionaryFromManagedObject { @"id": @"9", @"name": @"Insanely Rich" } - ], - @"publisher": @{ - @"id": @"10", - @"name": @"DC Comics" + ], + @"publisher": @{ + @"id": @"10", + @"name": @"DC Comics" + } + }, + @{ + @"id": NSNull.null, + @"name": @"Iron Man", + @"real_name": NSNull.null, + @"powers": @[], + @"publisher": NSNull.null } - }; - - XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize an initialized managed object to JSON dictionary"); -} - -- (void)testJSONDictionaryFromEmptyManagedObject { - GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; + ]; - NSDictionary *expectedDictionary = @{ - @"id": NSNull.null, - @"name": NSNull.null, - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - }; - - XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize an uninitialized managed object to JSON dictionary"); + XCTAssertEqualObjects(expectedArray, JSONArray, @"should serialize managed objects to JSON array"); } -- (void)testJSONDictionaryFromManagedObjectWithNestedDictionaries { +- (void)testSerializationToJSONWithNestedDictionaries { NSEntityDescription *entity = self.store.managedObjectModel.entitiesByName[@"Character"]; NSAttributeDescription *realNameAttribute = entity.attributesByName[@"realName"]; realNameAttribute.userInfo = @{ - @"JSONKeyPath": @"real_name.name" + @"JSONKeyPath": @"real_name.foo.bar.name" }; GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; batman.realName = @"Bruce Wayne"; - NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:batman]; + NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromObject:batman]; NSDictionary *expectedDictionary = @{ @"id": NSNull.null, @"name": NSNull.null, @"real_name": @{ - @"name": @"Bruce Wayne" + @"foo": @{ + @"bar": @{ + @"name": @"Bruce Wayne" + } + } }, @"powers": @[], @"publisher": NSNull.null @@ -432,33 +515,4 @@ - (void)testJSONDictionaryFromManagedObjectWithNestedDictionaries { XCTAssertEqualObjects(expectedDictionary, JSONDictionary, @"should serialize attributes with complex JSON key paths"); } -- (void)testJSONArrayFromManagedObjects { - GRTCharacter *batman = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - batman.name = @"Batman"; - - GRTCharacter *ironMan = [NSEntityDescription insertNewObjectForEntityForName:@"Character" inManagedObjectContext:self.context]; - ironMan.name = @"Iron Man"; - - NSArray *JSONArray = [GRTJSONSerialization JSONArrayFromManagedObjects:@[batman, ironMan]]; - - NSArray *expectedArray = @[ - @{ - @"id": NSNull.null, - @"name": @"Batman", - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - }, - @{ - @"id": NSNull.null, - @"name": @"Iron Man", - @"real_name": NSNull.null, - @"powers": @[], - @"publisher": NSNull.null - } - ]; - - XCTAssertEqualObjects(expectedArray, JSONArray, @"should serialize managed objects to JSON array"); -} - @end diff --git a/GrootTests/GRTManagedStoreTests.m b/GrootTests/GRTManagedStoreTests.m new file mode 100644 index 0000000..61af6eb --- /dev/null +++ b/GrootTests/GRTManagedStoreTests.m @@ -0,0 +1,78 @@ +// +// GRTManagedStoreTests.m +// Groot +// +// Created by Guillermo Gonzalez on 08/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import +#import + +#import "GRTModels.h" + +@interface GRTManagedStoreTests : XCTestCase + +@property (copy, nonatomic, nonnull) NSMutableArray *fileURLs; + +@end + +@implementation GRTManagedStoreTests + +- (NSArray * __nonnull)fileURLs { + if (_fileURLs == nil) { + _fileURLs = [NSMutableArray array]; + } + + return _fileURLs; +} + +- (void)tearDown { + for (NSURL *URL in self.fileURLs) { + [[NSFileManager defaultManager] removeItemAtURL:URL error:nil]; + } + [super tearDown]; +} + +- (void)testInMemoryStore { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithModel:model error:&error]; + XCTAssertNil(error); + + NSPersistentStore *persistentStore = store.persistentStoreCoordinator.persistentStores[0]; + XCTAssertEqual(NSInMemoryStoreType, persistentStore.type); +} + +- (void)testStoreWithCacheName { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithCacheName:@"Test.data" model:model error:&error]; + XCTAssertNil(error); + + NSString *bundleIdentifier = [NSBundle bundleForClass:[GRTManagedStore class]].bundleIdentifier; + NSURL *expectedURL = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:&error]; + XCTAssertNil(error); + + expectedURL = [expectedURL URLByAppendingPathComponent:bundleIdentifier]; + expectedURL = [expectedURL URLByAppendingPathComponent:@"Test.data"]; + XCTAssertEqualObjects(expectedURL, store.URL); + + [self.fileURLs addObject:store.URL]; +} + +- (void)testContextWithConcurrencyType { + NSManagedObjectModel *model = [NSManagedObjectModel grt_testModel]; + + NSError *error = nil; + GRTManagedStore *store = [[GRTManagedStore alloc] initWithModel:model error:&error]; + XCTAssertNil(error); + + NSManagedObjectContext *context = [store contextWithConcurrencyType:NSMainQueueConcurrencyType]; + XCTAssertEqualObjects(store.persistentStoreCoordinator, context.persistentStoreCoordinator); + XCTAssertEqual(NSMainQueueConcurrencyType, context.concurrencyType); +} + +@end diff --git a/GrootTests/GRTModels.h b/GrootTests/GRTModels.h index d504eae..dfaf5c0 100644 --- a/GrootTests/GRTModels.h +++ b/GrootTests/GRTModels.h @@ -8,6 +8,8 @@ #import +NS_ASSUME_NONNULL_BEGIN + @class GRTPower, GRTPublisher; @interface GRTCharacter : NSManagedObject @@ -15,24 +17,56 @@ @property (nonatomic, retain) NSNumber *identifier; @property (nonatomic, retain) NSString *name; @property (nonatomic, retain) NSString *realName; -@property (nonatomic, retain) NSDate *birthday; -@property (nonatomic, retain) NSOrderedSet *powers; -@property (nonatomic, retain) GRTPublisher *publisher; +@property (nonatomic, retain, nullable) NSOrderedSet *powers; +@property (nonatomic, retain, nullable) GRTPublisher *publisher; @end -@interface GRTPower : NSManagedObject +@interface GRTPublisher : NSManagedObject @property (nonatomic, retain) NSNumber *identifier; @property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSSet *characters; +@property (nonatomic, retain, nullable) NSSet *characters; @end -@interface GRTPublisher : NSManagedObject +@interface GRTPower : NSManagedObject @property (nonatomic, retain) NSNumber *identifier; @property (nonatomic, retain) NSString *name; -@property (nonatomic, retain) NSSet *characters; +@property (nonatomic, retain, nullable) NSSet *characters; + +@end + +@interface GRTContainer : NSManagedObject + +@property (nonatomic, retain, nullable) NSOrderedSet *abstracts; + +@end + +@interface GRTAbstract : NSManagedObject + +@property (nonatomic, retain) NSNumber *identifier; +@property (nonatomic, retain, nullable) GRTContainer *container; @end + +@interface GRTConcreteA : GRTAbstract + +@property (nonatomic, retain) NSString *foo; + +@end + +@interface GRTConcreteB : GRTAbstract + +@property (nonatomic, retain) NSString *bar; + +@end + +@interface NSManagedObjectModel (GrootTests) + ++ (nonnull instancetype)grt_testModel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GrootTests/GRTModels.m b/GrootTests/GRTModels.m index ad291f5..307d9fc 100644 --- a/GrootTests/GRTModels.m +++ b/GrootTests/GRTModels.m @@ -13,7 +13,6 @@ @implementation GRTCharacter @dynamic identifier; @dynamic name; @dynamic realName; -@dynamic birthday; @dynamic powers; @dynamic publisher; @@ -34,3 +33,37 @@ @implementation GRTPublisher @dynamic characters; @end + +@implementation GRTContainer + +@dynamic abstracts; + +@end + +@implementation GRTAbstract + +@dynamic identifier; +@dynamic container; + +@end + +@implementation GRTConcreteA + +@dynamic foo; + +@end + +@implementation GRTConcreteB + +@dynamic bar; + +@end + +@implementation NSManagedObjectModel (GrootTests) + ++ (nonnull instancetype)grt_testModel { + NSBundle *bundle = [NSBundle bundleForClass:[GRTCharacter class]]; + return [NSManagedObjectModel mergedModelFromBundles:@[bundle]]; +} + +@end diff --git a/GrootTests/GRTValueTransformerTests.m b/GrootTests/GRTValueTransformerTests.m deleted file mode 100644 index c3b1a23..0000000 --- a/GrootTests/GRTValueTransformerTests.m +++ /dev/null @@ -1,53 +0,0 @@ -// -// GRTValueTransformerTests.m -// Groot -// -// Created by guille on 11/07/14. -// Copyright (c) 2014 Guillermo Gonzalez. All rights reserved. -// - -#import -#import - -@interface GRTValueTransformerTests : XCTestCase - -@end - -@implementation GRTValueTransformerTests - -- (void)testTransformerWithBlock { - GRTValueTransformer *transformer = [GRTValueTransformer transformerWithBlock:^id(NSNumber *value) { - return [value stringValue]; - }]; - - XCTAssertFalse([transformer.class allowsReverseTransformation], @"should not allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the transform block"); -} - -- (void)testReversibleTransformerWithBlock { - GRTValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithBlock:^id(id value) { - if ([value isKindOfClass:NSNumber.class]) { - return [value stringValue]; - } else { - return @([value integerValue]); - } - }]; - - XCTAssertTrue([transformer.class allowsReverseTransformation], @"should allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the transform block"); - XCTAssertEqualObjects(@42, [transformer reverseTransformedValue:@"42"], @"should call the transform block"); -} - -- (void)testReversibleTransformerWithForwardAndReverseBlock { - GRTValueTransformer *transformer = [GRTValueTransformer reversibleTransformerWithForwardBlock:^id(NSNumber *value) { - return [value stringValue]; - } reverseBlock:^id(NSString *value) { - return @([value integerValue]); - }]; - - XCTAssertTrue([transformer.class allowsReverseTransformation], @"should allow reverse transformation"); - XCTAssertEqualObjects(@"42", [transformer transformedValue:@42], @"should call the forward block"); - XCTAssertEqualObjects(@42, [transformer reverseTransformedValue:@"42"], @"should call the reverse block"); -} - -@end diff --git a/GrootTests/JSON/characters.json b/GrootTests/JSON/characters.json new file mode 100755 index 0000000..b38138f --- /dev/null +++ b/GrootTests/JSON/characters.json @@ -0,0 +1,12 @@ +[ + { + "id": "1699", + "name": "Batman", + "real_name": "Guille Gonzalez" + }, + { + "id": "1700", + "name": "Superman", + "real_name": "Franck Letellier" + } +] diff --git a/GrootTests/JSON/characters_update.json b/GrootTests/JSON/characters_update.json new file mode 100755 index 0000000..c6dbcb6 --- /dev/null +++ b/GrootTests/JSON/characters_update.json @@ -0,0 +1,15 @@ +[ + { + "id": "1701", + "name": "Spiderman", + "_comment": "Should return a validation error when merging" + }, + { + "id": "1699", + "real_name": "Bruce Wayne" + }, + { + "id": "1700", + "real_name": "Clark Kent" + } +] diff --git a/GrootTests/JSON/container.json b/GrootTests/JSON/container.json new file mode 100644 index 0000000..6bd3955 --- /dev/null +++ b/GrootTests/JSON/container.json @@ -0,0 +1,14 @@ +{ + "abstracts": [ + { + "id": 1, + "type": "A", + "foo": "this is A" + }, + { + "id": 2, + "type": "B", + "bar": "this is B" + } + ] +} diff --git a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents index ada87bd..ff2d2b9 100644 --- a/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents +++ b/GrootTests/Model.xcdatamodeld/Model.xcdatamodel/contents @@ -1,53 +1,110 @@ - + + + + + + + + + + + + + - + - + - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + + + + + + - - + - + + + + + + - - - - + + + + + + + \ No newline at end of file diff --git a/GrootTests/NSData+Resource.h b/GrootTests/NSData+Resource.h new file mode 100644 index 0000000..707a539 --- /dev/null +++ b/GrootTests/NSData+Resource.h @@ -0,0 +1,19 @@ +// +// NSData+Resource.h +// Groot +// +// Created by Guillermo Gonzalez on 15/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSData (Resource) + ++ (nullable instancetype)grt_dataWithContentsOfResource:(NSString *)resource; + +@end + +NS_ASSUME_NONNULL_END diff --git a/GrootTests/NSData+Resource.m b/GrootTests/NSData+Resource.m new file mode 100644 index 0000000..2c65ad0 --- /dev/null +++ b/GrootTests/NSData+Resource.m @@ -0,0 +1,26 @@ +// +// NSData+Resource.m +// Groot +// +// Created by Guillermo Gonzalez on 15/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +#import "NSData+Resource.h" + +@interface Dummy : NSObject +@end + +@implementation Dummy +@end + +@implementation NSData (Resource) + ++ (nullable instancetype)grt_dataWithContentsOfResource:(NSString * __nonnull)resource { + NSBundle *bundle = [NSBundle bundleForClass:[Dummy class]]; + NSString *path = [bundle pathForResource:[resource stringByDeletingPathExtension] ofType:resource.pathExtension]; + + return [self dataWithContentsOfFile:path]; +} + +@end diff --git a/GrootTests/NSValueTransformerTests.swift b/GrootTests/NSValueTransformerTests.swift new file mode 100644 index 0000000..9c0ed32 --- /dev/null +++ b/GrootTests/NSValueTransformerTests.swift @@ -0,0 +1,71 @@ +// +// NSValueTransformerTests.swift +// Groot +// +// Created by Guillermo Gonzalez on 08/07/15. +// Copyright (c) 2015 Guillermo Gonzalez. All rights reserved. +// + +import XCTest +import Groot + +class NSValueTransformerTests: XCTestCase { + + func testValueTransformer() { + func toString(value: Int) -> String? { + return "\(value)" + } + + NSValueTransformer.setValueTransformerWithName("testTransformer", transform: toString) + let transformer = NSValueTransformer(forName: "testTransformer")! + + XCTAssertFalse(transformer.dynamicType.allowsReverseTransformation(), "should not allow reverse transformation") + XCTAssertEqual("42", transformer.transformedValue(42) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.transformedValue("unexpected"), "should handle unsupported values") + } + + func testReversibleValueTransformer() { + func toString(value: Int) -> String? { + return "\(value)" + } + + func toInt(value: String) -> Int? { + return value.toInt() + } + + NSValueTransformer.setValueTransformerWithName("testReversibleTransformer", transform: toString, reverseTransform: toInt) + let transformer = NSValueTransformer(forName: "testReversibleTransformer")! + + XCTAssertTrue(transformer.dynamicType.allowsReverseTransformation(), "should not allow reverse transformation") + XCTAssertEqual("42", transformer.transformedValue(42) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.transformedValue("unexpected"), "should handle unsupported values") + XCTAssertEqual(42, transformer.reverseTransformedValue("42") as! Int, "should call the reverse transform function") + XCTAssertNil(transformer.reverseTransformedValue(nil), "should handle nil values") + XCTAssertNil(transformer.reverseTransformedValue("not a number"), "should handle unsupported values") + } + + func testEntityMapper() { + func entityForJSONDictionary(dictionary: [String: AnyObject]) -> String? { + if let type = dictionary["type"] as? String { + switch type { + case "A": + return "ConcreteA" + case "B": + return "ConcreteB" + default: + return nil + } + } + return nil + } + + NSValueTransformer.setEntityMapperWithName("testEntityMapper", map: entityForJSONDictionary) + + let transformer = NSValueTransformer(forName: "testEntityMapper")! + XCTAssertEqual("ConcreteA", transformer.transformedValue(["type": "A"]) as! String, "should call the transform function") + XCTAssertEqual("ConcreteB", transformer.transformedValue(["type": "B"]) as! String, "should call the transform function") + XCTAssertNil(transformer.transformedValue(nil), "should handle nil values") + } +} diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 5ed077c..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -Groot - -Copyright (c) 2014 Guillermo Gonzalez - -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, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..55538c3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,9 @@ +**Groot** + +**Copyright (c) Guillermo Gonzalez** + +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, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 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. diff --git a/README.md b/README.md index aa1d17a..c12ae0c 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,37 @@ # Groot -With Groot you can convert JSON dictionaries and arrays to and from Core Data managed objects. +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![CocoaPods compatible](https://img.shields.io/cocoapods/v/Groot.svg) -## Requirements -Groot supports OS X 10.8+ and iOS 6.0+. +Groot provides a simple way of serializing Core Data object graphs from or into JSON. + +Groot uses [annotations](Documentation/Annotations.md) in the Core Data model to perform the serialization and provides the following features: + +1. Attribute and relationship mapping to JSON key paths. +2. Value transformation using named `NSValueTransformer` objects. +3. Object graph preservation. +4. Support for entity inheritance + +## Installing Groot + +##### Using CocoaPods -## Installation -### Cocoapods Add the following to your `Podfile`: ``` ruby -pod 'Groot' +pod ‘Groot’ +``` + +Or, if you need to support iOS 6 / OS X 10.8: + +``` ruby +pod ‘Groot/ObjC’ ``` Then run `$ pod install`. -If you don't have CocoaPods installed or integrated into your project, you can learn how to do so [here](http://cocoapods.org). +If you don’t have CocoaPods installed or integrated into your project, you can learn how to do so [here](http://cocoapods.org). + +##### Using Carthage -### Carthage Add the following to your `Cartfile`: ``` @@ -27,111 +42,223 @@ Then run `$ carthage update`. Follow the instructions in [Carthage’s README](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application]) to add the framework to your project. -## Usage -Suppose we would like to convert the JSON returned by a Comic Database web service into our own model objects. The JSON could look something like this: +You may need to set **Embedded Content Contains Swift Code** to **YES** in the build settings for targets that only contain Objective-C code. + +## Getting started + +Consider the following JSON describing a well-known comic book character: ```json -[ - { - "id": "1699", - "name": "Batman", - "powers": [ - { - "id": "4", - "name": "Agility" - }, - { - "id": "9", - "name": "Insanely Rich" - } - ], - "publisher": { - "id": "10", - "name": "DC Comics" +{ + "id": "1699", + "name": "Batman", + "real_name": "Bruce Wayne", + "powers": [ + { + "id": "4", + "name": "Agility" }, - "real_name": "Bruce Wayne" - }, - ... -] + { + "id": "9", + "name": "Insanely Rich" + } + ], + "publisher": { + "id": "10", + "name": "DC Comics" + } +} ``` -We could model this in Core Data using 3 entities: Character, Power and Publisher. +We could translate this into a Core Data model using three entities: `Character`, `Power` and `Publisher`. + +Model + + +### Mapping attributes and relationships + +Groot relies on the presence of certain key-value pairs in the user info dictionary associated with entities, attributes and relationships to serialize managed objects from or into JSON. These key-value pairs are often referred in the documentation as [annotations](Documentation/Annotations.md). -![Model](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/sample-model.jpg) +In our example, we should add a `JSONKeyPath` in the user info dictionary of each attribute and relationship specifying the corresponding key path in the JSON: -Note that we don't need to name our attributes as in the JSON. The serialization process can be customized by adding certain information to the user dictionary provided in Core Data *entities*, *attributes* and *relationships*. +* `id` for the `identifier` attribute, +* `name` for the `name` attribute, +* `real_name` for the `realName` attribute, +* `powers` for the `powers` relationship, +* `publisher` for the `publisher` relationship, +* etc. -For instance, we can specify that the `identifier` attribute will be mapped from the `id` JSON key path, and that its value will be transformed using an `NSValueTransformer` named *GRTTestTransfomer*. +Attributes and relationships that don't have a `JSONKeyPath` entry are **not considered** for JSON serialization or deserialization. -![Property User Info](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/property-userInfo.jpg) +### Value transformers -Now we can easily convert JSON data and insert the corresponding managed objects with a simple method call: +When we created the model we decided to use `Integer 64` for our `identifier` attributes. The problem is that, for compatibility reasons, the JSON uses strings for `id` values. + +We can add a `JSONTransformerName` entry to each `identifier` attribute's user info dictionary specifying the name of a value transformer that converts strings to numbers. + +Groot provides a simple way for creating and registering named value transformers: + +```swift +// Swift + +func toString(value: Int) -> String? { + return "\(value)" +} + +func toInt(value: String) -> Int? { + return value.toInt() +} + +NSValueTransformer.setValueTransformerWithName("StringToInteger", transform: toString, reverseTransform: toInt) +``` ```objc -NSDictionary *batmanJSON = @{ - @"id": @"1699", - @"name": @"Batman", - @"real_name": @"Bruce Wayne", - @"powers": @[ - @{ - @"id": @"4", - @"name": @"Agility" - }, - @{ - @"id": @"9", - @"name": @"Insanely Rich" - }], - @"publisher": @{ - @"id": @"10", - @"name": @"DC Comics" - } -}; +// Objective-C -NSError *error = nil; -NSManagedObject *batman = [GRTJSONSerialization insertObjectForEntityName:@"Character" - fromJSONDictionary:batmanJSON - inManagedObjectContext:context - error:&error]; +[NSValueTransformer grt_setValueTransformerWithName:@"StringToInteger" transformBlock:^id(NSString *value) { + return @([value integerValue]); +} reverseTransformBlock:^id(NSNumber *value) { + return [value stringValue]; +}]; ``` -### Merging data +### Object graph preservation + +To preserve the object graph and avoid duplicating information when serializing managed objects from JSON, Groot needs to know how to uniquely identify your model objects. + +In our example, we should add an `identityAttribute` entry to the `Character`, `Power` and `Publisher` entities user dictionaries with the value `identifier`. -When inserting data, Groot does not check if the serialized managed objects already exist and simply treats them as new. +For more information about annotating your model have a look at [Annotations](Documentation/Annotations.md). -If instead, you would like to merge (that is, create or update) the serialized managed objects, then you need to tell Groot how to uniquely identify your model objects. You can do that by associating the `identityAttribute` key with the name of an attribute in the *entity* user info dictionary. +### Serializing from JSON -![Entity User Info](https://raw.githubusercontent.com/gonzalezreal/Groot/master/Images/entity-userInfo.jpg) +Now that we have our Core Data model ready we can start adding some data. -In our sample, all of our models are identified by the `identifier` attribute. +```swift +// Swift -Now we can update the Batman character we just inserted in the previous snippet: +let batmanJSON: JSONObject = [ + "name": "Batman", + "id": "1699", + "powers": [ + [ + "id": "4", + "name": "Agility" + ], + [ + "id": "9", + "name": "Insanely Rich" + ] + ], + "publisher": [ + "id": "10", + "name": "DC Comics" + ] +] + +let batman: Character = objectFromJSONDictionary(batmanJSON, + inContext: context, mergeChanges: false, error: &error) +``` ```objc +// Objective-C + +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; +``` + +If we want to update the object we just created, Groot can merge the changes for us: + +```objc +// Objective-C + NSDictionary *updateJSON = @{ - @"id": @"1699", - @"real_name": @"Guille Gonzalez" -} + @"id": @"1699", + @"real_name": @"Bruce Wayne", +}; // This will return the previously created managed object -NSManagedObject *batman = [GRTJSONSerialization mergeObjectForEntityName:@"Character" - fromJSONDictionary:batmanJSON - inManagedObjectContext:context - error:NULL]; +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:updateJSON inContext:self.context error:&error]; ``` -If you want to merge a JSON array, its better to call `mergeObjectsForEntityName:fromJSONArray:inManagedObjectContext:error:`. This method will perform a single fetch per entity regardless of the number of objects in the JSON array. +#### Serializing relationships from identifiers -### Back to JSON +Suppose that our API does not return full objects for the relationships but only the identifiers. -You can convert managed objects into their JSON representations by using `JSONDictionaryFromManagedObject:` or `JSONArrayFromManagedObjects:`. +We don't need to change our model to support this situation: ```objc -NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromManagedObject:someManagedObject]; +// Objective-C + +NSDictionary *batmanJSON = @{ + @"name": @"Batman", + @"real_name": @"Bruce Wayne", + @"id": @"1699", + @"powers": @[@"4", @"9"], + @"publisher": @"10" +}; + +Character *batman = [GRTJSONSerialization objectWithEntityName:@"Character" fromJSONDictionary:batmanJSON inContext:self.context error:&error]; ``` +The above code creates a full `Character` object and the corresponding relationships pointing to `Power` and `Publisher` objects that just have the identifier attribute populated. + +We can import powers and publisher from different JSON objects and Groot will merge them nicely: + +```objc +// Objective-C + +NSArray *powersJSON = @[ + @{ + @"id": @"4", + @"name": @"Agility" + }, + @{ + @"id": @"9", + @"name": @"Insanely Rich" + } +]; + +[GRTJSONSerialization objectsWithEntityName:@"Power" fromJSONArray:powersJSON inContext:self.context error:&error]; + +NSDictionary *publisherJSON = @{ + @"id": @"10", + @"name": @"DC Comics" +}; + +[GRTJSONSerialization objectWithEntityName:@"Publisher" fromJSONDictionary:publisherJSON inContext:self.context error:&error]; +``` + +For more serialization methods check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). + +### Entity inheritance + +Groot supports entity inheritance via the [entityMapperName](Documentation/Annotations.md#entitymappername) annotation. + +If you are using SQLite as your persistent store, Core Data implements entity inheritance by creating one table for the parent entity and all child entities, with a superset of all their attributes. This can obviously have unintended performance consequences if you have a lot of data in the entities, so use this feature wisely. + +### Serializing to JSON + +Groot provides methods to serialize managed objects back to JSON: + +```swift +// Swift + +let JSONDictionary = JSONDictionaryFromObject(batman) +``` + +```objc +// Objective-C + +NSDictionary *JSONDictionary = [GRTJSONSerialization JSONDictionaryFromObject:batman]; +``` + +For more serialization methods check [GRTJSONSerialization.h](Groot/GRTJSONSerialization.h) and [Groot.swift](Groot/Groot.swift). + ## Contact + [Guillermo Gonzalez](http://github.com/gonzalezreal) [@gonzalezreal](https://twitter.com/gonzalezreal) ## License -Groot is available under the MIT license. See [LICENSE](https://github.com/gonzalezreal/Groot/blob/master/LICENSE). + +Groot is available under the [MIT license](LICENSE.md). \ No newline at end of file