├── .gitignore ├── .travis.yml ├── CoreDataDandy.podspec ├── Framework ├── CoreDataDandy.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── CoreDataDandy.xcscheme └── CoreDataDandy │ ├── CoreDataDandy.h │ └── Info.plist ├── LICENSE ├── README.md ├── Sources ├── Constants.swift ├── Core │ ├── CoreDataDandy.swift │ ├── EntityMapper.swift │ ├── Logger.swift │ ├── MappingFinalizer.swift │ ├── ObjectFactory.swift │ ├── PersistentStackCoordinator.swift │ ├── PropertyDescription.swift │ └── Serializer.swift ├── Extensions │ ├── Dictionary+Dandy.swift │ ├── NSEntityDescription+Dandy.swift │ ├── NSFileManager+Directory.swift │ └── NSManagedObject+Dandy.swift └── Value Conversion │ ├── ConvertibleType.swift │ ├── CoreDataValueConverter.swift │ └── ValueConverter.swift ├── Tests ├── .swiftlint.yml ├── CoreDataDandyTests.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── CoreDataDandyTests.xcscheme └── CoreDataDandyTests │ ├── CoreDataDandyTests.swift │ ├── DandyModel.xcdatamodeld │ └── DandyModel.xcdatamodel │ │ └── contents │ ├── Info.plist │ └── Models │ ├── Conclusion+CoreDataClass.swift │ ├── Conclusion+CoreDataProperties.swift │ ├── Conclusion+Finalization.swift │ ├── Dandy_+CoreDataClass.swift │ ├── Dandy_+CoreDataProperties.swift │ ├── Flattery+CoreDataClass.swift │ ├── Flattery+CoreDataProperties.swift │ ├── Gossip+CoreDataClass.swift │ ├── Gossip+CoreDataProperties.swift │ ├── Hat+CoreDataClass.swift │ ├── Hat+CoreDataProperties.swift │ ├── Material+CoreDataClass.swift │ ├── Material+CoreDataProperties.swift │ ├── Plebian+CoreDataClass.swift │ ├── Plebian+CoreDataProperties.swift │ ├── Slander+CoreDataClass.swift │ ├── Slander+CoreDataProperties.swift │ ├── Space+CoreDataClass.swift │ └── Space+CoreDataProperties.swift └── header.png /.gitignore: -------------------------------------------------------------------------------- 1 | # From Alamofire: https://github.com/Alamofire/Alamofire/blob/master/.gitignore#L11 2 | 3 | # Mac OS X 4 | .DS_Store 5 | 6 | # Xcode 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | 24 | # SPM 25 | .build/ 26 | 27 | # Carthage 28 | Carthage/Build -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: objective-c 2 | osx_image: xcode8 3 | env: 4 | global: 5 | - PROJECT=Tests/CoreDataDandyTests.xcodeproj 6 | - SCHEME=CoreDataDandyTests 7 | - DESTINATION="OS=10.0,name=iPhone 7" 8 | script: 9 | - set -o pipefail 10 | - xcodebuild -version 11 | - xcodebuild -showsdks 12 | - xcodebuild -project "$PROJECT" -scheme "$SCHEME" -destination "$DESTINATION" -sdk iphonesimulator test -------------------------------------------------------------------------------- /CoreDataDandy.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'CoreDataDandy' 3 | s.version = '0.6.1' 4 | s.summary = 'A feature-light wrapper around Core Data that simplifies common database operations.' 5 | s.description = 'Initializes your Core Data stack, manages your saves, inserts, and fetches, and maps json to NSManagedObjects.' 6 | s.homepage = 'https://github.com/fuzz-productions/CoreDataDandy' 7 | s.license = 'MIT' 8 | s.author = { 'Noah Blake' => 'noah@fuzzproductions.com' } 9 | s.source = { :git => "https://github.com/fuzz-productions/CoreDataDandy.git", :tag => s.version.to_s } 10 | s.social_media_url = 'https://twitter.com/fuzzpro' 11 | 12 | s.platform = :ios, '9.0' 13 | s.requires_arc = true 14 | 15 | s.source_files = 'Sources/**/*' 16 | 17 | s.frameworks = 'CoreData' 18 | end 19 | -------------------------------------------------------------------------------- /Framework/CoreDataDandy.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 98345A3C1DCCE7C600E8AF88 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A291DCCE7C600E8AF88 /* Constants.swift */; }; 11 | 98345A3D1DCCE7C600E8AF88 /* CoreDataDandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A2B1DCCE7C600E8AF88 /* CoreDataDandy.swift */; }; 12 | 98345A3E1DCCE7C600E8AF88 /* EntityMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A2C1DCCE7C600E8AF88 /* EntityMapper.swift */; }; 13 | 98345A3F1DCCE7C600E8AF88 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A2D1DCCE7C600E8AF88 /* Logger.swift */; }; 14 | 98345A401DCCE7C600E8AF88 /* MappingFinalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A2E1DCCE7C600E8AF88 /* MappingFinalizer.swift */; }; 15 | 98345A411DCCE7C600E8AF88 /* ObjectFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A2F1DCCE7C600E8AF88 /* ObjectFactory.swift */; }; 16 | 98345A421DCCE7C600E8AF88 /* PersistentStackCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A301DCCE7C600E8AF88 /* PersistentStackCoordinator.swift */; }; 17 | 98345A431DCCE7C600E8AF88 /* PropertyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A311DCCE7C600E8AF88 /* PropertyDescription.swift */; }; 18 | 98345A441DCCE7C600E8AF88 /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A321DCCE7C600E8AF88 /* Serializer.swift */; }; 19 | 98345A451DCCE7C600E8AF88 /* Dictionary+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A341DCCE7C600E8AF88 /* Dictionary+Dandy.swift */; }; 20 | 98345A461DCCE7C600E8AF88 /* NSEntityDescription+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A351DCCE7C600E8AF88 /* NSEntityDescription+Dandy.swift */; }; 21 | 98345A471DCCE7C600E8AF88 /* NSFileManager+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A361DCCE7C600E8AF88 /* NSFileManager+Directory.swift */; }; 22 | 98345A481DCCE7C600E8AF88 /* NSManagedObject+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A371DCCE7C600E8AF88 /* NSManagedObject+Dandy.swift */; }; 23 | 98345A491DCCE7C600E8AF88 /* ConvertibleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A391DCCE7C600E8AF88 /* ConvertibleType.swift */; }; 24 | 98345A4A1DCCE7C600E8AF88 /* CoreDataValueConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A3A1DCCE7C600E8AF88 /* CoreDataValueConverter.swift */; }; 25 | 98345A4B1DCCE7C600E8AF88 /* ValueConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98345A3B1DCCE7C600E8AF88 /* ValueConverter.swift */; }; 26 | 985CD4DC1C617024005EB9E2 /* CoreDataDandy.h in Headers */ = {isa = PBXBuildFile; fileRef = 985CD4DB1C617024005EB9E2 /* CoreDataDandy.h */; settings = {ATTRIBUTES = (Public, ); }; }; 27 | /* End PBXBuildFile section */ 28 | 29 | /* Begin PBXFileReference section */ 30 | 98345A291DCCE7C600E8AF88 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 31 | 98345A2B1DCCE7C600E8AF88 /* CoreDataDandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataDandy.swift; sourceTree = ""; }; 32 | 98345A2C1DCCE7C600E8AF88 /* EntityMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntityMapper.swift; sourceTree = ""; }; 33 | 98345A2D1DCCE7C600E8AF88 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 34 | 98345A2E1DCCE7C600E8AF88 /* MappingFinalizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappingFinalizer.swift; sourceTree = ""; }; 35 | 98345A2F1DCCE7C600E8AF88 /* ObjectFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectFactory.swift; sourceTree = ""; }; 36 | 98345A301DCCE7C600E8AF88 /* PersistentStackCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentStackCoordinator.swift; sourceTree = ""; }; 37 | 98345A311DCCE7C600E8AF88 /* PropertyDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertyDescription.swift; sourceTree = ""; }; 38 | 98345A321DCCE7C600E8AF88 /* Serializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializer.swift; sourceTree = ""; }; 39 | 98345A341DCCE7C600E8AF88 /* Dictionary+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Dandy.swift"; sourceTree = ""; }; 40 | 98345A351DCCE7C600E8AF88 /* NSEntityDescription+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSEntityDescription+Dandy.swift"; sourceTree = ""; }; 41 | 98345A361DCCE7C600E8AF88 /* NSFileManager+Directory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFileManager+Directory.swift"; sourceTree = ""; }; 42 | 98345A371DCCE7C600E8AF88 /* NSManagedObject+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Dandy.swift"; sourceTree = ""; }; 43 | 98345A391DCCE7C600E8AF88 /* ConvertibleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertibleType.swift; sourceTree = ""; }; 44 | 98345A3A1DCCE7C600E8AF88 /* CoreDataValueConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataValueConverter.swift; sourceTree = ""; }; 45 | 98345A3B1DCCE7C600E8AF88 /* ValueConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueConverter.swift; sourceTree = ""; }; 46 | 985CD4D81C617024005EB9E2 /* CoreDataDandy.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreDataDandy.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 985CD4DB1C617024005EB9E2 /* CoreDataDandy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CoreDataDandy.h; sourceTree = ""; }; 48 | 985CD4DD1C617024005EB9E2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | /* End PBXFileReference section */ 50 | 51 | /* Begin PBXFrameworksBuildPhase section */ 52 | 985CD4D41C617024005EB9E2 /* Frameworks */ = { 53 | isa = PBXFrameworksBuildPhase; 54 | buildActionMask = 2147483647; 55 | files = ( 56 | ); 57 | runOnlyForDeploymentPostprocessing = 0; 58 | }; 59 | /* End PBXFrameworksBuildPhase section */ 60 | 61 | /* Begin PBXGroup section */ 62 | 98345A281DCCE7C600E8AF88 /* Sources */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 98345A291DCCE7C600E8AF88 /* Constants.swift */, 66 | 98345A2A1DCCE7C600E8AF88 /* Core */, 67 | 98345A331DCCE7C600E8AF88 /* Extensions */, 68 | 98345A381DCCE7C600E8AF88 /* Value Conversion */, 69 | ); 70 | name = Sources; 71 | path = ../../Sources; 72 | sourceTree = ""; 73 | }; 74 | 98345A2A1DCCE7C600E8AF88 /* Core */ = { 75 | isa = PBXGroup; 76 | children = ( 77 | 98345A2B1DCCE7C600E8AF88 /* CoreDataDandy.swift */, 78 | 98345A2C1DCCE7C600E8AF88 /* EntityMapper.swift */, 79 | 98345A2D1DCCE7C600E8AF88 /* Logger.swift */, 80 | 98345A2E1DCCE7C600E8AF88 /* MappingFinalizer.swift */, 81 | 98345A2F1DCCE7C600E8AF88 /* ObjectFactory.swift */, 82 | 98345A301DCCE7C600E8AF88 /* PersistentStackCoordinator.swift */, 83 | 98345A311DCCE7C600E8AF88 /* PropertyDescription.swift */, 84 | 98345A321DCCE7C600E8AF88 /* Serializer.swift */, 85 | ); 86 | path = Core; 87 | sourceTree = ""; 88 | }; 89 | 98345A331DCCE7C600E8AF88 /* Extensions */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 98345A341DCCE7C600E8AF88 /* Dictionary+Dandy.swift */, 93 | 98345A351DCCE7C600E8AF88 /* NSEntityDescription+Dandy.swift */, 94 | 98345A361DCCE7C600E8AF88 /* NSFileManager+Directory.swift */, 95 | 98345A371DCCE7C600E8AF88 /* NSManagedObject+Dandy.swift */, 96 | ); 97 | path = Extensions; 98 | sourceTree = ""; 99 | }; 100 | 98345A381DCCE7C600E8AF88 /* Value Conversion */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 98345A391DCCE7C600E8AF88 /* ConvertibleType.swift */, 104 | 98345A3A1DCCE7C600E8AF88 /* CoreDataValueConverter.swift */, 105 | 98345A3B1DCCE7C600E8AF88 /* ValueConverter.swift */, 106 | ); 107 | path = "Value Conversion"; 108 | sourceTree = ""; 109 | }; 110 | 985CD4CE1C617024005EB9E2 = { 111 | isa = PBXGroup; 112 | children = ( 113 | 985CD4DA1C617024005EB9E2 /* CoreDataDandy */, 114 | 985CD4D91C617024005EB9E2 /* Products */, 115 | ); 116 | sourceTree = ""; 117 | }; 118 | 985CD4D91C617024005EB9E2 /* Products */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 985CD4D81C617024005EB9E2 /* CoreDataDandy.framework */, 122 | ); 123 | name = Products; 124 | sourceTree = ""; 125 | }; 126 | 985CD4DA1C617024005EB9E2 /* CoreDataDandy */ = { 127 | isa = PBXGroup; 128 | children = ( 129 | 985CD4DB1C617024005EB9E2 /* CoreDataDandy.h */, 130 | 985CD4DD1C617024005EB9E2 /* Info.plist */, 131 | 98345A281DCCE7C600E8AF88 /* Sources */, 132 | ); 133 | path = CoreDataDandy; 134 | sourceTree = ""; 135 | }; 136 | /* End PBXGroup section */ 137 | 138 | /* Begin PBXHeadersBuildPhase section */ 139 | 985CD4D51C617024005EB9E2 /* Headers */ = { 140 | isa = PBXHeadersBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 985CD4DC1C617024005EB9E2 /* CoreDataDandy.h in Headers */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | /* End PBXHeadersBuildPhase section */ 148 | 149 | /* Begin PBXNativeTarget section */ 150 | 985CD4D71C617024005EB9E2 /* CoreDataDandy */ = { 151 | isa = PBXNativeTarget; 152 | buildConfigurationList = 985CD4E01C617024005EB9E2 /* Build configuration list for PBXNativeTarget "CoreDataDandy" */; 153 | buildPhases = ( 154 | 985CD4D31C617024005EB9E2 /* Sources */, 155 | 985CD4D41C617024005EB9E2 /* Frameworks */, 156 | 985CD4D51C617024005EB9E2 /* Headers */, 157 | 985CD4D61C617024005EB9E2 /* Resources */, 158 | ); 159 | buildRules = ( 160 | ); 161 | dependencies = ( 162 | ); 163 | name = CoreDataDandy; 164 | productName = CoreDataDandy; 165 | productReference = 985CD4D81C617024005EB9E2 /* CoreDataDandy.framework */; 166 | productType = "com.apple.product-type.framework"; 167 | }; 168 | /* End PBXNativeTarget section */ 169 | 170 | /* Begin PBXProject section */ 171 | 985CD4CF1C617024005EB9E2 /* Project object */ = { 172 | isa = PBXProject; 173 | attributes = { 174 | LastUpgradeCheck = 0800; 175 | ORGANIZATIONNAME = "Fuzz Productions"; 176 | TargetAttributes = { 177 | 985CD4D71C617024005EB9E2 = { 178 | CreatedOnToolsVersion = 7.2; 179 | LastSwiftMigration = 0800; 180 | }; 181 | }; 182 | }; 183 | buildConfigurationList = 985CD4D21C617024005EB9E2 /* Build configuration list for PBXProject "CoreDataDandy" */; 184 | compatibilityVersion = "Xcode 3.2"; 185 | developmentRegion = English; 186 | hasScannedForEncodings = 0; 187 | knownRegions = ( 188 | en, 189 | ); 190 | mainGroup = 985CD4CE1C617024005EB9E2; 191 | productRefGroup = 985CD4D91C617024005EB9E2 /* Products */; 192 | projectDirPath = ""; 193 | projectRoot = ""; 194 | targets = ( 195 | 985CD4D71C617024005EB9E2 /* CoreDataDandy */, 196 | ); 197 | }; 198 | /* End PBXProject section */ 199 | 200 | /* Begin PBXResourcesBuildPhase section */ 201 | 985CD4D61C617024005EB9E2 /* Resources */ = { 202 | isa = PBXResourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | ); 206 | runOnlyForDeploymentPostprocessing = 0; 207 | }; 208 | /* End PBXResourcesBuildPhase section */ 209 | 210 | /* Begin PBXSourcesBuildPhase section */ 211 | 985CD4D31C617024005EB9E2 /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 98345A3C1DCCE7C600E8AF88 /* Constants.swift in Sources */, 216 | 98345A461DCCE7C600E8AF88 /* NSEntityDescription+Dandy.swift in Sources */, 217 | 98345A441DCCE7C600E8AF88 /* Serializer.swift in Sources */, 218 | 98345A411DCCE7C600E8AF88 /* ObjectFactory.swift in Sources */, 219 | 98345A4B1DCCE7C600E8AF88 /* ValueConverter.swift in Sources */, 220 | 98345A471DCCE7C600E8AF88 /* NSFileManager+Directory.swift in Sources */, 221 | 98345A491DCCE7C600E8AF88 /* ConvertibleType.swift in Sources */, 222 | 98345A3E1DCCE7C600E8AF88 /* EntityMapper.swift in Sources */, 223 | 98345A3D1DCCE7C600E8AF88 /* CoreDataDandy.swift in Sources */, 224 | 98345A4A1DCCE7C600E8AF88 /* CoreDataValueConverter.swift in Sources */, 225 | 98345A3F1DCCE7C600E8AF88 /* Logger.swift in Sources */, 226 | 98345A431DCCE7C600E8AF88 /* PropertyDescription.swift in Sources */, 227 | 98345A401DCCE7C600E8AF88 /* MappingFinalizer.swift in Sources */, 228 | 98345A451DCCE7C600E8AF88 /* Dictionary+Dandy.swift in Sources */, 229 | 98345A421DCCE7C600E8AF88 /* PersistentStackCoordinator.swift in Sources */, 230 | 98345A481DCCE7C600E8AF88 /* NSManagedObject+Dandy.swift in Sources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXSourcesBuildPhase section */ 235 | 236 | /* Begin XCBuildConfiguration section */ 237 | 985CD4DE1C617024005EB9E2 /* Debug */ = { 238 | isa = XCBuildConfiguration; 239 | buildSettings = { 240 | ALWAYS_SEARCH_USER_PATHS = NO; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BOOL_CONVERSION = YES; 246 | CLANG_WARN_CONSTANT_CONVERSION = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_EMPTY_BODY = YES; 249 | CLANG_WARN_ENUM_CONVERSION = YES; 250 | CLANG_WARN_INFINITE_RECURSION = YES; 251 | CLANG_WARN_INT_CONVERSION = YES; 252 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNREACHABLE_CODE = YES; 255 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 256 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 257 | COPY_PHASE_STRIP = NO; 258 | CURRENT_PROJECT_VERSION = 1; 259 | DEBUG_INFORMATION_FORMAT = dwarf; 260 | ENABLE_STRICT_OBJC_MSGSEND = YES; 261 | ENABLE_TESTABILITY = YES; 262 | GCC_C_LANGUAGE_STANDARD = gnu99; 263 | GCC_DYNAMIC_NO_PIC = NO; 264 | GCC_NO_COMMON_BLOCKS = YES; 265 | GCC_OPTIMIZATION_LEVEL = 0; 266 | GCC_PREPROCESSOR_DEFINITIONS = ( 267 | "DEBUG=1", 268 | "$(inherited)", 269 | ); 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 277 | MTL_ENABLE_DEBUG_INFO = YES; 278 | ONLY_ACTIVE_ARCH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VERSIONING_SYSTEM = "apple-generic"; 283 | VERSION_INFO_PREFIX = ""; 284 | }; 285 | name = Debug; 286 | }; 287 | 985CD4DF1C617024005EB9E2 /* Release */ = { 288 | isa = XCBuildConfiguration; 289 | buildSettings = { 290 | ALWAYS_SEARCH_USER_PATHS = NO; 291 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 292 | CLANG_CXX_LIBRARY = "libc++"; 293 | CLANG_ENABLE_MODULES = YES; 294 | CLANG_ENABLE_OBJC_ARC = YES; 295 | CLANG_WARN_BOOL_CONVERSION = YES; 296 | CLANG_WARN_CONSTANT_CONVERSION = YES; 297 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 303 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 304 | CLANG_WARN_UNREACHABLE_CODE = YES; 305 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 306 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 307 | COPY_PHASE_STRIP = NO; 308 | CURRENT_PROJECT_VERSION = 1; 309 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 310 | ENABLE_NS_ASSERTIONS = NO; 311 | ENABLE_STRICT_OBJC_MSGSEND = YES; 312 | GCC_C_LANGUAGE_STANDARD = gnu99; 313 | GCC_NO_COMMON_BLOCKS = YES; 314 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 315 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 316 | GCC_WARN_UNDECLARED_SELECTOR = YES; 317 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 318 | GCC_WARN_UNUSED_FUNCTION = YES; 319 | GCC_WARN_UNUSED_VARIABLE = YES; 320 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 321 | MTL_ENABLE_DEBUG_INFO = NO; 322 | SDKROOT = iphoneos; 323 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 324 | TARGETED_DEVICE_FAMILY = "1,2"; 325 | VALIDATE_PRODUCT = YES; 326 | VERSIONING_SYSTEM = "apple-generic"; 327 | VERSION_INFO_PREFIX = ""; 328 | }; 329 | name = Release; 330 | }; 331 | 985CD4E11C617024005EB9E2 /* Debug */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 335 | DEFINES_MODULE = YES; 336 | DYLIB_COMPATIBILITY_VERSION = 1; 337 | DYLIB_CURRENT_VERSION = 1; 338 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 339 | INFOPLIST_FILE = CoreDataDandy/Info.plist; 340 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 341 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 342 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 343 | PRODUCT_BUNDLE_IDENTIFIER = com.fuzz.CoreDataDandy; 344 | PRODUCT_NAME = "$(TARGET_NAME)"; 345 | SKIP_INSTALL = YES; 346 | SWIFT_VERSION = 3.0; 347 | }; 348 | name = Debug; 349 | }; 350 | 985CD4E21C617024005EB9E2 /* Release */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 354 | DEFINES_MODULE = YES; 355 | DYLIB_COMPATIBILITY_VERSION = 1; 356 | DYLIB_CURRENT_VERSION = 1; 357 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 358 | INFOPLIST_FILE = CoreDataDandy/Info.plist; 359 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 360 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 361 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 362 | PRODUCT_BUNDLE_IDENTIFIER = com.fuzz.CoreDataDandy; 363 | PRODUCT_NAME = "$(TARGET_NAME)"; 364 | SKIP_INSTALL = YES; 365 | SWIFT_VERSION = 3.0; 366 | }; 367 | name = Release; 368 | }; 369 | /* End XCBuildConfiguration section */ 370 | 371 | /* Begin XCConfigurationList section */ 372 | 985CD4D21C617024005EB9E2 /* Build configuration list for PBXProject "CoreDataDandy" */ = { 373 | isa = XCConfigurationList; 374 | buildConfigurations = ( 375 | 985CD4DE1C617024005EB9E2 /* Debug */, 376 | 985CD4DF1C617024005EB9E2 /* Release */, 377 | ); 378 | defaultConfigurationIsVisible = 0; 379 | defaultConfigurationName = Release; 380 | }; 381 | 985CD4E01C617024005EB9E2 /* Build configuration list for PBXNativeTarget "CoreDataDandy" */ = { 382 | isa = XCConfigurationList; 383 | buildConfigurations = ( 384 | 985CD4E11C617024005EB9E2 /* Debug */, 385 | 985CD4E21C617024005EB9E2 /* Release */, 386 | ); 387 | defaultConfigurationIsVisible = 0; 388 | defaultConfigurationName = Release; 389 | }; 390 | /* End XCConfigurationList section */ 391 | }; 392 | rootObject = 985CD4CF1C617024005EB9E2 /* Project object */; 393 | } 394 | -------------------------------------------------------------------------------- /Framework/CoreDataDandy.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Framework/CoreDataDandy.xcodeproj/xcshareddata/xcschemes/CoreDataDandy.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 45 | 46 | 52 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 70 | 71 | 72 | 73 | 75 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /Framework/CoreDataDandy/CoreDataDandy.h: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataDandy.h 3 | // CoreDataDandy 4 | // 5 | // Created by Noah Blake on 2/2/16. 6 | // Copyright © 2016 Fuzz Productions. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | #import 29 | 30 | //! Project version number for CoreDataDandy. 31 | FOUNDATION_EXPORT double CoreDataDandyVersionNumber; 32 | 33 | //! Project version string for CoreDataDandy. 34 | FOUNDATION_EXPORT const unsigned char CoreDataDandyVersionString[]; 35 | 36 | 37 | -------------------------------------------------------------------------------- /Framework/CoreDataDandy/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.6.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This code is distributed under the terms and conditions of the MIT license. 2 | 3 | Copyright (c) 2015-2016 Fuzz Productions, LLC. 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![header](header.png) 2 | 3 | [![Build Status](https://travis-ci.org/fuzz-productions/CoreDataDandy.svg?branch=master)](https://travis-ci.org/fuzz-productions/CoreDataDandy) 4 | [![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/fuzz-productions/CoreDataDandy) 5 | [![CocoaPods Compatible](https://img.shields.io/badge/pod-0.6.1-blue.svg)](https://cocoapods.org/pods/CoreDataDandy) 6 | [![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](https://github.com/fuzz-productions/CoreDataDandy/blob/master/LICENSE) 7 | 8 | ## Introduction 9 | Core Data Dandy is a feature-light wrapper around Core Data that simplifies common database operations. 10 | 11 | ## Feature summary 12 | 13 | * Initializes and maintains a Core Data stack. 14 | * Provides convenience methods for saves, inserts, fetches, and deletes. 15 | * Maps json into NSManagedObjects via a lightweight API. 16 | * Deserializes NSManagedObjects into json 17 | 18 | ## Installation 19 | 20 | ### Carthage 21 | 22 | ``` 23 | github "fuzz-productions/CoreDataDandy" ~> 0.6.1 24 | ``` 25 | 26 | ### CocoaPods 27 | 28 | ``` 29 | pod 'CoreDataDandy', '0.6.1' 30 | ``` 31 | 32 | ## Usage 33 | 34 | All standard usage of Core Data Dandy should flow through CoreDataDandy's sharedDandy. More advanced users, however, may find its various components useful in isolation. 35 | 36 | ### Bootstrapping 37 | ```swift 38 | CoreDataDandy.wake("ModelName") 39 | ``` 40 | 41 | ### Saving and deleting 42 | 43 | Save with or without a closure. 44 | 45 | ```swift 46 | Dandy.save() 47 | Dandy.save { error in 48 | // Respond to save completion. 49 | } 50 | ``` 51 | 52 | Delete with or without a closure. 53 | 54 | ```swift 55 | Dandy.delete(object) 56 | Dandy.delete(object) { 57 | // Respond to deletion completion. 58 | } 59 | ``` 60 | 61 | Destroy the contents of the database. Called, for example, to recover from a failure to perform a migration. 62 | 63 | ```swift 64 | Dandy.tearDown() 65 | ``` 66 | 67 | ### Fetching 68 | 69 | Fetch all objects of a given type. 70 | 71 | ```swift 72 | Dandy.fetch(Gossip.self) 73 | ``` 74 | 75 | Fetch an object corresponding to an entity and primaryKey value. 76 | 77 | ```swift 78 | Dandy.fetchUnique(Hat.self, identifiedBy: "bowler") 79 | ``` 80 | 81 | Fetch an array of objects filtered by a predicate. 82 | 83 | ```swift 84 | Dandy.fetch(Gossip.self, filterBy: NSPredicate(format: "topic == %@", "John Keats")) 85 | ``` 86 | 87 | ### Insertions and updates 88 | 89 | Insert object of a given type. 90 | 91 | ```swift 92 | Dandy.insert(Gossip.self) 93 | ``` 94 | 95 | Insert or fetch a unique a object from a primary key. 96 | 97 | ```swift 98 | Dandy.insertUnique(Slander.self, identifiedBy: "WILDE") 99 | ``` 100 | 101 | Upsert a unique object, or insert and update a non-unique object. 102 | 103 | ```swift 104 | Dandy.upsert(Gossip.self, from: json) 105 | ``` 106 | 107 | Upsert an array of unique objects, or insert and update non-unique objects. 108 | 109 | ```swift 110 | Dandy.batchUpsert(Gossip.self, from: json) 111 | ``` 112 | 113 | ### Mapping finalization 114 | 115 | Objects requiring custom mapping finalization should adopt the `MappingFinalizer` protocol. The protocol has a single function, `finalizeMapping(_:)`. 116 | 117 | ```swift 118 | extension Conclusion: MappingFinalizer { 119 | func finalizeMapping(of json: [String : AnyObject]) { 120 | if var content = content { 121 | content += "_FINALIZED" 122 | self.content = content 123 | } 124 | } 125 | } 126 | ``` 127 | 128 | ### Serialization 129 | 130 | Serialize a single object. 131 | 132 | ```swift 133 | Serializer.serialize(gossip) 134 | ``` 135 | 136 | Serialize an array of objects. 137 | 138 | ```swift 139 | Serializer.serialize([byron, wilde, andre3000]) 140 | ``` 141 | 142 | Serialize an object and its relationships. 143 | 144 | ```swift 145 | Serializer.serialize(gossip, including: ["purveyor"]) 146 | ``` 147 | 148 | Serialize an object and its nested relationships. 149 | 150 | ```swift 151 | Serializer.serialize(gossip, including: ["purveyor.hats.material, purveyor.predecessor"]) 152 | ``` 153 | 154 | ## xcdatamodel decorations 155 | 156 | CoreDataDandy supports four xcdatamodel attributes. All decorations are declared and documented in DandyConstants. 157 | 158 | **@primaryKey** 159 | 160 | Add this decoration to the entity's userInfo to specify which property on the entity functions as its primaryKey. 161 | 162 | **@mapping** 163 | 164 | Add this decoration to a property to specify an alternate mapping for this property. For instance, if a property is named "abbreviatedState," but the json value for this property is found at the key "state," add @mapping : state to the abbreviatedState's userInfo. 165 | 166 | **@false** 167 | 168 | Use this decoration in conjunction with the @mapping keyword to disable mapping to the property. For instance, if your entity has an attribute named "secret" that you'd prefer to map yourself, add @mapping : @false to secret's userInfo. 169 | 170 | **@singleton** 171 | 172 | Add this decoration to an entity's userInfo if there should never be more than one instance of this entity in your database. This decoration may be useful for objects like Tokens and CurrentUsers, though it's primarily included to suggest the kind of decorations that may be added in the future. 173 | 174 | ## Warnings 175 | 176 | To receive console warnings in Swift projects, add the entry -D DEBUG in your project's build settings under Swift Compiler - Custom Flags. 177 | -------------------------------------------------------------------------------- /Sources/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants.swift 3 | // CoreDataDandy 4 | // 5 | // Created by Noah Blake on 6/20/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // MARK: - Error constants - 31 | let DandyErrorDomain = "CoreDataDandyDomain" 32 | 33 | // MARK: - xcdatamodel decorations - 34 | // Inserted into the userInfo of an entity to mark its primaryKey 35 | let PRIMARY_KEY = "@primaryKey" 36 | // A special primaryKey for identifying a unique entity in the database. Multiple instances of this entity will not be produced. 37 | let SINGLETON = "@singleton" 38 | // Marks the mapping value in a userInfo used to read json into a property. For instance, if the json value of interest is expected 39 | // to be keyed as "id" for a property named "dandyID," specify "@mapping":"id" in dandyID's userInfo. 40 | let MAPPING = "@mapping" 41 | // An @mapping keyword used to turn off Dandy mapping for a given property. 42 | let NO_MAPPING = "@false" 43 | 44 | let CACHED_MAPPING_LOCATION = "EntityMapper_EntityMapper" 45 | 46 | public typealias JSONObject = [String: Any] 47 | -------------------------------------------------------------------------------- /Sources/Core/CoreDataDandy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataDandy.swift 3 | // CoreDataDandy 4 | // 5 | // Created by Noah Blake on 6/20/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | /// `CoreDataDandy` provides an interface to the majority of the module's features, which include Core Data 31 | /// bootstrapping, main and background-threaded context management, convenient `NSFetchRequests`, 32 | /// database inserts, database deletes, and `NSManagedObject` deserialization. 33 | open class CoreDataDandy { 34 | // MARK: - Properties - 35 | /// A singleton encapsulating much of CoreDataDandy's base functionality. 36 | fileprivate static let defaultDandy = CoreDataDandy() 37 | 38 | /// The default implementation of Dandy. Subclasses looking to extend or alter Dandy's functionality 39 | /// should override this getter and provide a new instance. 40 | open class var sharedDandy: CoreDataDandy { 41 | return defaultDandy 42 | } 43 | 44 | /// A manager of the NSManagedObjectContext, NSPersistentStore, and NSPersistentStoreCoordinator. 45 | /// Accessing this property directly is generaly discouraged - it is intended for use within the module alone. 46 | open var coordinator: PersistentStackCoordinator! 47 | 48 | // MARK: - Initialization- 49 | /// Bootstraps the application's core data stack. 50 | /// 51 | /// - parameter managedObjectModelName: The name of the .xcdatamodel file 52 | /// - parameter completion: A completion block executed on initialization completion 53 | @discardableResult open class func wake(_ managedObjectModelName: String, 54 | completion: (() -> Void)? = nil) -> CoreDataDandy { 55 | EntityMapper.clearCache() 56 | sharedDandy.coordinator = PersistentStackCoordinator(managedObjectModelName: managedObjectModelName, 57 | persistentStoreConnectionCompletion: completion) 58 | return sharedDandy 59 | } 60 | 61 | // MARK: - Deinitialization - 62 | /// Removes all cached data from the application without endangering future database 63 | /// interactions. 64 | open func tearDown() { 65 | coordinator.resetManageObjectContext() 66 | 67 | do { 68 | try FileManager.default.removeItem(at: PersistentStackCoordinator.persistentStoreURL as URL) 69 | } catch { 70 | log(format("Failed to delete persistent store")) 71 | } 72 | 73 | coordinator.resetPersistentStore() 74 | EntityMapper.clearCache() 75 | save() 76 | } 77 | 78 | // MARK: - Inserts - 79 | /// Inserts a new Model from the specified entity type. In general, this function should not be invoked 80 | /// directly, as its incautious use is likely to lead to database leaks. 81 | /// 82 | /// - parameter type: The type of Model to insert 83 | /// 84 | /// - returns: A managed object if one could be inserted for the specified Entity. 85 | @discardableResult open func insert(_ type: Model.Type) -> Model? { 86 | if let entityDescription = type.entityDescription() { 87 | // Ignore this insert if the entity is a singleton and a pre-existing insert exists. 88 | if entityDescription.primaryKey == SINGLETON { 89 | if let singleton = singleton(type) { 90 | return singleton 91 | } 92 | } 93 | // Otherwise, insert a new managed object 94 | return type.inserted() as? Model 95 | } else { 96 | log(format("NSEntityDescriptionNotFound for entity named " + String(describing: type) + ". No object will be returned")) 97 | return nil 98 | } 99 | } 100 | 101 | /// MARK: - Upserts - 102 | /// This function performs upserts differently depending on whether the Model is marked as unique or not. 103 | /// 104 | /// If the Model is marked as unique (either through an @primaryKey decoration or an xcdatamode constraint), the 105 | /// primaryKeyValue is extracted and an upsert is performed through 106 | /// `upsertUnique(_:, identifiedBy:) -> NSManagedObject?`. 107 | /// 108 | /// Otherwise, an insert is performed and a Model is written to from the json provided. 109 | /// 110 | /// - parameter type: The type of Model to insert 111 | /// - parameter json: A json object to map into the returned object's attributes and relationships 112 | /// 113 | /// - returns: A managed object if one could be created. 114 | @discardableResult open func upsert(_ type: Model.Type, 115 | from json: JSONObject) -> Model? { 116 | guard let entity = NSEntityDescription.forType(type) else { 117 | log(format("Could not retrieve NSEntityDescription for type \(type)")) 118 | return nil 119 | } 120 | 121 | if entity.isUnique { 122 | if let primaryKeyValue = entity.primaryKeyValueFromJSON(json) { 123 | return upsertUnique(type, identifiedBy: primaryKeyValue, from: json) 124 | } else { 125 | log(format("Could not retrieve primary key from json \(json).")) 126 | return nil 127 | } 128 | } 129 | 130 | if let managedObject = insert(type) { 131 | return ObjectFactory.build(managedObject, from: json) 132 | } 133 | 134 | return nil 135 | } 136 | 137 | /// Attempts to build an array Models from a json array. Through recursion, behaves identically to 138 | /// upsert(_:, _:) -> Model?. 139 | /// 140 | /// - parameter type: The type of Model to insert 141 | /// - parameter json: An array to map into the returned objects' attributes and relationships 142 | /// 143 | /// - returns: An array of managed objects if one could be created. 144 | @discardableResult open func batchUpsert(_ type: Model.Type, 145 | from json: [JSONObject]) -> [Model]? { 146 | var models = [Model]() 147 | for object in json { 148 | if let model = upsert(type, from: object) { 149 | models.append(model) 150 | } 151 | } 152 | return models.isEmpty ? nil : models 153 | } 154 | 155 | // MARK: - Unique objects - 156 | /// Attempts to fetch a Model of the specified type matching the primary key provided. 157 | /// - If no property on the type's `NSEntityDescription` is marked with the @primaryKey identifier or constraint, 158 | /// a warning is issued and no managed object is returned. 159 | /// - If an object matching the primaryKey is found, it is returned. Otherwise a new object is inserted and returned. 160 | /// - If more than one object is fetched for this primaryKey, a warning is issued and one is returned. 161 | /// 162 | /// - parameter type: The type of Model to insert. 163 | /// - parameter primaryKeyValue: The value of the unique object's primary key 164 | @discardableResult open func insertUnique(_ type: Model.Type, 165 | identifiedBy primaryKeyValue: Any) -> Model? { 166 | // Return an object if one exists. Otherwise, attempt to insert one. 167 | if let object = fetchUnique(type, identifiedBy: primaryKeyValue) { 168 | return object 169 | } else if let entityDescription = type.entityDescription(), 170 | let primaryKey = entityDescription.primaryKey { 171 | let object = insert(type) 172 | let convertedPrimaryKeyValue = CoreDataValueConverter.convert(primaryKeyValue, for: entityDescription, property: primaryKey) 173 | object?.setValue(convertedPrimaryKeyValue, forKey: primaryKey) 174 | 175 | return object 176 | } 177 | return nil 178 | } 179 | 180 | /// Invokes `upsertUnique(_:, identifiedBy:) -> Model?`, then attempts to write values from 181 | /// the provided JSON into the returned object. 182 | /// 183 | /// - parameter type: The type of the requested entity 184 | /// - parameter primaryKeyValue: The value of the unique object's primary key 185 | /// - parameter json: A json object to map into the returned object's attributes and relationships 186 | /// 187 | /// - returns: A managed object if one could be created. 188 | private func upsertUnique(_ type: Model.Type, 189 | identifiedBy primaryKeyValue: Any, 190 | from json: JSONObject) -> Model? { 191 | if let object = insertUnique(type, identifiedBy: primaryKeyValue) { 192 | ObjectFactory.build(object, from: json) 193 | return object 194 | } else { 195 | log(format("Could not upsert managed object of type \(type), identified by \(primaryKeyValue), json \(json).")) 196 | return nil 197 | } 198 | } 199 | 200 | // MARK: - Fetches - 201 | /// A wrapper around NSFetchRequest. 202 | /// 203 | /// - parameter type: The type of the fetched entity 204 | /// - parameter predicate: The predicate used to filter results 205 | /// 206 | /// - throws: If the ensuing NSManagedObjectContext's executeFetchRequest() throws, the exception will be passed. 207 | /// 208 | /// - returns: If the fetch was successful, the fetched Model. 209 | open func fetch(_ type: Model.Type, 210 | filterBy predicate: NSPredicate? = nil) throws -> [Model]? { 211 | let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: String(describing: type)) 212 | fetchRequest.predicate = predicate 213 | return try coordinator.mainContext.fetch(fetchRequest) 214 | } 215 | 216 | /// Attempts to fetch a unique Model with a primary key value matching the passed in parameter. 217 | /// 218 | /// - parameter type: The type of the fetched entity 219 | /// - parameter primaryKeyValue: The value of unique object's primary key 220 | /// 221 | /// - returns: If the fetch was successful, the fetched Model. 222 | open func fetchUnique(_ type: Model.Type, 223 | identifiedBy primaryKeyValue: Any, 224 | emitResultCountWarnings: Bool = false) -> Model? { 225 | if let entityDescription = type.entityDescription() { 226 | if entityDescription.primaryKey == SINGLETON { 227 | if let singleton = singleton(type) { 228 | return singleton 229 | } 230 | } else if let predicate = entityDescription.primaryKeyPredicate(for: primaryKeyValue) { 231 | var results: [NSManagedObject]? = nil 232 | var resultCount = 0 233 | do { 234 | results = try fetch(type, filterBy: predicate) 235 | resultCount = results?.count ?? 0 236 | } catch { 237 | log(format("Your fetch for a unique entity named \(String(describing: type)) with identified by \(primaryKeyValue) raised an exception. This is a serious error that should be resolved immediately.")) 238 | } 239 | if resultCount == 0 && emitResultCountWarnings { 240 | log(format("Your fetch for a unique entity named \(String(describing: type)) with identified by \(primaryKeyValue) returned no results.")) 241 | } else if resultCount > 1 && emitResultCountWarnings { 242 | log(format("Your fetch for a unique entity named \(String(describing: type)) with identified by \(primaryKeyValue) returned multiple results. This is a serious error that should be resolved immediately.")) 243 | } 244 | return results?.first as? Model 245 | } else { 246 | log(format("Failed to produce predicate for \(String(describing: type)) with identified by \(primaryKeyValue).")) 247 | } 248 | } 249 | 250 | log(format("A unique NSManaged for entity named \(String(describing: type)) could not be retrieved for primaryKey \(primaryKeyValue). No object will be returned")) 251 | 252 | return nil 253 | } 254 | 255 | // MARK: - Saves and Deletes - 256 | /// Save the current state of the `NSManagedObjectContext` to disk and optionally receive notice of the save 257 | /// operation's completion. 258 | /// 259 | /// - parameter completion: An optional closure that is invoked when the save operation complete. If the save operation 260 | /// resulted in an error, the error is returned. 261 | open func save(_ completion:((_ error: Error?) -> Void)? = nil) { 262 | /** 263 | Note: http://www.openradar.me/21745663. Currently, there is no way to throw out of performBlock. If one arises, 264 | this code should be refactored to throw. 265 | */ 266 | if !coordinator.mainContext.hasChanges && !coordinator.privateContext.hasChanges { 267 | completion?(nil) 268 | return 269 | } 270 | coordinator.mainContext.performAndWait({[unowned self] in 271 | do { 272 | try self.coordinator.mainContext.save() 273 | } catch { 274 | log(format( "Failed to save main context.")) 275 | completion?(error) 276 | return 277 | } 278 | 279 | self.coordinator.privateContext.perform({ [unowned self] in 280 | do { 281 | try self.coordinator.privateContext.save() 282 | completion?(nil) 283 | } catch { 284 | log(format( "Failed to save private context.")) 285 | completion?(error) 286 | } 287 | }) 288 | }) 289 | } 290 | 291 | /// Delete a managed object. 292 | /// 293 | /// - parameter object: The object to be deleted. 294 | /// - parameter completion: An optional closure that is invoked when the deletion is complete. 295 | open func delete(_ object: NSManagedObject, completion: (() -> Void)? = nil) { 296 | if let context = object.managedObjectContext { 297 | context.perform({ 298 | context.delete(object) 299 | completion?() 300 | }) 301 | } 302 | } 303 | 304 | // MARK: - Singletons - 305 | /// Attempts to singleton of a given type. 306 | /// 307 | /// - parameter type: The type of the singleton 308 | /// 309 | /// - returns: The singleton for this type if one could be found. 310 | fileprivate func singleton(_ type: Model.Type) -> Model? { 311 | do { 312 | if let results = try fetch(type) { 313 | if results.count == 1 { 314 | return results.first 315 | } else if results.count == 0 { 316 | return type.inserted() as? Model 317 | } else { 318 | log(format("Failed to fetch unique instance of entity named " + String(describing: type) + ".")) 319 | return nil 320 | 321 | } 322 | } 323 | } catch { 324 | log(format("Your singleton fetch for entity named \(String(describing: type)) raised an exception. This is a serious error that should be resolved immediately.")) 325 | } 326 | 327 | log(format("Failed to fetch unique instance of entity named " + String(describing: type) + ".")) 328 | return nil 329 | } 330 | } 331 | 332 | // MARK: - Convenience accessors - 333 | /// A lazy global for more succinct access to CoreDataDandy's sharedDandy. 334 | public let Dandy: CoreDataDandy = CoreDataDandy.sharedDandy 335 | -------------------------------------------------------------------------------- /Sources/Core/EntityMapper.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EntityMapper.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 6/21/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | struct EntityMapper { 31 | // MARK: - Entity Mapping - 32 | /// The primary function of this class. Creates, caches, and returns mappings that are subsequently 33 | /// used to read json into an instance of an entity. 34 | /// 35 | /// - parameter entity: The `NSEntityDescription` to map 36 | /// 37 | /// - returns: A mapping used to read json into the specified entity. 38 | @discardableResult static func map(_ entity: NSEntityDescription) -> [String: PropertyDescription]? { 39 | // Search for a cached entity map 40 | if let entityName = entity.name { 41 | // A mapping has already been created for this entity. Return it. 42 | if let map = cachedEntityMap[entityName] { 43 | return map 44 | } else { 45 | // A mapping has not been created for this entity. Create it, cache it, and return it. 46 | var map = [String: PropertyDescription]() 47 | 48 | // Map attributes 49 | if let attributes = entity.allAttributes { 50 | add(attributes, to: &map) 51 | } 52 | 53 | // Map relationships 54 | if let relationships = entity.allRelationships { 55 | add(relationships, to: &map) 56 | } 57 | 58 | archive(map, forEntity:entityName) 59 | return map 60 | } 61 | } else { 62 | log(format("Entity Name is nil for Entity " + entity.description + ". No mapping will be returned")) 63 | } 64 | return nil 65 | } 66 | 67 | /// A convenience function for producing mapped values of an entity's attributes relationships. 68 | /// 69 | /// - parameter dictionary: A dictionary containing either NSAttributeDescriptions or NSRelationshipDescriptions 70 | /// - parameter map: The map for reading json into an entity 71 | fileprivate static func add(_ dictionary: JSONObject, to map: inout [String: PropertyDescription]) { 72 | for (name, description) in dictionary { 73 | if let propertyDescription = description as? NSPropertyDescription, 74 | let newMapping = mappingForUserInfo(propertyDescription.userInfo) { 75 | // Do not add values specified as non-mapping to the mapping dictionary 76 | if newMapping != NO_MAPPING { 77 | map[newMapping] = PropertyDescription(description: description) 78 | } 79 | } else { 80 | map[name] = PropertyDescription(description: description) 81 | } 82 | } 83 | } 84 | 85 | /// Returns any mapping values found in a userInfo dictionary. 86 | /// 87 | /// - parameter userInfo: The userInfo of an `NSEntityDescription`, `NSAttributeDescription`, or `NSRelationshipDescription` 88 | /// 89 | /// - returns: A mapping value if one was found. Otherwise, nil. 90 | static func mappingForUserInfo(_ userInfo: [AnyHashable: Any]?) -> String? { 91 | if let userInfo = userInfo as? [String: String], 92 | let mapping = userInfo[MAPPING] { 93 | return mapping 94 | } 95 | 96 | return nil 97 | } 98 | } 99 | 100 | // MARK: - EntityMapper+Caching - 101 | extension EntityMapper { 102 | /// A lazy, nillable reference to a cached map. 103 | fileprivate static var _cachedEntityMap: [String: [String: PropertyDescription]]? 104 | /// A dictionary containing mappings in the following structure: ['entityName': 'map']. 105 | static var cachedEntityMap: [String: [String: PropertyDescription]] { 106 | get { 107 | if _cachedEntityMap == nil { 108 | if let archivedMap = NSKeyedUnarchiver.unarchiveObject(withFile: self.entityMapFilePath) as? [String: [String: PropertyDescription]] { 109 | _cachedEntityMap = archivedMap 110 | } else { 111 | _cachedEntityMap = [String: [String: PropertyDescription]]() 112 | } 113 | } 114 | return _cachedEntityMap! 115 | } 116 | 117 | set { 118 | _cachedEntityMap = newValue 119 | } 120 | } 121 | 122 | /// - returns: The file path where the entityMap is archived. 123 | fileprivate static var entityMapFilePath: String = { 124 | let pathArray = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 125 | let documentPath = pathArray.first! 126 | return NSString(string: documentPath).appendingPathComponent(CACHED_MAPPING_LOCATION) 127 | }() 128 | 129 | /// Archives an entity's mapping. Note, this mapping, will be saved to the `cachedEntityMap` at the key 130 | /// of the forEntity parameter. 131 | /// 132 | /// - parameter map: A mapping for reading json into an entity. 133 | /// - parameter forEntity: The name of the entity `map` corresponds to. 134 | fileprivate static func archive(_ map: [String: PropertyDescription], forEntity entity: String) { 135 | cachedEntityMap[entity] = map 136 | NSKeyedArchiver.archiveRootObject(cachedEntityMap, toFile: entityMapFilePath) 137 | } 138 | 139 | /// Clears cached mappings. 140 | /// 141 | /// This method should be invoked when the database is undergoing a migration or any other time 142 | /// where the cached entity mappings may be invalidated.. 143 | static func clearCache() { 144 | _cachedEntityMap = nil 145 | if FileManager.default.fileExists(atPath: entityMapFilePath) { 146 | do { 147 | try FileManager.default.removeItem(atPath: entityMapFilePath) 148 | } catch { 149 | log(format("Failure to remove entity map from cache")) 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Sources/Core/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 1/24/16. 6 | // Copyright © 2016 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | 29 | import Foundation 30 | 31 | // MARK: - Warnings - 32 | func log(_ message: @autoclosure () -> String, filename: String = #file, function: String = #function, line: Int = #line) { 33 | #if DEBUG 34 | NSLog("[\(NSString(string: filename).lastPathComponent):\(line)] \(function) - %@", message()) 35 | #endif 36 | } 37 | 38 | func format(_ message: String, with error: NSError? = nil) -> String { 39 | #if DEBUG 40 | var errorDescription = "" 41 | if let error = error { 42 | errorDescription = " Error:\n" + error.description 43 | } 44 | let warning = "(CoreDataDandy) warning: " + message + errorDescription 45 | return warning 46 | #else 47 | return "" 48 | #endif 49 | } 50 | -------------------------------------------------------------------------------- /Sources/Core/MappingFinalizer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MappingFinalizer.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 2/5/16. 6 | // Copyright © 2016 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | public protocol MappingFinalizer { 29 | func finalizeMapping(of json: JSONObject) 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Core/ObjectFactory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectFactory.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 7/4/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | public struct ObjectFactory { 31 | 32 | /// Returns an object of a given type constructed from the provided json. This function is primarily accessed 33 | /// within Dandy to recursively produce objects when parsing nested json, and is thereby only accessed indirectly. 34 | /// Others, however, may find direct access to this convenience useful. 35 | /// 36 | /// By default, this function will recursively parse through a json hierarchy. 37 | /// 38 | /// Note that this method enforces the specification of a primaryKey for the given entity. 39 | /// 40 | /// Finally, as invocations of this function implicitly involve database fetches, it may bottleneck when used to 41 | /// process json with thousands of objects. 42 | /// 43 | /// - parameter type: The type of object to make. 44 | /// - parameter from: The json to map into the returned object. 45 | /// 46 | /// - returns: A Model of the specified type if one could be inserted or fetched. The values that could be mapped 47 | /// from the json to the object will be found on the returned object. 48 | @discardableResult public static func make(_ type: Model.Type, 49 | from json: JSONObject) -> Model? { 50 | guard let entity = type.entityDescription() else { 51 | log(format("An entityDescription was not found for type \(type) from json \n\(json).")) 52 | return nil 53 | } 54 | 55 | var object: NSManagedObject? = nil 56 | 57 | if entity.isUnique { 58 | // Attempt to fetch or create unique object for primaryKey 59 | if let primaryKeyValue = entity.primaryKeyValueFromJSON(json) { 60 | object = Dandy.insertUnique(type, identifiedBy: primaryKeyValue) 61 | } 62 | } else { 63 | // The object is not unique. Simply insert it. 64 | object = Dandy.insert(type) 65 | } 66 | 67 | if let object = object { 68 | build(object, from: json) 69 | finalizeMapping(of: object, from: json) 70 | 71 | return object as? Model 72 | } 73 | 74 | log(format("An object could not be made for entity \(entity.name) from json \n\(json).")) 75 | return nil 76 | } 77 | 78 | /// Transcribes attributes and relationships from json to a given object. Use this function to perform bulk upates 79 | /// on an object from json. 80 | /// 81 | /// - parameter object: The `NSManagedObject` to configure. 82 | /// - parameter json: The json to map into the returned object. 83 | /// 84 | /// - returns: The object passed in with newly mapped values where mapping was possible. 85 | @discardableResult public static func build(_ object: Model, 86 | from json: JSONObject) -> Model { 87 | if let map = EntityMapper.map(object.entity) { 88 | // Begin mapping values from json to object properties 89 | for (key, description) in map { 90 | if let value: Any = _value(at: key, of: json) { 91 | if description.type == .attribute, 92 | let type = description.attributeType { 93 | // A valid mapping was found for an attribute of a known type 94 | object.setValue(CoreDataValueConverter.convert(value, to: type), forKey: description.name) 95 | } else if description.type == .relationship { 96 | // A valid mapping was found for a relationship of a known type 97 | make(description, to: object, from: value) 98 | } 99 | } 100 | } 101 | } 102 | return object 103 | } 104 | 105 | /// Builds a relationship to a passed in object from json. 106 | /// Note that the json type must match the relationship type. For instance, passing a json array to build a toOne 107 | /// relationship is invalid, just as passing a single json object to build a toMany relationship is invalid. 108 | /// 109 | /// - parameter relationship: An object specifying the details of the relationship, including its name, whether it is 110 | /// toMany, and whether it is ordered. 111 | /// - parameter object: The parent object or "owner" of the relationship. If relationship objects are built, they will 112 | /// be assigned to this object relationship. 113 | /// - parameter json: The json with which to build the related objects. 114 | /// 115 | /// - returns: The object passed in with a newly mapped relationship if relationship objects were built. 116 | @discardableResult static func make(_ relationship: PropertyDescription, 117 | to object: NSManagedObject, 118 | from json: Any) -> NSManagedObject { 119 | guard let relatedEntity = relationship.destinationEntity else { 120 | log(format("The entity named \(relationship.name) for entity \(object.entity.name) lacks an NSEntityDescription. No relationthip will be built.")) 121 | return object 122 | } 123 | 124 | guard let relatedTypeName = relatedEntity.managedObjectClassName, 125 | let relatedEntityType = NSManagedObject.type(named: relatedTypeName) else { 126 | log(format("No type could be found for \(relatedEntity.managedObjectClassName) of type named \(relatedEntity.name). Provide a subclassed type to enable serialization.")) 127 | return object 128 | } 129 | 130 | if let json = json as? JSONObject, 131 | !relationship.toMany { 132 | // A dictionary was passed for a toOne relationship 133 | if let relation = make(relatedEntityType, from: json) { 134 | object.setValue(relation, forKey: relationship.name) 135 | } else { 136 | // No relationship could be made from the json. Nil out the relationship. 137 | log(format("A relationship named \(relationship.name) could not be made for \(object) from json \n\(json).\n\(relationship.name) will be nilled out if it is an optional relationship.")) 138 | 139 | object.nilIfOptional(relationship) 140 | } 141 | 142 | return object 143 | } else if let json = json as? [JSONObject], 144 | relationship.toMany { 145 | // An array was passed for a toMany relationship 146 | var relations = [NSManagedObject]() 147 | for child in json { 148 | if let relation = make(relatedEntityType, from: child) { 149 | relations.append(relation) 150 | } else { 151 | log(format("A relationship named \(relationship.name) could not be established for object \(object) from json \n\(child).")) 152 | } 153 | } 154 | 155 | object.setValue(relationship.ordered ? NSOrderedSet(array: relations) 156 | : NSSet(array: relations), 157 | forKey: relationship.name) 158 | 159 | return object 160 | } else if let possibleNull = json as? NilConvertible, 161 | possibleNull.convertToNil() == nil { 162 | // A nil-convertible value was passed 163 | object.nilIfOptional(relationship) 164 | } else { 165 | // The value provided did not match the expected type. For instance, an array was passed where an object 166 | // was expected. 167 | log(format("A relationship named \(relationship.name) could not be established for object \n\(object) from json \n\(json)." 168 | + { 169 | if relationship.toMany { 170 | return " An array is expected to create toMany relationships." 171 | } else { 172 | return " An object is expected to create toOne relationships." 173 | } 174 | }() 175 | + "\(relationship.name)")) 176 | } 177 | 178 | return object 179 | } 180 | 181 | /// Allows for adopters of `MappingFinalizer` to perform custom mapping after the ObjectFactory has completed its 182 | /// work. 183 | /// 184 | /// - parameter object: The newly created object and the potential adopter of `MappingFinalizer`. 185 | /// - parameter from: The json that was used to create the object. Note that this json will include all nested 186 | /// "child" relationships, but no "parent" relationships. 187 | fileprivate static func finalizeMapping(of object: NSManagedObject, from json: JSONObject) { 188 | if let object = object as? MappingFinalizer { 189 | object.finalizeMapping(of: json) 190 | } 191 | } 192 | } 193 | 194 | // MARK: - NSManagedObject+Nil - 195 | private extension NSManagedObject { 196 | /// If a property is optional, set it to nil. 197 | /// 198 | /// - parameter property: The relationship to nil if optional. 199 | func nilIfOptional(_ property: PropertyDescription) { 200 | if property.optional { 201 | setValue(nil, forKey: property.name) 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Sources/Core/PersistentStackCoordinator.swift: -------------------------------------------------------------------------------- 1 | 2 | // 3 | // PersistentStackCoordinator.swift 4 | // CoreDataDandy 5 | // 6 | // Created by Noah Blake on 6/20/15. 7 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 8 | // 9 | // This code is distributed under the terms and conditions of the MIT license. 10 | // 11 | // Permission is hereby granted, free of charge, to any person obtaining a copy 12 | // of this software and associated documentation files (the "Software"), to 13 | // deal in the Software without restriction, including without limitation the 14 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 15 | // sell copies of the Software, and to permit persons to whom the Software is 16 | // furnished to do so, subject to the following conditions: 17 | // 18 | // The above copyright notice and this permission notice shall be included in 19 | // all copies or substantial portions of the Software. 20 | // 21 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 27 | // IN THE SOFTWARE. 28 | 29 | import CoreData 30 | 31 | /// The class responsible for maintaining the Core Data stack, including the `NSManagedObjectContexts`, 32 | /// the `NSPersistentStore`, and the `NSPersistentStoreCoordinators`. 33 | open class PersistentStackCoordinator { 34 | fileprivate var managedObjectModelName: String 35 | fileprivate var persistentStoreConnectionCompletion: (() -> Void)? 36 | 37 | public init(managedObjectModelName: String, 38 | persistentStoreConnectionCompletion: (() -> Void)? = nil) { 39 | self.managedObjectModelName = managedObjectModelName 40 | self.persistentStoreConnectionCompletion = persistentStoreConnectionCompletion 41 | } 42 | 43 | // MARK: - Lazy stack initialization - 44 | /// The .xcdatamodel to read from. 45 | lazy var managedObjectModel: NSManagedObjectModel = { 46 | for bundle in Bundle.allBundles { 47 | if let url = bundle.url(forResource: self.managedObjectModelName, withExtension: "momd"), 48 | let mom = NSManagedObjectModel(contentsOf: url) { 49 | return mom 50 | } 51 | } 52 | preconditionFailure("Failed to find a managed object model named \(self.managedObjectModelName) in any bundle.") 53 | }() 54 | 55 | /// The persistent store coordinator, which manages disk operations. 56 | open lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { 57 | var coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) 58 | coordinator.resetPersistentStore() 59 | return coordinator 60 | }() 61 | 62 | /// The primary managed object context. Note the inclusion of the parent context, which takes disk operations off 63 | /// the main thread. 64 | open lazy var mainContext: NSManagedObjectContext = { [unowned self] in 65 | var mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) 66 | mainContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) 67 | self.connectPrivateContextToPersistentStoreCoordinator() 68 | mainContext.parent = self.privateContext 69 | return mainContext 70 | }() 71 | 72 | /// Connects the private context with its PSC on the correct thread, waits for the connection to take place, 73 | /// then announces its completion via the initializationCompletion closure. 74 | func connectPrivateContextToPersistentStoreCoordinator() { 75 | self.privateContext.performAndWait({ [unowned self] in 76 | self.privateContext.persistentStoreCoordinator = self.persistentStoreCoordinator 77 | if let completion = self.persistentStoreConnectionCompletion { 78 | DispatchQueue.main.async(execute: { 79 | completion() 80 | }) 81 | } 82 | }) 83 | } 84 | 85 | /// A context that escorts disk operations off the main thread. 86 | lazy var privateContext: NSManagedObjectContext = { [unowned self] in 87 | var privateContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) 88 | privateContext.mergePolicy = NSMergePolicy(merge: NSMergePolicyType.mergeByPropertyObjectTrumpMergePolicyType) 89 | return privateContext 90 | }() 91 | 92 | // MARK: - Convenience accessors - 93 | /// - returns: The path to the sqlite file that stores the application's data. 94 | static var persistentStoreURL: URL = { 95 | return FileManager.documentDirectoryURL.appendingPathComponent("Model.sqlite") 96 | }() 97 | 98 | // MARK: - Stack clearing - 99 | /// Clear the managed object contexts. 100 | func resetManageObjectContext() { 101 | mainContext.performAndWait({[unowned self] in 102 | self.mainContext.reset() 103 | self.privateContext.performAndWait({ 104 | self.privateContext.reset() 105 | }) 106 | }) 107 | } 108 | 109 | /// Attempt to remove existing persistent stores attach a new one. 110 | /// Note: this method should not be invoked in lazy instantiatiations of a persistentStoreCoordinator. Instead, 111 | /// directly call the corresponding function on the coordinator itself. 112 | open func resetPersistentStore() { 113 | persistentStoreCoordinator.resetPersistentStore() 114 | } 115 | } 116 | 117 | // MARK: - NSPersistentStoreCoordinator+Recovery - 118 | extension NSPersistentStoreCoordinator { 119 | /// Attempt to remove existing persistent stores attach a new one. 120 | func resetPersistentStore() { 121 | for store in persistentStores { 122 | do { 123 | try remove(store) 124 | } catch { 125 | log(format("Failure to remove persistent store")) 126 | } 127 | } 128 | do { 129 | let document = FileManager.documentDirectoryURL 130 | if !FileManager.directoryExists(at: document) { 131 | // In the event a Document directory does not exist, create one. 132 | // Otherwise, the persistent store will not be added. 133 | try FileManager.createDirectory(at: document) 134 | } 135 | 136 | let options = [NSMigratePersistentStoresAutomaticallyOption: NSNumber(value: true as Bool), NSInferMappingModelAutomaticallyOption: NSNumber(value: true as Bool)] 137 | try addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, 138 | at: PersistentStackCoordinator.persistentStoreURL, 139 | options: options) 140 | 141 | } catch { 142 | var dict = JSONObject() 143 | dict[NSLocalizedDescriptionKey] = "Failed to initialize the application's saved data" as AnyObject? 144 | dict[NSLocalizedFailureReasonErrorKey] = "There was an error creating or loading the application's saved data." as AnyObject? 145 | dict[NSUnderlyingErrorKey] = error as NSError 146 | let error = NSError(domain: DandyErrorDomain, code: 9999, userInfo: dict) 147 | log(format("Failure to add persistent store", with: error)) 148 | do { 149 | try FileManager.default.removeItem(at: PersistentStackCoordinator.persistentStoreURL) 150 | } catch { 151 | log(format("Failure to remove cached sqlite file")) 152 | } 153 | EntityMapper.clearCache() 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Sources/Core/PropertyDescription.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PropertyDescription.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 7/3/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | /// The type of a `PropertyDescription`. 31 | /// 32 | /// - Unknown: An unknown property description type. Generally indicative of an improperly instantiated `PropertyDescription.` 33 | /// - Attribute: Marks a property description corresponding to an attribute. 34 | /// - Relationship: Marks a property description corresponding to an relationship. 35 | enum PropertyType: Int { 36 | case unknown = 0 37 | case attribute 38 | case relationship 39 | } 40 | 41 | /// `PropertyDescription` provides a convenient means of accessing information necessary to map json into an 42 | /// NSManagedObject. It encapsulates values found in `NSAttributeDescriptions` and `NSRelationshipDescriptions`, such as 43 | /// an attribute's type or if a relationship is ordered or not. 44 | final class PropertyDescription: NSObject { 45 | /// The name of of the property. 46 | var name = String() 47 | 48 | /// The property type: .Unknown, .Attribute, or .Relationship. 49 | var type = PropertyType.unknown 50 | 51 | /// The type of an attribute. 52 | var attributeType: NSAttributeType? 53 | 54 | /// A boolean indicating whether the property is optional or not. 55 | var optional = true 56 | 57 | /// The entity description of a relationship. 58 | var destinationEntity: NSEntityDescription? 59 | 60 | /// A boolean describing if a relationship is ordered or not. By default, false. 61 | var ordered = false 62 | 63 | /// A boolean describing if a relationship is toMany or not. By default, false. 64 | var toMany = false 65 | 66 | override init() { super.init() } 67 | 68 | /// An initializer that builds a `PropertyDescription` from either an `NSAttributeDescription` or an 69 | /// `NSRelationshipDescription`. 70 | /// 71 | /// Note that a default object without meaningful values will be returned if neither of the above is 72 | convenience init(description: Any) { 73 | if let description = description as? NSAttributeDescription { 74 | self.init(attributeDescription: description) 75 | } else if let description = description as? NSRelationshipDescription { 76 | self.init(relationshipDescription: description) 77 | } else { 78 | self.init() 79 | log(format("Unknown property type for description: \(description)")) 80 | } 81 | } 82 | 83 | /// An initializer that builds a `PropertyDescription` by extracting relevant values from an `NSAttributeDescription`. 84 | fileprivate init(attributeDescription: NSAttributeDescription) { 85 | name = attributeDescription.name 86 | type = PropertyType.attribute 87 | attributeType = attributeDescription.attributeType 88 | optional = attributeDescription.isOptional 89 | super.init() 90 | } 91 | 92 | /// An initializer that builds a `PropertyDescription` by extracting relevant values from an `NSRelationshipDescription`. 93 | fileprivate init(relationshipDescription: NSRelationshipDescription) { 94 | name = relationshipDescription.name 95 | type = PropertyType.relationship 96 | destinationEntity = relationshipDescription.destinationEntity 97 | ordered = relationshipDescription.isOrdered 98 | toMany = relationshipDescription.isToMany 99 | optional = relationshipDescription.isOptional 100 | super.init() 101 | } 102 | } 103 | 104 | // MARK: - - 105 | extension PropertyDescription : NSCoding { 106 | convenience init?(coder aDecoder: NSCoder) { 107 | self.init() 108 | if let n = aDecoder.decodeObject(forKey: "name") as? String { 109 | name = n 110 | } 111 | type = PropertyType(rawValue: aDecoder.decodeInteger(forKey: "type"))! 112 | attributeType = NSAttributeType(rawValue: UInt(aDecoder.decodeInteger(forKey: "attributeType")))! 113 | ordered = aDecoder.decodeBool(forKey: "ordered") 114 | toMany = aDecoder.decodeBool(forKey: "toMany") 115 | } 116 | 117 | @objc func encode(with aCoder: NSCoder) { 118 | aCoder.encode(name, forKey:"name") 119 | aCoder.encode(type.rawValue, forKey:"type") 120 | if let attributeType = attributeType { 121 | aCoder.encode(Int(attributeType.rawValue), forKey:"attributeType") 122 | } 123 | if let destinationEntity = destinationEntity { 124 | aCoder.encode(destinationEntity, forKey:"destinationEntity") 125 | } 126 | aCoder.encode(ordered, forKey: "ordered") 127 | aCoder.encode(toMany, forKey: "toMany") 128 | } 129 | 130 | override var hashValue: Int { 131 | get { 132 | return "\(name)_\(type)_\(attributeType)_\(destinationEntity)_\(ordered)_\(toMany)".hashValue 133 | } 134 | } 135 | } 136 | 137 | // MARK: - Equality - 138 | extension PropertyDescription { 139 | /// Compares the hashValue of two `PropertyDescription` objects. `PropertyDescription` objects are never considered 140 | /// equal to other types. 141 | override func isEqual(_ object: Any?) -> Bool { 142 | if let object = object as? PropertyDescription { 143 | return hashValue == object.hashValue 144 | } else { 145 | return false 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Sources/Core/Serializer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Serializer.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 7/15/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | fileprivate func < (lhs: T?, rhs: T?) -> Bool { 30 | switch (lhs, rhs) { 31 | case let (l?, r?): 32 | return l < r 33 | case (nil, _?): 34 | return true 35 | default: 36 | return false 37 | } 38 | } 39 | 40 | fileprivate func > (lhs: T?, rhs: T?) -> Bool { 41 | switch (lhs, rhs) { 42 | case let (l?, r?): 43 | return l > r 44 | default: 45 | return rhs < lhs 46 | } 47 | } 48 | 49 | /** 50 | Serializes NSManagedObjects objects to json. 51 | */ 52 | public struct Serializer { 53 | /// Produces json representing an object and, potentially, members of its relationship tree. 54 | /// 55 | /// Specify each relationship you wish to include in the serialization by including its name in the relationship 56 | /// array. To serialize further into a relationship "tree", use keypaths. 57 | /// 58 | /// As an example, imagine the following relationship tree: 59 | /// 60 | /// dandy (relationships to) 61 | /// -> hats 62 | /// -> gossip (relationship to) 63 | /// -> purveyor 64 | /// 65 | /// - If no relationships are specified when serializing the dandy object, only its attributes will be serialized 66 | /// into json. 67 | /// - If ["hats", "gossip"] is specified when serializing the dandy object, Dandy's attributes will be serialize 68 | /// along with its hats and gossipe relationships. 69 | /// - If ["gossip.purveyor"] is specified when serializing the dandy object, Dandy's attributes, gossip, and 70 | /// gossip's purveyor will be serialized into json. 71 | /// 72 | /// Note: attributes with nil values will not be included in the returned json. However, nil relationships that are 73 | /// specified will be included as empty arrays or objects. 74 | /// 75 | /// - parameter object: An object to serialize into json 76 | /// - parameter relationships: Relationships and keypaths to nested relationships targeted for serialization. 77 | /// 78 | /// - returns: A json representation of this object and its relationships if one could be produced. Otherwise, nil. 79 | public static func serialize(_ object: NSManagedObject, 80 | including relationships: [String]? = nil) -> JSONObject? { 81 | var json = JSONObject() 82 | let map = EntityMapper.map(object.entity) 83 | if let map = map { 84 | for (property, description) in map { 85 | // Map attributes, ensuring mapping conversion 86 | if description.type == .attribute { 87 | json[property] = object.value(forKey: description.name) 88 | } 89 | else if let relationships = relationships 90 | , (relationships.contains(description.name) 91 | || nestedSerializationTargets(for: description.name, including: relationships)?.count > 0) { 92 | let nestedRelationships = nestedSerializationTargets(for: description.name, including: relationships) 93 | // Map relationships and recurse into nested relationships 94 | if description.toMany { 95 | let relatedObjects = description.ordered ? (object.value(forKey: description.name) as AnyObject).array 96 | : (object.value(forKey: description.name) as AnyObject).allObjects 97 | if let relatedObjects = relatedObjects as? [NSManagedObject] { 98 | if relatedObjects.count > 0 { 99 | json[property] = serialize(relatedObjects, including: nestedRelationships) 100 | } 101 | else { 102 | json[property] = [[:]] 103 | } 104 | } 105 | // Assume nils to denote empty objects 106 | else { 107 | json[property] = [[:]] 108 | } 109 | } 110 | else { 111 | if let relationship = object.value(forKey: description.name) as? NSManagedObject { 112 | json[property] = serialize(relationship, including: nestedRelationships) 113 | } 114 | // Assume nils to denote empty objects 115 | else { 116 | json[property] = [:] 117 | } 118 | } 119 | } 120 | } 121 | } 122 | if json.count == 0 { 123 | log(format("Failed to serialize object \(object) including relationships \(relationships)")) 124 | return nil 125 | } 126 | return json 127 | } 128 | 129 | /// Recursively invokes other class methods to produce a json array, including relationships. 130 | /// 131 | /// - parameter objects: An array of `NSManagedObjects` to serialize 132 | /// - parameter relationships: The relationships targeted for serialization. 133 | /// 134 | /// - returns: A json representation of the objects and their relationships if one could be produced. Otherwise, nil. 135 | public static func serialize(_ objects: [NSManagedObject], 136 | including relationships: [String]? = nil) -> [JSONObject]? { 137 | var json = [JSONObject]() 138 | for object in objects { 139 | if let relationshipJSON = serialize(object, including: relationships) { 140 | json.append(relationshipJSON) 141 | } 142 | } 143 | return json.count > 0 ? json: nil 144 | } 145 | 146 | /// Determines which relationships to a given relationship require serialization. Relationships to a relationship 147 | /// are referred to as "nested" relationships. Invoked at every "level" of serialization to recursively convert 148 | /// keypaths with the name of a top-level relationship into a string which no longer references that relationship. 149 | /// 150 | /// - parameter relationship: The top-level relationship to query. 151 | /// - parameter relationships: All serialization targets for the top-level object. 152 | /// 153 | /// - returns: An array of nested relationships targeted for serialization. 154 | static func nestedSerializationTargets(for relationship: String, 155 | including nestedRelationships: [String]?) -> [String]? { 156 | if let nestedRelationships = nestedRelationships { 157 | let keypaths = nestedRelationships.filter({$0.range(of: relationship) != nil && $0.range(of: ".") != nil}) 158 | // Eliminate the relationship name and the period, recursing one level deeper. 159 | let nestedTargets = keypaths.map({$0.replacingOccurrences(of: relationship + ".", with: "")}) 160 | return nestedTargets.count > 0 ? nestedTargets: nil 161 | } 162 | return nil 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /Sources/Extensions/Dictionary+Dandy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dictionary+Dandy.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 1/22/16. 6 | // Copyright © 2016 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import Foundation 29 | 30 | 31 | public extension Dictionary { 32 | /// Convenience function for adding values from one dictionary to another, like 33 | /// `NSMutableDictionary`'s `-addEntriesFromDictionary` 34 | public mutating func addEntriesFrom(_ dictionary: Dictionary) { 35 | for (key, value) in dictionary { 36 | self[key] = value 37 | } 38 | } 39 | } 40 | 41 | /// Functions similarly to `NSDictionary's` valueForKeyPath. 42 | /// 43 | /// - parameter keypath: The keypath of the value. 44 | /// - parameter dictionary: The dictionary in which the value may exist. 45 | /// 46 | /// - returns: The value at the given keypath is one exists. 47 | /// If no key exists at the specified keypath, nil is returned. 48 | func _value(at keypath: String, 49 | of dictionary: JSONObject) -> T? { 50 | let keys = keypath.components(separatedBy: ".") 51 | var copy = dictionary 52 | var possibleValue: Any? 53 | for key in keys { 54 | possibleValue = copy[key] ?? nil 55 | if let value = copy[key] as? JSONObject { 56 | copy = value 57 | } else if possibleValue is NSNull { 58 | // A null has been encountered in the dictionary. Return it, and ignore further potential nesting. 59 | return possibleValue as? T 60 | } 61 | } 62 | 63 | return possibleValue as? T 64 | } 65 | -------------------------------------------------------------------------------- /Sources/Extensions/NSEntityDescription+Dandy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSEntity+Dandy.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 1/22/16. 6 | // Copyright © 2016 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | // MARK: - NSEntityDescription+UserInfo - 31 | extension NSEntityDescription { 32 | /// Recursively collects all userInfo values from potential superentities 33 | var allUserInfo: [AnyHashable: Any]? { 34 | get { 35 | return collectedEntityValuesFromDictionaryClosure({return $0.userInfo as? JSONObject}) 36 | } 37 | } 38 | 39 | /// Recursively collects all attribute values from potential superentities 40 | var allAttributes: [String: NSAttributeDescription]? { 41 | get { 42 | return collectedEntityValuesFromDictionaryClosure({return $0.attributesByName}) as? [String: NSAttributeDescription] 43 | } 44 | } 45 | 46 | /// Recursively collects all relationship values from potential superentities 47 | var allRelationships: [String: NSRelationshipDescription]? { 48 | get { 49 | return collectedEntityValuesFromDictionaryClosure({return $0.relationshipsByName}) as? [String: NSRelationshipDescription] 50 | } 51 | } 52 | 53 | /// Recursively collects arbitrary values from potential superentities. This function contains the boilerplate 54 | /// required for collecting userInfo, attributesByName, and relationshipsByName. 55 | /// 56 | /// - parameter dictionaryClosure: A closure returning userInfo, attributesByName, or relationshipsByName. 57 | /// 58 | /// - returns: The values collected from the entity's hierarchy. 59 | fileprivate func collectedEntityValuesFromDictionaryClosure(_ dictionaryClosure: (NSEntityDescription) -> JSONObject?) -> JSONObject? { 60 | var values = JSONObject() 61 | // Collect values down the entity hierarchy, stopping on the current entity. 62 | // This approach ensures children override parent values. 63 | for entity in entityHierarchy { 64 | if let newValues = dictionaryClosure(entity) { 65 | values.addEntriesFrom(newValues) 66 | } 67 | } 68 | return values 69 | } 70 | 71 | /// - returns: The entity's hierarchy, sorted by "superiority". The most super entity will be the first element 72 | /// in the array, the current entity will be the last. 73 | fileprivate var entityHierarchy: [NSEntityDescription] { 74 | get { 75 | var entities = [NSEntityDescription]() 76 | var entity: NSEntityDescription? = self 77 | while let currentEntity = entity { 78 | entities.insert(currentEntity, at: 0) 79 | entity = entity?.superentity 80 | } 81 | return entities 82 | } 83 | } 84 | } 85 | 86 | 87 | // MARK: - NSEntityDescription+Construction - 88 | extension NSEntityDescription { 89 | class func forEntity(_ name: String) -> NSEntityDescription? { 90 | return NSEntityDescription.entity(forEntityName: name, in: Dandy.coordinator.mainContext) 91 | } 92 | 93 | class func forType(_ type: T.Type) -> NSEntityDescription? { 94 | return forEntity(String(describing: type)) 95 | } 96 | } 97 | 98 | // MARK: - NSEntityDescription+PrimaryKey - 99 | extension NSEntityDescription { 100 | /// Returns the primary key of of the `NSEntityDescription`, a value used to ensure a unique record 101 | /// for this entity. 102 | /// 103 | /// - returns: The property on the entity marked as a unique constraint or as its primaryKey if either is found. 104 | /// Otherwise, nil. 105 | var primaryKey: String? { 106 | get { 107 | return allUserInfo?[PRIMARY_KEY] as? String 108 | } 109 | } 110 | 111 | /// Returns whether the entity is unique or not. 112 | /// 113 | /// - returns: If the entity has either one unique constraint or an identified primaryKey attribute, true. 114 | /// Otherwise, false. 115 | var isUnique: Bool { 116 | get { 117 | return primaryKey != nil 118 | } 119 | } 120 | 121 | /// Extracts the value of a primary key from the passed in json if one can be extracted. Takes alternate mappings 122 | /// for the primaryKey into account. 123 | /// 124 | /// - parameter json: JSON form which a primaryKey will be extracted 125 | func primaryKeyValueFromJSON(_ json: JSONObject) -> Any? { 126 | if let primaryKey = primaryKey, 127 | let entityMap = EntityMapper.map(self) { 128 | let filteredMap = entityMap.filter({ $1.name == primaryKey }).map({ $0.0 }) 129 | // If the primary key has an alternate mapping, return the value from the alternate mapping. 130 | // Otherwise, return the json value matching the name of the primary key. 131 | if let mappedKey = filteredMap.first { 132 | return json[mappedKey] ?? nil 133 | } 134 | 135 | return json[primaryKey] ?? nil 136 | } 137 | return nil 138 | } 139 | 140 | /// Generates a predicate to fetch for the entity with a given primary key value 141 | /// 142 | /// - parameter primaryKeyValue: The value of the entity's primary key 143 | /// - returns: A predicate that may be used to fetch unique objects 144 | func primaryKeyPredicate(for primaryKeyValue: Any) -> NSPredicate? { 145 | if let primaryKey = primaryKey, 146 | let value: Any = CoreDataValueConverter.convert(primaryKeyValue, for: self, property: primaryKey) { 147 | return NSPredicate(format: "%K = %@", argumentArray: [primaryKey, value]) 148 | } 149 | return nil 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Sources/Extensions/NSFileManager+Directory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSFileManager+Directory.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 4/28/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | extension FileManager { 13 | /// The url of the NSFileManager's `.DocumentDirectory` 14 | static var documentDirectoryURL: URL { 15 | let urls = `default`.urls(for: .documentDirectory, in: .userDomainMask) 16 | 17 | if let url = urls.last { 18 | return url 19 | } 20 | 21 | preconditionFailure("Failed to find or a Documents directory.") 22 | } 23 | 24 | /// Returns whether a directory exists at a given file url. 25 | /// 26 | /// - parameter url: The file url of the directory. For instance "file://root/dandy/documents" 27 | /// 28 | /// - returns: Whether a directory exists at the given url. Note that false may indicate either that 29 | /// a file exists at this url or that nothing exists at this directory. 30 | static func directoryExists(at url: URL) -> Bool { 31 | let path = url.pathComponents.joined(separator: "/") 32 | return `default`.fileExists(atPath: path) 33 | } 34 | 35 | /// Creates a directory at a given URL. 36 | /// 37 | /// - parameter url: The file url of the directory to create. 38 | /// 39 | /// - throws: A number of errors may lead to an exception here. The two most common exceptions are raised 40 | /// when a directory already exists at this url or in response to insufficient user permissions. 41 | static func createDirectory(at url: URL) throws { 42 | try `default`.createDirectory(at: url, withIntermediateDirectories: false, attributes: nil) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/Extensions/NSManagedObject+Dandy.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSManagedObject+Dandy.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 10/31/16. 6 | // 7 | // 8 | 9 | import CoreData 10 | 11 | extension NSManagedObject { 12 | public class func entityDescription() -> NSEntityDescription? { 13 | return NSEntityDescription.entity(forEntityName: String(describing: self), 14 | in: Dandy.coordinator.mainContext) 15 | } 16 | 17 | public class func inserted() -> NSManagedObject? { 18 | if #available(iOS 10.0, *) { 19 | return self.init(context: Dandy.coordinator.mainContext) 20 | } else { 21 | if let description = entityDescription() { 22 | return NSManagedObject(entity: description, 23 | insertInto: Dandy.coordinator.mainContext) 24 | } 25 | } 26 | 27 | return nil 28 | } 29 | 30 | public class func type(named className: String) -> NSManagedObject.Type? { 31 | return NSClassFromString(className) as? NSManagedObject.Type 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/Value Conversion/ConvertibleType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConvertibleTypes.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 10/26/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import Foundation 29 | 30 | // MARK: - ConvertibleType - 31 | protocol BooleanConvertible { 32 | func convertToBoolean() -> Bool? 33 | } 34 | 35 | protocol DateConvertible { 36 | func convertToDate() -> Date? 37 | } 38 | 39 | protocol DataConvertible { 40 | func convertToData() -> Data? 41 | } 42 | 43 | protocol DoubleConvertible { 44 | func convertToDouble() -> Double? 45 | } 46 | 47 | protocol DecimalConvertible { 48 | func convertToDecimal() -> NSDecimalNumber? 49 | } 50 | 51 | protocol FloatConvertible { 52 | func convertToFloat() -> Float? 53 | } 54 | 55 | protocol IntConvertible { 56 | func convertToInt() -> Int? 57 | } 58 | 59 | protocol StringConvertible { 60 | func convertToString() -> String? 61 | } 62 | 63 | protocol NilConvertible { 64 | func convertToNil() -> Any? 65 | } 66 | 67 | protocol NumericConvertible: DoubleConvertible, DecimalConvertible, FloatConvertible, IntConvertible {} 68 | protocol ConvertibleType: BooleanConvertible, DataConvertible, DateConvertible, NumericConvertible, StringConvertible {} 69 | 70 | // MARK: - Date - 71 | extension Date: DateConvertible, NumericConvertible, StringConvertible { 72 | func convertToDate() -> Date? { 73 | return self 74 | } 75 | 76 | func convertToDecimal() -> NSDecimalNumber? { 77 | return NSDecimalNumber(value: self.timeIntervalSince1970 as Double) 78 | } 79 | 80 | func convertToDouble() -> Double? { 81 | return timeIntervalSince1970 82 | } 83 | 84 | func convertToFloat() -> Float? { 85 | return Float(timeIntervalSince1970) 86 | } 87 | 88 | func convertToInt() -> Int? { 89 | return Int(round(self.timeIntervalSince1970)) 90 | } 91 | 92 | func convertToString() -> String? { 93 | return CoreDataValueConverter.dateFormatter.string(from: self) 94 | } 95 | } 96 | // MARK: - Data - 97 | extension Data : DataConvertible, StringConvertible { 98 | func convertToData() -> Data? { 99 | return self 100 | } 101 | 102 | func convertToString() -> String? { 103 | return String(data: self, encoding: String.Encoding.utf8) 104 | } 105 | } 106 | // MARK: - Numbers - 107 | protocol NumericConvertibleType: SignedNumber, ConvertibleType { } 108 | extension NumericConvertibleType { 109 | private func asDouble() -> Double? { 110 | switch self { 111 | case let i as Int: return Double(i) 112 | case let f as Float: return Double(f) 113 | case let d as Double: return d 114 | default: return nil 115 | } 116 | } 117 | 118 | func convertToBoolean() -> Bool? { 119 | if self == 0 { 120 | return false 121 | } else if self >= 1 { 122 | return true 123 | } 124 | return nil 125 | } 126 | 127 | func convertToDate() -> Date? { 128 | if let double = asDouble() { 129 | return Date(timeIntervalSince1970: TimeInterval(double)) 130 | } 131 | return nil 132 | } 133 | 134 | func convertToData() -> Data? { 135 | var cpy = self 136 | return Data(bytes: &cpy, count: MemoryLayout.size) 137 | } 138 | 139 | func convertToDecimal() -> NSDecimalNumber? { 140 | if let double = asDouble() { 141 | return NSDecimalNumber(value: Double(double)) 142 | } 143 | return nil 144 | } 145 | 146 | func convertToDouble() -> Double? { 147 | if let double = asDouble() { 148 | return double 149 | } 150 | return nil 151 | } 152 | 153 | func convertToFloat() -> Float? { 154 | if let double = asDouble() { 155 | return Float(double) 156 | } 157 | return nil 158 | } 159 | 160 | func convertToInt() -> Int? { 161 | if let double = asDouble() { 162 | return Int(double) 163 | } 164 | return nil 165 | } 166 | 167 | func convertToString() -> String? { 168 | return "\(self)" 169 | } 170 | } 171 | extension Int: NumericConvertibleType { } 172 | extension Double: NumericConvertibleType { } 173 | extension Float: NumericConvertibleType { } 174 | 175 | extension NSNumber: ConvertibleType { 176 | func convertToBoolean() -> Bool? { 177 | return doubleValue.convertToBoolean() 178 | } 179 | 180 | func convertToDate() -> Date? { 181 | return doubleValue.convertToDate() 182 | } 183 | 184 | func convertToData() -> Data? { 185 | return doubleValue.convertToData() 186 | } 187 | 188 | func convertToDecimal() -> NSDecimalNumber? { 189 | return doubleValue.convertToDecimal() 190 | } 191 | 192 | func convertToDouble() -> Double? { 193 | return doubleValue.convertToDouble() 194 | } 195 | 196 | func convertToFloat() -> Float? { 197 | return doubleValue.convertToFloat() 198 | } 199 | 200 | func convertToInt() -> Int? { 201 | return doubleValue.convertToInt() 202 | } 203 | 204 | func convertToString() -> String? { 205 | return doubleValue.convertToString() 206 | } 207 | } 208 | 209 | // MARK: - Strings - 210 | extension String: ConvertibleType, NilConvertible { 211 | func convertToBoolean() -> Bool? { 212 | let lowercaseValue = lowercased() 213 | if lowercaseValue == "yes" 214 | || lowercaseValue == "true" 215 | || lowercaseValue == "1" { 216 | return true 217 | } else if lowercaseValue == "no" 218 | || lowercaseValue == "false" 219 | || lowercaseValue == "0" { 220 | return false 221 | } 222 | return nil 223 | } 224 | 225 | func convertToDate() -> Date? { 226 | return CoreDataValueConverter.dateFormatter.date(from: self as String) 227 | } 228 | 229 | func convertToData() -> Data? { 230 | return data(using: String.Encoding(rawValue: String.Encoding.utf8.rawValue)) 231 | } 232 | 233 | func convertToDecimal() -> NSDecimalNumber? { 234 | if let num = Double(self) { 235 | return NSDecimalNumber(value: num) 236 | } 237 | 238 | return nil 239 | } 240 | 241 | func convertToDouble() -> Double? { 242 | return Double(self) 243 | } 244 | 245 | func convertToFloat() -> Float? { 246 | return Float(self) 247 | } 248 | 249 | func convertToInt() -> Int? { 250 | return Int(self) 251 | } 252 | 253 | func convertToString() -> String? { 254 | if lowercased() == "null" 255 | || lowercased() == "nil" { 256 | return nil 257 | } 258 | 259 | return self 260 | } 261 | 262 | func convertToNil() -> Any? { 263 | return convertToString() 264 | } 265 | } 266 | 267 | extension NSString: ConvertibleType, NilConvertible { 268 | func convertToBoolean() -> Bool? { 269 | return String(self).convertToBoolean() 270 | } 271 | 272 | func convertToDate() -> Date? { 273 | return String(self).convertToDate() 274 | } 275 | 276 | func convertToData() -> Data? { 277 | return String(self).convertToData() 278 | } 279 | 280 | func convertToDecimal() -> NSDecimalNumber? { 281 | return String(self).convertToDecimal() 282 | } 283 | 284 | func convertToDouble() -> Double? { 285 | return String(self).convertToDouble() 286 | } 287 | 288 | func convertToFloat() -> Float? { 289 | return String(self).convertToFloat() 290 | } 291 | 292 | func convertToInt() -> Int? { 293 | return String(self).convertToInt() 294 | } 295 | 296 | func convertToString() -> String? { 297 | return String(self).convertToString() 298 | } 299 | 300 | func convertToNil() -> Any? { 301 | return String(self).convertToNil() 302 | } 303 | } 304 | 305 | extension NSNull: NilConvertible { 306 | func convertToNil() -> Any? { 307 | return nil 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /Sources/Value Conversion/CoreDataValueConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeConverters.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 10/26/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import CoreData 29 | 30 | /// The central type conversion class. 31 | /// 32 | /// `CoreDataValueConverter` compares a given value to a given entity's property type, then attempts to convert the value 33 | /// to the property type. This class ensures that values written to the NSManagedObject are always of the appropriate 34 | /// tyope. 35 | /// 36 | /// The actual conversion is conducted by an appropriate `ValueConverter`. 37 | public struct CoreDataValueConverter { 38 | /// A shared dateFormatter for regularly converting strings of a known pattern 39 | /// to dates and vice-versa. 40 | public static let dateFormatter = DateFormatter() 41 | 42 | /// Maps `NSAttributeTypes` to their corresponding type converters. 43 | fileprivate static let typeConverters: [NSAttributeType: ValueConverter] = [ 44 | .integer16AttributeType: IntConverter(), 45 | .integer32AttributeType: IntConverter(), 46 | .integer64AttributeType: IntConverter(), 47 | .decimalAttributeType: DecimalConverter(), 48 | .doubleAttributeType: DoubleConverter(), 49 | .floatAttributeType: FloatConverter(), 50 | .stringAttributeType: StringConverter(), 51 | .booleanAttributeType: BooleanConverter(), 52 | .dateAttributeType: DateConverter(), 53 | .binaryDataAttributeType: DataConverter() 54 | ] 55 | 56 | /// Attempts to convert a given value to a type matching the specified entity property type. For instance, 57 | /// if "3" is passed but the specified entity's property is defined as an NSNumber, @3 will be returned. 58 | /// 59 | /// For a list of supported `value` types, see `ConvertibleType` 60 | /// 61 | /// - parameter value: The value to convert. 62 | /// - parameter entity: The entity this value is expected to map to. 63 | /// - parameter property: The property on the entity where this value will be written. 64 | /// 65 | /// - returns: If the conversion was successful, the converted value. Otherwise, nil. 66 | public static func convert(_ value: Any, 67 | for entity: NSEntityDescription, 68 | property: String) -> Any? { 69 | if let attributeDescription = entity.propertiesByName[property] as? NSAttributeDescription { 70 | return convert(value, to: attributeDescription.attributeType) 71 | } 72 | return nil 73 | } 74 | 75 | /// The class's central function. Attempts to convert values from one type to another. 76 | /// In general, this method is invoked indirectly via convertValue:forEntity:property 77 | /// 78 | /// - parameter value: The value to convert. 79 | /// - parameter type: The desired end type of the value. 80 | /// 81 | /// - returns: If the conversion was successful, the converted value. Otherwise, nil. 82 | public static func convert(_ value: Any, 83 | to type: NSAttributeType) -> Any? { 84 | if let converter = typeConverters[type] { 85 | return converter.convert(value) 86 | } 87 | return nil 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Sources/Value Conversion/ValueConverter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TypeConverters.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 10/26/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import Foundation 29 | 30 | /// Adopters of this protocol provide implementations of `convert(_:)` so that all type conversions may proceed 31 | /// via a consistent interface. 32 | protocol ValueConverter { 33 | /// Attempts to convert a value from one type to another. Note that the value must be castable to a specified type 34 | /// for the conversion to succeed. 35 | func convert(_ value: Any) -> Any? 36 | /// A convenience function for checking type conformance before attempting type conversion. 37 | func convert(_ value: Any, to type: T.Type, using converter: (T) -> Any?) -> Any? 38 | } 39 | extension ValueConverter { 40 | func convert(_ value: Any, to type: T.Type, using converter: (T) -> Any?) -> Any? { 41 | if let value = value as? T { 42 | return converter(value) 43 | } 44 | return nil 45 | } 46 | } 47 | 48 | struct BooleanConverter: ValueConverter { 49 | func convert(_ value: Any) -> Any? { 50 | return convert(value, to: BooleanConvertible.self) { $0.convertToBoolean() } 51 | } 52 | } 53 | 54 | struct DateConverter: ValueConverter { 55 | func convert(_ value: Any) -> Any? { 56 | return convert(value, to: DateConvertible.self) { $0.convertToDate() } 57 | } 58 | } 59 | 60 | struct DataConverter: ValueConverter { 61 | func convert(_ value: Any) -> Any? { 62 | return convert(value, to: DataConvertible.self) { $0.convertToData() } 63 | } 64 | } 65 | 66 | struct DoubleConverter: ValueConverter { 67 | func convert(_ value: Any) -> Any? { 68 | return convert(value, to: DoubleConvertible.self) { $0.convertToDouble() } 69 | } 70 | } 71 | 72 | struct DecimalConverter: ValueConverter { 73 | func convert(_ value: Any) -> Any? { 74 | return convert(value, to: DecimalConvertible.self) { $0.convertToDecimal() } 75 | } 76 | } 77 | 78 | struct FloatConverter: ValueConverter { 79 | func convert(_ value: Any) -> Any? { 80 | return convert(value, to: FloatConvertible.self) { $0.convertToFloat() } 81 | } 82 | } 83 | 84 | struct IntConverter: ValueConverter { 85 | func convert(_ value: Any) -> Any? { 86 | return convert(value, to: IntConvertible.self) { $0.convertToInt() } 87 | } 88 | } 89 | 90 | struct StringConverter: ValueConverter { 91 | func convert(_ value: Any) -> Any? { 92 | return convert(value, to: StringConvertible.self) { $0.convertToString() } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Tests/.swiftlint.yml: -------------------------------------------------------------------------------- 1 | reporter: "xcode" 2 | line_length: 180 3 | excluded: 4 | - CoreDataDandyTests/CoreDataDandyTests.swift 5 | - CoreDataDandyTests/Conclusion+CoreDataProperties.swift 6 | disabled_rules: 7 | - trailing_newline 8 | - type_name 9 | - variable_name 10 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 981C80231DC919BF003EFB04 /* DandyModel.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 981C80211DC919BF003EFB04 /* DandyModel.xcdatamodeld */; }; 11 | 9836F9021CBEFE2300AD92DF /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F01CBEFE2300AD92DF /* Constants.swift */; }; 12 | 9836F9041CBEFE2300AD92DF /* CoreDataDandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F31CBEFE2300AD92DF /* CoreDataDandy.swift */; }; 13 | 9836F9051CBEFE2300AD92DF /* EntityMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F41CBEFE2300AD92DF /* EntityMapper.swift */; }; 14 | 9836F9061CBEFE2300AD92DF /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F51CBEFE2300AD92DF /* Logger.swift */; }; 15 | 9836F9071CBEFE2300AD92DF /* MappingFinalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F61CBEFE2300AD92DF /* MappingFinalizer.swift */; }; 16 | 9836F9081CBEFE2300AD92DF /* ObjectFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F71CBEFE2300AD92DF /* ObjectFactory.swift */; }; 17 | 9836F9091CBEFE2300AD92DF /* PersistentStackCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F81CBEFE2300AD92DF /* PersistentStackCoordinator.swift */; }; 18 | 9836F90A1CBEFE2300AD92DF /* PropertyDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8F91CBEFE2300AD92DF /* PropertyDescription.swift */; }; 19 | 9836F90B1CBEFE2300AD92DF /* Serializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8FA1CBEFE2300AD92DF /* Serializer.swift */; }; 20 | 9836F90C1CBEFE2300AD92DF /* Dictionary+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8FC1CBEFE2300AD92DF /* Dictionary+Dandy.swift */; }; 21 | 9836F90D1CBEFE2300AD92DF /* NSEntityDescription+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8FD1CBEFE2300AD92DF /* NSEntityDescription+Dandy.swift */; }; 22 | 9836F90E1CBEFE2300AD92DF /* ConvertibleType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F8FF1CBEFE2300AD92DF /* ConvertibleType.swift */; }; 23 | 9836F90F1CBEFE2300AD92DF /* CoreDataValueConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F9001CBEFE2300AD92DF /* CoreDataValueConverter.swift */; }; 24 | 9836F9101CBEFE2300AD92DF /* ValueConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9836F9011CBEFE2300AD92DF /* ValueConverter.swift */; }; 25 | 9868D5541DC7A2FA000C2E49 /* NSManagedObject+Dandy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9868D5531DC7A2FA000C2E49 /* NSManagedObject+Dandy.swift */; }; 26 | 986BAF9F1CD2C3BB00F309C0 /* NSFileManager+Directory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986BAF9E1CD2C3BB00F309C0 /* NSFileManager+Directory.swift */; }; 27 | 986F4AC31DCA7F71009AC3C3 /* Conclusion+Finalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986F4AC21DCA7F71009AC3C3 /* Conclusion+Finalization.swift */; }; 28 | 98713E351DD4E87900986E52 /* Gossip+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E231DD4E87900986E52 /* Gossip+CoreDataClass.swift */; }; 29 | 98713E361DD4E87900986E52 /* Gossip+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E241DD4E87900986E52 /* Gossip+CoreDataProperties.swift */; }; 30 | 98713E371DD4E87900986E52 /* Slander+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E251DD4E87900986E52 /* Slander+CoreDataClass.swift */; }; 31 | 98713E381DD4E87900986E52 /* Slander+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E261DD4E87900986E52 /* Slander+CoreDataProperties.swift */; }; 32 | 98713E391DD4E87900986E52 /* Space+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E271DD4E87900986E52 /* Space+CoreDataClass.swift */; }; 33 | 98713E3A1DD4E87900986E52 /* Space+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E281DD4E87900986E52 /* Space+CoreDataProperties.swift */; }; 34 | 98713E3B1DD4E87900986E52 /* Hat+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E291DD4E87900986E52 /* Hat+CoreDataClass.swift */; }; 35 | 98713E3C1DD4E87900986E52 /* Hat+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2A1DD4E87900986E52 /* Hat+CoreDataProperties.swift */; }; 36 | 98713E3D1DD4E87900986E52 /* Flattery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2B1DD4E87900986E52 /* Flattery+CoreDataClass.swift */; }; 37 | 98713E3E1DD4E87900986E52 /* Flattery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2C1DD4E87900986E52 /* Flattery+CoreDataProperties.swift */; }; 38 | 98713E3F1DD4E87900986E52 /* Conclusion+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2D1DD4E87900986E52 /* Conclusion+CoreDataClass.swift */; }; 39 | 98713E401DD4E87900986E52 /* Conclusion+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2E1DD4E87900986E52 /* Conclusion+CoreDataProperties.swift */; }; 40 | 98713E411DD4E87900986E52 /* Material+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E2F1DD4E87900986E52 /* Material+CoreDataClass.swift */; }; 41 | 98713E421DD4E87900986E52 /* Material+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E301DD4E87900986E52 /* Material+CoreDataProperties.swift */; }; 42 | 98713E431DD4E87900986E52 /* Plebian+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E311DD4E87900986E52 /* Plebian+CoreDataClass.swift */; }; 43 | 98713E441DD4E87900986E52 /* Plebian+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E321DD4E87900986E52 /* Plebian+CoreDataProperties.swift */; }; 44 | 98713E451DD4E87900986E52 /* Dandy_+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E331DD4E87900986E52 /* Dandy_+CoreDataClass.swift */; }; 45 | 98713E461DD4E87900986E52 /* Dandy_+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98713E341DD4E87900986E52 /* Dandy_+CoreDataProperties.swift */; }; 46 | 988F4A6A1B3632EE00E71046 /* CoreDataDandyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 988F4A691B3632EE00E71046 /* CoreDataDandyTests.swift */; }; 47 | /* End PBXBuildFile section */ 48 | 49 | /* Begin PBXFileReference section */ 50 | 981C80221DC919BF003EFB04 /* DandyModel.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DandyModel.xcdatamodel; sourceTree = ""; }; 51 | 9836F8F01CBEFE2300AD92DF /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 52 | 9836F8F31CBEFE2300AD92DF /* CoreDataDandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataDandy.swift; sourceTree = ""; }; 53 | 9836F8F41CBEFE2300AD92DF /* EntityMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EntityMapper.swift; sourceTree = ""; }; 54 | 9836F8F51CBEFE2300AD92DF /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 55 | 9836F8F61CBEFE2300AD92DF /* MappingFinalizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MappingFinalizer.swift; sourceTree = ""; }; 56 | 9836F8F71CBEFE2300AD92DF /* ObjectFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjectFactory.swift; sourceTree = ""; }; 57 | 9836F8F81CBEFE2300AD92DF /* PersistentStackCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PersistentStackCoordinator.swift; sourceTree = ""; }; 58 | 9836F8F91CBEFE2300AD92DF /* PropertyDescription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PropertyDescription.swift; sourceTree = ""; }; 59 | 9836F8FA1CBEFE2300AD92DF /* Serializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Serializer.swift; sourceTree = ""; }; 60 | 9836F8FC1CBEFE2300AD92DF /* Dictionary+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Dandy.swift"; sourceTree = ""; }; 61 | 9836F8FD1CBEFE2300AD92DF /* NSEntityDescription+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSEntityDescription+Dandy.swift"; sourceTree = ""; }; 62 | 9836F8FF1CBEFE2300AD92DF /* ConvertibleType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConvertibleType.swift; sourceTree = ""; }; 63 | 9836F9001CBEFE2300AD92DF /* CoreDataValueConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CoreDataValueConverter.swift; sourceTree = ""; }; 64 | 9836F9011CBEFE2300AD92DF /* ValueConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ValueConverter.swift; sourceTree = ""; }; 65 | 9868D5531DC7A2FA000C2E49 /* NSManagedObject+Dandy.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSManagedObject+Dandy.swift"; sourceTree = ""; }; 66 | 986BAF9E1CD2C3BB00F309C0 /* NSFileManager+Directory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSFileManager+Directory.swift"; sourceTree = ""; }; 67 | 986F4AC21DCA7F71009AC3C3 /* Conclusion+Finalization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Conclusion+Finalization.swift"; sourceTree = ""; }; 68 | 98713E231DD4E87900986E52 /* Gossip+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Gossip+CoreDataClass.swift"; sourceTree = ""; }; 69 | 98713E241DD4E87900986E52 /* Gossip+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Gossip+CoreDataProperties.swift"; sourceTree = ""; }; 70 | 98713E251DD4E87900986E52 /* Slander+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Slander+CoreDataClass.swift"; sourceTree = ""; }; 71 | 98713E261DD4E87900986E52 /* Slander+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Slander+CoreDataProperties.swift"; sourceTree = ""; }; 72 | 98713E271DD4E87900986E52 /* Space+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Space+CoreDataClass.swift"; sourceTree = ""; }; 73 | 98713E281DD4E87900986E52 /* Space+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Space+CoreDataProperties.swift"; sourceTree = ""; }; 74 | 98713E291DD4E87900986E52 /* Hat+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hat+CoreDataClass.swift"; sourceTree = ""; }; 75 | 98713E2A1DD4E87900986E52 /* Hat+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Hat+CoreDataProperties.swift"; sourceTree = ""; }; 76 | 98713E2B1DD4E87900986E52 /* Flattery+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Flattery+CoreDataClass.swift"; sourceTree = ""; }; 77 | 98713E2C1DD4E87900986E52 /* Flattery+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Flattery+CoreDataProperties.swift"; sourceTree = ""; }; 78 | 98713E2D1DD4E87900986E52 /* Conclusion+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Conclusion+CoreDataClass.swift"; sourceTree = ""; }; 79 | 98713E2E1DD4E87900986E52 /* Conclusion+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Conclusion+CoreDataProperties.swift"; sourceTree = ""; }; 80 | 98713E2F1DD4E87900986E52 /* Material+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CoreDataClass.swift"; sourceTree = ""; }; 81 | 98713E301DD4E87900986E52 /* Material+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Material+CoreDataProperties.swift"; sourceTree = ""; }; 82 | 98713E311DD4E87900986E52 /* Plebian+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Plebian+CoreDataClass.swift"; sourceTree = ""; }; 83 | 98713E321DD4E87900986E52 /* Plebian+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Plebian+CoreDataProperties.swift"; sourceTree = ""; }; 84 | 98713E331DD4E87900986E52 /* Dandy_+CoreDataClass.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dandy_+CoreDataClass.swift"; sourceTree = ""; }; 85 | 98713E341DD4E87900986E52 /* Dandy_+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dandy_+CoreDataProperties.swift"; sourceTree = ""; }; 86 | 988F4A641B3632EE00E71046 /* CoreDataDandyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CoreDataDandyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 87 | 988F4A681B3632EE00E71046 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 88 | 988F4A691B3632EE00E71046 /* CoreDataDandyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataDandyTests.swift; sourceTree = ""; }; 89 | /* End PBXFileReference section */ 90 | 91 | /* Begin PBXFrameworksBuildPhase section */ 92 | 988F4A611B3632EE00E71046 /* Frameworks */ = { 93 | isa = PBXFrameworksBuildPhase; 94 | buildActionMask = 2147483647; 95 | files = ( 96 | ); 97 | runOnlyForDeploymentPostprocessing = 0; 98 | }; 99 | /* End PBXFrameworksBuildPhase section */ 100 | 101 | /* Begin PBXGroup section */ 102 | 9836F8EF1CBEFE2300AD92DF /* CoreDataDandy */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | 9836F8F01CBEFE2300AD92DF /* Constants.swift */, 106 | 9836F8F11CBEFE2300AD92DF /* Core */, 107 | 9836F8FB1CBEFE2300AD92DF /* Extensions */, 108 | 9836F8FE1CBEFE2300AD92DF /* Value Conversion */, 109 | ); 110 | name = CoreDataDandy; 111 | path = ../Sources; 112 | sourceTree = ""; 113 | }; 114 | 9836F8F11CBEFE2300AD92DF /* Core */ = { 115 | isa = PBXGroup; 116 | children = ( 117 | 9836F8F31CBEFE2300AD92DF /* CoreDataDandy.swift */, 118 | 9836F8F41CBEFE2300AD92DF /* EntityMapper.swift */, 119 | 9836F8F51CBEFE2300AD92DF /* Logger.swift */, 120 | 9836F8F61CBEFE2300AD92DF /* MappingFinalizer.swift */, 121 | 9836F8F71CBEFE2300AD92DF /* ObjectFactory.swift */, 122 | 9836F8F81CBEFE2300AD92DF /* PersistentStackCoordinator.swift */, 123 | 9836F8F91CBEFE2300AD92DF /* PropertyDescription.swift */, 124 | 9836F8FA1CBEFE2300AD92DF /* Serializer.swift */, 125 | ); 126 | path = Core; 127 | sourceTree = ""; 128 | }; 129 | 9836F8FB1CBEFE2300AD92DF /* Extensions */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 9836F8FC1CBEFE2300AD92DF /* Dictionary+Dandy.swift */, 133 | 9836F8FD1CBEFE2300AD92DF /* NSEntityDescription+Dandy.swift */, 134 | 9868D5531DC7A2FA000C2E49 /* NSManagedObject+Dandy.swift */, 135 | 986BAF9E1CD2C3BB00F309C0 /* NSFileManager+Directory.swift */, 136 | ); 137 | path = Extensions; 138 | sourceTree = ""; 139 | }; 140 | 9836F8FE1CBEFE2300AD92DF /* Value Conversion */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | 9836F8FF1CBEFE2300AD92DF /* ConvertibleType.swift */, 144 | 9836F9001CBEFE2300AD92DF /* CoreDataValueConverter.swift */, 145 | 9836F9011CBEFE2300AD92DF /* ValueConverter.swift */, 146 | ); 147 | path = "Value Conversion"; 148 | sourceTree = ""; 149 | }; 150 | 9867478B1CBBFD6400559E35 /* Models */ = { 151 | isa = PBXGroup; 152 | children = ( 153 | 98713E231DD4E87900986E52 /* Gossip+CoreDataClass.swift */, 154 | 98713E241DD4E87900986E52 /* Gossip+CoreDataProperties.swift */, 155 | 98713E251DD4E87900986E52 /* Slander+CoreDataClass.swift */, 156 | 98713E261DD4E87900986E52 /* Slander+CoreDataProperties.swift */, 157 | 98713E271DD4E87900986E52 /* Space+CoreDataClass.swift */, 158 | 98713E281DD4E87900986E52 /* Space+CoreDataProperties.swift */, 159 | 98713E291DD4E87900986E52 /* Hat+CoreDataClass.swift */, 160 | 98713E2A1DD4E87900986E52 /* Hat+CoreDataProperties.swift */, 161 | 98713E2B1DD4E87900986E52 /* Flattery+CoreDataClass.swift */, 162 | 98713E2C1DD4E87900986E52 /* Flattery+CoreDataProperties.swift */, 163 | 98713E2D1DD4E87900986E52 /* Conclusion+CoreDataClass.swift */, 164 | 98713E2E1DD4E87900986E52 /* Conclusion+CoreDataProperties.swift */, 165 | 98713E2F1DD4E87900986E52 /* Material+CoreDataClass.swift */, 166 | 98713E301DD4E87900986E52 /* Material+CoreDataProperties.swift */, 167 | 98713E311DD4E87900986E52 /* Plebian+CoreDataClass.swift */, 168 | 98713E321DD4E87900986E52 /* Plebian+CoreDataProperties.swift */, 169 | 98713E331DD4E87900986E52 /* Dandy_+CoreDataClass.swift */, 170 | 98713E341DD4E87900986E52 /* Dandy_+CoreDataProperties.swift */, 171 | 986F4AC21DCA7F71009AC3C3 /* Conclusion+Finalization.swift */, 172 | ); 173 | path = Models; 174 | sourceTree = ""; 175 | }; 176 | 988F4A591B3632DC00E71046 = { 177 | isa = PBXGroup; 178 | children = ( 179 | 9836F8EF1CBEFE2300AD92DF /* CoreDataDandy */, 180 | 988F4A661B3632EE00E71046 /* CoreDataDandyTests */, 181 | 988F4A651B3632EE00E71046 /* Products */, 182 | ); 183 | sourceTree = ""; 184 | }; 185 | 988F4A651B3632EE00E71046 /* Products */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 988F4A641B3632EE00E71046 /* CoreDataDandyTests.xctest */, 189 | ); 190 | name = Products; 191 | sourceTree = ""; 192 | }; 193 | 988F4A661B3632EE00E71046 /* CoreDataDandyTests */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 981C80211DC919BF003EFB04 /* DandyModel.xcdatamodeld */, 197 | 9867478B1CBBFD6400559E35 /* Models */, 198 | 988F4A691B3632EE00E71046 /* CoreDataDandyTests.swift */, 199 | 988F4A671B3632EE00E71046 /* Supporting Files */, 200 | ); 201 | path = CoreDataDandyTests; 202 | sourceTree = ""; 203 | }; 204 | 988F4A671B3632EE00E71046 /* Supporting Files */ = { 205 | isa = PBXGroup; 206 | children = ( 207 | 988F4A681B3632EE00E71046 /* Info.plist */, 208 | ); 209 | name = "Supporting Files"; 210 | sourceTree = ""; 211 | }; 212 | /* End PBXGroup section */ 213 | 214 | /* Begin PBXNativeTarget section */ 215 | 988F4A631B3632EE00E71046 /* CoreDataDandyTests */ = { 216 | isa = PBXNativeTarget; 217 | buildConfigurationList = 988F4A6B1B3632EE00E71046 /* Build configuration list for PBXNativeTarget "CoreDataDandyTests" */; 218 | buildPhases = ( 219 | 988F4A601B3632EE00E71046 /* Sources */, 220 | 988F4A611B3632EE00E71046 /* Frameworks */, 221 | 988F4A621B3632EE00E71046 /* Resources */, 222 | ); 223 | buildRules = ( 224 | ); 225 | dependencies = ( 226 | ); 227 | name = CoreDataDandyTests; 228 | productName = CoreDataDandyTests; 229 | productReference = 988F4A641B3632EE00E71046 /* CoreDataDandyTests.xctest */; 230 | productType = "com.apple.product-type.bundle.unit-test"; 231 | }; 232 | /* End PBXNativeTarget section */ 233 | 234 | /* Begin PBXProject section */ 235 | 988F4A5A1B3632DC00E71046 /* Project object */ = { 236 | isa = PBXProject; 237 | attributes = { 238 | LastSwiftUpdateCheck = 0710; 239 | LastUpgradeCheck = 0800; 240 | TargetAttributes = { 241 | 988F4A631B3632EE00E71046 = { 242 | CreatedOnToolsVersion = 6.3.2; 243 | LastSwiftMigration = 0800; 244 | }; 245 | }; 246 | }; 247 | buildConfigurationList = 988F4A5D1B3632DC00E71046 /* Build configuration list for PBXProject "CoreDataDandyTests" */; 248 | compatibilityVersion = "Xcode 3.2"; 249 | developmentRegion = English; 250 | hasScannedForEncodings = 0; 251 | knownRegions = ( 252 | en, 253 | ); 254 | mainGroup = 988F4A591B3632DC00E71046; 255 | productRefGroup = 988F4A651B3632EE00E71046 /* Products */; 256 | projectDirPath = ""; 257 | projectRoot = ""; 258 | targets = ( 259 | 988F4A631B3632EE00E71046 /* CoreDataDandyTests */, 260 | ); 261 | }; 262 | /* End PBXProject section */ 263 | 264 | /* Begin PBXResourcesBuildPhase section */ 265 | 988F4A621B3632EE00E71046 /* Resources */ = { 266 | isa = PBXResourcesBuildPhase; 267 | buildActionMask = 2147483647; 268 | files = ( 269 | ); 270 | runOnlyForDeploymentPostprocessing = 0; 271 | }; 272 | /* End PBXResourcesBuildPhase section */ 273 | 274 | /* Begin PBXSourcesBuildPhase section */ 275 | 988F4A601B3632EE00E71046 /* Sources */ = { 276 | isa = PBXSourcesBuildPhase; 277 | buildActionMask = 2147483647; 278 | files = ( 279 | 9836F90F1CBEFE2300AD92DF /* CoreDataValueConverter.swift in Sources */, 280 | 9868D5541DC7A2FA000C2E49 /* NSManagedObject+Dandy.swift in Sources */, 281 | 9836F90A1CBEFE2300AD92DF /* PropertyDescription.swift in Sources */, 282 | 98713E371DD4E87900986E52 /* Slander+CoreDataClass.swift in Sources */, 283 | 9836F90C1CBEFE2300AD92DF /* Dictionary+Dandy.swift in Sources */, 284 | 9836F9081CBEFE2300AD92DF /* ObjectFactory.swift in Sources */, 285 | 986F4AC31DCA7F71009AC3C3 /* Conclusion+Finalization.swift in Sources */, 286 | 988F4A6A1B3632EE00E71046 /* CoreDataDandyTests.swift in Sources */, 287 | 98713E451DD4E87900986E52 /* Dandy_+CoreDataClass.swift in Sources */, 288 | 9836F9051CBEFE2300AD92DF /* EntityMapper.swift in Sources */, 289 | 9836F9071CBEFE2300AD92DF /* MappingFinalizer.swift in Sources */, 290 | 98713E411DD4E87900986E52 /* Material+CoreDataClass.swift in Sources */, 291 | 986BAF9F1CD2C3BB00F309C0 /* NSFileManager+Directory.swift in Sources */, 292 | 9836F9021CBEFE2300AD92DF /* Constants.swift in Sources */, 293 | 98713E431DD4E87900986E52 /* Plebian+CoreDataClass.swift in Sources */, 294 | 98713E3F1DD4E87900986E52 /* Conclusion+CoreDataClass.swift in Sources */, 295 | 9836F9041CBEFE2300AD92DF /* CoreDataDandy.swift in Sources */, 296 | 98713E3C1DD4E87900986E52 /* Hat+CoreDataProperties.swift in Sources */, 297 | 98713E3D1DD4E87900986E52 /* Flattery+CoreDataClass.swift in Sources */, 298 | 98713E461DD4E87900986E52 /* Dandy_+CoreDataProperties.swift in Sources */, 299 | 9836F90B1CBEFE2300AD92DF /* Serializer.swift in Sources */, 300 | 98713E441DD4E87900986E52 /* Plebian+CoreDataProperties.swift in Sources */, 301 | 98713E381DD4E87900986E52 /* Slander+CoreDataProperties.swift in Sources */, 302 | 9836F9101CBEFE2300AD92DF /* ValueConverter.swift in Sources */, 303 | 98713E3A1DD4E87900986E52 /* Space+CoreDataProperties.swift in Sources */, 304 | 98713E361DD4E87900986E52 /* Gossip+CoreDataProperties.swift in Sources */, 305 | 98713E3E1DD4E87900986E52 /* Flattery+CoreDataProperties.swift in Sources */, 306 | 9836F90D1CBEFE2300AD92DF /* NSEntityDescription+Dandy.swift in Sources */, 307 | 9836F9091CBEFE2300AD92DF /* PersistentStackCoordinator.swift in Sources */, 308 | 98713E401DD4E87900986E52 /* Conclusion+CoreDataProperties.swift in Sources */, 309 | 981C80231DC919BF003EFB04 /* DandyModel.xcdatamodeld in Sources */, 310 | 98713E391DD4E87900986E52 /* Space+CoreDataClass.swift in Sources */, 311 | 9836F9061CBEFE2300AD92DF /* Logger.swift in Sources */, 312 | 98713E421DD4E87900986E52 /* Material+CoreDataProperties.swift in Sources */, 313 | 98713E351DD4E87900986E52 /* Gossip+CoreDataClass.swift in Sources */, 314 | 9836F90E1CBEFE2300AD92DF /* ConvertibleType.swift in Sources */, 315 | 98713E3B1DD4E87900986E52 /* Hat+CoreDataClass.swift in Sources */, 316 | ); 317 | runOnlyForDeploymentPostprocessing = 0; 318 | }; 319 | /* End PBXSourcesBuildPhase section */ 320 | 321 | /* Begin XCBuildConfiguration section */ 322 | 988F4A5E1B3632DC00E71046 /* Debug */ = { 323 | isa = XCBuildConfiguration; 324 | buildSettings = { 325 | CLANG_WARN_BOOL_CONVERSION = YES; 326 | CLANG_WARN_CONSTANT_CONVERSION = YES; 327 | CLANG_WARN_EMPTY_BODY = YES; 328 | CLANG_WARN_ENUM_CONVERSION = YES; 329 | CLANG_WARN_INFINITE_RECURSION = YES; 330 | CLANG_WARN_INT_CONVERSION = YES; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_UNREACHABLE_CODE = YES; 333 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 334 | ENABLE_STRICT_OBJC_MSGSEND = YES; 335 | ENABLE_TESTABILITY = YES; 336 | GCC_NO_COMMON_BLOCKS = YES; 337 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 338 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 339 | GCC_WARN_UNDECLARED_SELECTOR = YES; 340 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 341 | GCC_WARN_UNUSED_FUNCTION = YES; 342 | GCC_WARN_UNUSED_VARIABLE = YES; 343 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 344 | ONLY_ACTIVE_ARCH = YES; 345 | PROVISIONING_PROFILE = ""; 346 | SDKROOT = iphoneos; 347 | }; 348 | name = Debug; 349 | }; 350 | 988F4A5F1B3632DC00E71046 /* Release */ = { 351 | isa = XCBuildConfiguration; 352 | buildSettings = { 353 | CLANG_WARN_BOOL_CONVERSION = YES; 354 | CLANG_WARN_CONSTANT_CONVERSION = YES; 355 | CLANG_WARN_EMPTY_BODY = YES; 356 | CLANG_WARN_ENUM_CONVERSION = YES; 357 | CLANG_WARN_INFINITE_RECURSION = YES; 358 | CLANG_WARN_INT_CONVERSION = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNREACHABLE_CODE = YES; 361 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 362 | ENABLE_STRICT_OBJC_MSGSEND = YES; 363 | ENABLE_TESTABILITY = YES; 364 | GCC_NO_COMMON_BLOCKS = YES; 365 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 366 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 367 | GCC_WARN_UNDECLARED_SELECTOR = YES; 368 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 369 | GCC_WARN_UNUSED_FUNCTION = YES; 370 | GCC_WARN_UNUSED_VARIABLE = YES; 371 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 372 | PROVISIONING_PROFILE = ""; 373 | SDKROOT = iphoneos; 374 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 375 | }; 376 | name = Release; 377 | }; 378 | 988F4A6C1B3632EE00E71046 /* Debug */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | ALWAYS_SEARCH_USER_PATHS = NO; 382 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 383 | CLANG_CXX_LIBRARY = "libc++"; 384 | CLANG_ENABLE_MODULES = YES; 385 | CLANG_ENABLE_OBJC_ARC = YES; 386 | CLANG_WARN_BOOL_CONVERSION = YES; 387 | CLANG_WARN_CONSTANT_CONVERSION = YES; 388 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 389 | CLANG_WARN_EMPTY_BODY = YES; 390 | CLANG_WARN_ENUM_CONVERSION = YES; 391 | CLANG_WARN_INT_CONVERSION = YES; 392 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 393 | CLANG_WARN_UNREACHABLE_CODE = YES; 394 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 395 | COPY_PHASE_STRIP = NO; 396 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 397 | ENABLE_STRICT_OBJC_MSGSEND = YES; 398 | ENABLE_TESTABILITY = NO; 399 | FRAMEWORK_SEARCH_PATHS = ""; 400 | GCC_C_LANGUAGE_STANDARD = gnu99; 401 | GCC_DYNAMIC_NO_PIC = NO; 402 | GCC_NO_COMMON_BLOCKS = YES; 403 | GCC_OPTIMIZATION_LEVEL = 0; 404 | GCC_PREPROCESSOR_DEFINITIONS = ( 405 | "DEBUG=1", 406 | "$(inherited)", 407 | "TEST=1", 408 | ); 409 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 410 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 411 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 412 | GCC_WARN_UNDECLARED_SELECTOR = YES; 413 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 414 | GCC_WARN_UNUSED_FUNCTION = YES; 415 | GCC_WARN_UNUSED_VARIABLE = YES; 416 | INFOPLIST_FILE = CoreDataDandyTests/Info.plist; 417 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 418 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 419 | MTL_ENABLE_DEBUG_INFO = YES; 420 | ONLY_ACTIVE_ARCH = YES; 421 | OTHER_SWIFT_FLAGS = "-D DEBUG"; 422 | PRODUCT_BUNDLE_IDENTIFIER = "fuzz.$(PRODUCT_NAME:rfc1034identifier)"; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | PROVISIONING_PROFILE = ""; 425 | SDKROOT = iphoneos; 426 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 427 | SWIFT_VERSION = 3.0; 428 | }; 429 | name = Debug; 430 | }; 431 | 988F4A6D1B3632EE00E71046 /* Release */ = { 432 | isa = XCBuildConfiguration; 433 | buildSettings = { 434 | ALWAYS_SEARCH_USER_PATHS = NO; 435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 436 | CLANG_CXX_LIBRARY = "libc++"; 437 | CLANG_ENABLE_MODULES = YES; 438 | CLANG_ENABLE_OBJC_ARC = YES; 439 | CLANG_WARN_BOOL_CONVERSION = YES; 440 | CLANG_WARN_CONSTANT_CONVERSION = YES; 441 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 442 | CLANG_WARN_EMPTY_BODY = YES; 443 | CLANG_WARN_ENUM_CONVERSION = YES; 444 | CLANG_WARN_INT_CONVERSION = YES; 445 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 446 | CLANG_WARN_UNREACHABLE_CODE = YES; 447 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 448 | COPY_PHASE_STRIP = NO; 449 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 450 | ENABLE_NS_ASSERTIONS = NO; 451 | ENABLE_STRICT_OBJC_MSGSEND = YES; 452 | ENABLE_TESTABILITY = NO; 453 | FRAMEWORK_SEARCH_PATHS = ""; 454 | GCC_C_LANGUAGE_STANDARD = gnu99; 455 | GCC_NO_COMMON_BLOCKS = YES; 456 | GCC_PREPROCESSOR_DEFINITIONS = "TEST=1"; 457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 459 | GCC_WARN_UNDECLARED_SELECTOR = YES; 460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 461 | GCC_WARN_UNUSED_FUNCTION = YES; 462 | GCC_WARN_UNUSED_VARIABLE = YES; 463 | INFOPLIST_FILE = CoreDataDandyTests/Info.plist; 464 | IPHONEOS_DEPLOYMENT_TARGET = 9.2; 465 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 466 | MTL_ENABLE_DEBUG_INFO = NO; 467 | PRODUCT_BUNDLE_IDENTIFIER = "fuzz.$(PRODUCT_NAME:rfc1034identifier)"; 468 | PRODUCT_NAME = "$(TARGET_NAME)"; 469 | SDKROOT = iphoneos; 470 | SWIFT_VERSION = 3.0; 471 | VALIDATE_PRODUCT = YES; 472 | }; 473 | name = Release; 474 | }; 475 | /* End XCBuildConfiguration section */ 476 | 477 | /* Begin XCConfigurationList section */ 478 | 988F4A5D1B3632DC00E71046 /* Build configuration list for PBXProject "CoreDataDandyTests" */ = { 479 | isa = XCConfigurationList; 480 | buildConfigurations = ( 481 | 988F4A5E1B3632DC00E71046 /* Debug */, 482 | 988F4A5F1B3632DC00E71046 /* Release */, 483 | ); 484 | defaultConfigurationIsVisible = 0; 485 | defaultConfigurationName = Debug; 486 | }; 487 | 988F4A6B1B3632EE00E71046 /* Build configuration list for PBXNativeTarget "CoreDataDandyTests" */ = { 488 | isa = XCConfigurationList; 489 | buildConfigurations = ( 490 | 988F4A6C1B3632EE00E71046 /* Debug */, 491 | 988F4A6D1B3632EE00E71046 /* Release */, 492 | ); 493 | defaultConfigurationIsVisible = 0; 494 | defaultConfigurationName = Debug; 495 | }; 496 | /* End XCConfigurationList section */ 497 | 498 | /* Begin XCVersionGroup section */ 499 | 981C80211DC919BF003EFB04 /* DandyModel.xcdatamodeld */ = { 500 | isa = XCVersionGroup; 501 | children = ( 502 | 981C80221DC919BF003EFB04 /* DandyModel.xcdatamodel */, 503 | ); 504 | currentVersion = 981C80221DC919BF003EFB04 /* DandyModel.xcdatamodel */; 505 | path = DandyModel.xcdatamodeld; 506 | sourceTree = ""; 507 | versionGroupType = wrapper.xcdatamodel; 508 | }; 509 | /* End XCVersionGroup section */ 510 | }; 511 | rootObject = 988F4A5A1B3632DC00E71046 /* Project object */; 512 | } 513 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests.xcodeproj/xcshareddata/xcschemes/CoreDataDandyTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/CoreDataDandyTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoreDataDandyTests.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 6/20/15. 6 | // Copyright © 2015 Fuzz Productions, LLC. All rights reserved. 7 | // 8 | // This code is distributed under the terms and conditions of the MIT license. 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to 12 | // deal in the Software without restriction, including without limitation the 13 | // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 14 | // sell copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in 18 | // all copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 25 | // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 26 | // IN THE SOFTWARE. 27 | 28 | import XCTest 29 | import CoreData 30 | 31 | class CoreDataDandyTests: XCTestCase { 32 | 33 | override func setUp() { 34 | CoreDataDandy.wake("DandyModel") 35 | CoreDataValueConverter.dateFormatter.dateStyle = .long 36 | CoreDataValueConverter.dateFormatter.timeStyle = .short 37 | super.setUp() 38 | } 39 | override func tearDown() { 40 | Dandy.tearDown() 41 | super.tearDown() 42 | } 43 | // MARK: - Initialization and deinitialization - 44 | 45 | /// When the clean up completes, no data should remain. 46 | func testCleanUp() { 47 | Dandy.insert(Dandy_.self) 48 | Dandy.save() 49 | let initialResultsCount = try! Dandy.fetch(Dandy_.self)?.count 50 | Dandy.tearDown() 51 | let finalResultsCount = try! Dandy.fetch(Dandy_.self)?.count 52 | XCTAssert(initialResultsCount == 1 && finalResultsCount == 0, "Pass") 53 | } 54 | 55 | // MARK: - Saves - 56 | 57 | /// After a save, the size of the persistent store should increase 58 | func testSave() { 59 | let expectation = self.expectation(description: "save") 60 | do { 61 | let fileSizeKey = FileAttributeKey("NSFileSize") 62 | let unsavedData = try FileManager.default.attributesOfItem(atPath: PersistentStackCoordinator.persistentStoreURL.path)[fileSizeKey] as! Int 63 | for i in 0...100000 { 64 | let dandy = Dandy.insert(Dandy_.self) 65 | dandy?.setValue("\(i)", forKey: "dandyID") 66 | } 67 | Dandy.save({ (error) in 68 | do { 69 | let savedData = try FileManager.default.attributesOfItem(atPath: PersistentStackCoordinator.persistentStoreURL.path)[fileSizeKey] as! Int 70 | XCTAssert(savedData > unsavedData, "Pass") 71 | expectation.fulfill() 72 | } catch { 73 | XCTAssert(false, "Failure to retrieive file attributes.") 74 | expectation.fulfill() 75 | } 76 | }) 77 | } catch { 78 | XCTAssert(false, "Failure to retrieive file attributes.") 79 | expectation.fulfill() 80 | } 81 | self.waitForExpectations(timeout: 20, handler: { (error) -> Void in }) 82 | } 83 | 84 | // MARK: - Object insertions, deletions, and fetches - 85 | 86 | /// Objects should be insertable. 87 | func testObjectInsertion() { 88 | let dandy = Dandy.insert(Dandy_.self) 89 | XCTAssert(dandy != nil, "Pass") 90 | XCTAssert(try! Dandy.fetch(Dandy_.self)?.count == 1, "Pass") 91 | } 92 | 93 | /// Objects should be insertable in multiples. 94 | func testMultipleObjectInsertion() { 95 | for _ in 0...2 { 96 | Dandy.insert(Dandy_.self) 97 | } 98 | XCTAssert(try! Dandy.fetch(Dandy_.self)?.count == 3, "Pass") 99 | } 100 | 101 | /// Objects marked with the `@unique` primaryKey should not be inserted more than once. 102 | func testUniqueObjectInsertion() { 103 | Dandy.insert(Space.self) 104 | Dandy.insert(Space.self) 105 | XCTAssert(try! Dandy.fetch(Space.self)?.count == 1, "Pass") 106 | } 107 | 108 | /// Passing an invalid entity name should result in warning emission and a nil return 109 | func testInvalidObjectInsertion() { 110 | class ZZZ: NSManagedObject {} 111 | 112 | let object = Dandy.insert(ZZZ.self) 113 | XCTAssert(object == nil, "Pass") 114 | } 115 | 116 | /// After a value has been inserted with a primary key, the next fetch for it should return it and it alone. 117 | func testUniqueObjectMaintenance() { 118 | let dandy = Dandy.insertUnique(Dandy_.self, identifiedBy: "WILDE") 119 | dandy?.setValue("An author, let's say", forKey: "bio") 120 | let repeatedDandy = Dandy.insertUnique(Dandy_.self, identifiedBy: "WILDE") 121 | let dandies = try! Dandy.fetch(Dandy_.self)?.count 122 | XCTAssert(dandies == 1 && (repeatedDandy!.value(forKey: "bio") as! String == "An author, let's say"), "Pass") 123 | } 124 | 125 | /// Objects should be fetchable via typical NSPredicate configured NSFetchRequests. 126 | func testPredicateFetch() { 127 | let wilde = Dandy.insertUnique(Dandy_.self, identifiedBy: "WILDE")! 128 | wilde.setValue("An author, let's say", forKey: "bio") 129 | let byron = Dandy.insertUnique(Dandy_.self, identifiedBy: "BYRON")! 130 | byron.setValue("A poet, let's say", forKey: "bio") 131 | let dandies = try! Dandy.fetch(Dandy_.self)?.count 132 | let byrons = try! Dandy.fetch(Dandy_.self, filterBy: NSPredicate(format: "bio == %@", "A poet, let's say"))?.count 133 | XCTAssert(dandies == 2 && byrons == 1, "Pass") 134 | } 135 | 136 | /// After a fetch for an object with a primaryKey of the wrong type should undergo type conversion and 137 | /// resolve correctly.. 138 | func testPrimaryKeyTypeConversion() { 139 | let dandy = Dandy.insertUnique(Dandy_.self, identifiedBy: 1) 140 | dandy?.setValue("A poet, let's say", forKey: "bio") 141 | let repeatedDandy = Dandy.insertUnique(Dandy_.self, identifiedBy: "1") 142 | let dandies = try! Dandy.fetch(Dandy_.self)?.count 143 | XCTAssert(dandies == 1 && (repeatedDandy!.value(forKey: "bio") as! String == "A poet, let's say"), "Pass") 144 | } 145 | 146 | /// Mistaken use of a primaryKey identifying function for singleton objects should not lead to unexpected 147 | /// behavior. 148 | func testSingletonsIgnorePrimaryKey() { 149 | let space = Dandy.insertUnique(Space.self, identifiedBy: "name") 150 | space?.setValue("The Gogol Empire, let's say", forKey: "name") 151 | let repeatedSpace = Dandy.insertUnique(Space.self, identifiedBy: "void") 152 | let spaces = try! Dandy.fetch(Space.self)?.count 153 | XCTAssert(spaces == 1 && (repeatedSpace!.value(forKey: "name") as! String == "The Gogol Empire, let's say"), "Pass") 154 | } 155 | 156 | /// The convenience function for fetching objects by primary key should return a unique object that has been inserted. 157 | func testUniqueObjectFetch() { 158 | let dandy = Dandy.insertUnique(Dandy_.self, identifiedBy: "WILDE") 159 | dandy?.setValue("An author, let's say", forKey: "bio") 160 | let fetchedDandy = Dandy.fetchUnique(Dandy_.self, identifiedBy: "WILDE")! 161 | XCTAssert((fetchedDandy.value(forKey: "bio") as! String == "An author, let's say"), "Pass") 162 | } 163 | 164 | /// If a primary key is not specified for an object, the fetch should fail and emit a warning. 165 | func testUnspecifiedPrimaryKeyValueUniqueObjectFetch() { 166 | let plebian = Dandy.insertUnique(Plebian.self, identifiedBy: "plebianID") 167 | XCTAssert(plebian == nil, "Pass") 168 | } 169 | 170 | /// A deleted object should not be represented in the database 171 | func testObjectDeletion() { 172 | let space = Dandy.insertUnique(Space.self, identifiedBy: "name") 173 | let previousSpaceCount = try! Dandy.fetch(Space.self)?.count 174 | let expectation = self.expectation(description: "Object deletion") 175 | Dandy.delete(space!) { 176 | let newSpaceCount = try! Dandy.fetch(Space.self)?.count 177 | XCTAssert(previousSpaceCount == 1 && newSpaceCount == 0, "Pass") 178 | expectation.fulfill() 179 | } 180 | self.waitForExpectations(timeout: 0.5, handler: { (error) -> Void in }) 181 | } 182 | 183 | // MARK: - Persistent Stack - 184 | 185 | /// The managed object model associated with the stack coordinator should be consistent with DandyModel.xcdatamodel. 186 | func testPersistentStackManagerObjectModelConstruction() { 187 | let persistentStackCoordinator = PersistentStackCoordinator(managedObjectModelName: "DandyModel") 188 | XCTAssert(persistentStackCoordinator.managedObjectModel.entities.count == 9, "Pass") 189 | } 190 | 191 | /// The parentContext of the mainContext should be the privateContext. Changes to the structure of 192 | /// Dandy's persistent stack will be revealed with this test. 193 | func testPersistentStackManagerObjectContextConstruction() { 194 | let persistentStackCoordinator = PersistentStackCoordinator(managedObjectModelName: "DandyModel") 195 | XCTAssert(persistentStackCoordinator.mainContext.parent === persistentStackCoordinator.privateContext, "Pass") 196 | } 197 | 198 | /// The privateContext should share a reference to the `DandyStackCoordinator's` persistentStoreCoordinator. 199 | func testPersistentStoreCoordinatorConnection() { 200 | let persistentStackCoordinator = PersistentStackCoordinator(managedObjectModelName: "DandyModel") 201 | persistentStackCoordinator.connectPrivateContextToPersistentStoreCoordinator() 202 | XCTAssert(persistentStackCoordinator.privateContext.persistentStoreCoordinator! === persistentStackCoordinator.persistentStoreCoordinator, "Pass") 203 | } 204 | /// Resetting `DandyStackCoordinator's` should remove pre-existing persistent stores and create a new one. 205 | func testPersistentStoreReset() { 206 | let persistentStackCoordinator = PersistentStackCoordinator(managedObjectModelName: "DandyModel") 207 | let oldPersistentStore = persistentStackCoordinator.persistentStoreCoordinator.persistentStores.first! 208 | persistentStackCoordinator.resetPersistentStore() 209 | let newPersistentStore = persistentStackCoordinator.persistentStoreCoordinator.persistentStores.first! 210 | XCTAssert((newPersistentStore !== oldPersistentStore), "Pass") 211 | } 212 | 213 | /// When initialization completes, the completion closure should execute. 214 | func testPersistentStackManagerConnectionClosureExecution() { 215 | let expectation = self.expectation(description: "initialization") 216 | let persistentStackCoordinator = PersistentStackCoordinator(managedObjectModelName: "DandyModel", persistentStoreConnectionCompletion: { 217 | XCTAssert(true, "Pass.") 218 | expectation.fulfill() 219 | }) 220 | persistentStackCoordinator.connectPrivateContextToPersistentStoreCoordinator() 221 | self.waitForExpectations(timeout: 5, handler: { (error) -> Void in }) 222 | } 223 | 224 | // MARK: - Value conversions - 225 | 226 | /// Conversions to undefined types should not occur 227 | func testUndefinedTypeConversion() { 228 | let result: Any? = CoreDataValueConverter.convert("For us, life is five minutes of introspection", to: .undefinedAttributeType) 229 | XCTAssert(result == nil, "Pass") 230 | } 231 | 232 | /// Test non-conforming protocol type conversion 233 | func testNonConformingProtocolTypeConversion() { 234 | let result: Any? = CoreDataValueConverter.convert(["life", "is", "five", "minutes", "of", "introspection"], to: .stringAttributeType) 235 | XCTAssert(result == nil, "Pass") 236 | } 237 | 238 | /// A type converts to the same type should undergo no changes 239 | func testSameTypeConversion() { 240 | let string: Any? = CoreDataValueConverter.convert("For us, life is five minutes of introspection", to: .stringAttributeType) 241 | XCTAssert(string is String, "Pass") 242 | 243 | let integer: Any? = CoreDataValueConverter.convert(Int(1), to: .integer64AttributeType) 244 | XCTAssert(integer is Int, "Pass") 245 | 246 | let float: Any? = CoreDataValueConverter.convert(Float(1), to: .floatAttributeType) 247 | XCTAssert(float is Float, "Pass") 248 | 249 | let double: Any? = CoreDataValueConverter.convert(Double(1), to: .doubleAttributeType) 250 | XCTAssert(double is Double, "Pass") 251 | 252 | let date: Any? = CoreDataValueConverter.convert(Date(), to: .dateAttributeType) 253 | XCTAssert(date is Date, "Pass") 254 | 255 | let encodedString = "suave".data(using: String.Encoding.utf8, allowLossyConversion: true) 256 | let data: Any? = CoreDataValueConverter.convert(encodedString!, to: .binaryDataAttributeType) 257 | XCTAssert(data is Data, "Pass") 258 | } 259 | 260 | /// NSData objects should be convertible to Strings. 261 | func testDataToStringConversion() { 262 | let expectation: NSString = "testing string" 263 | let input: Data? = expectation.data(using: String.Encoding.utf8.rawValue) 264 | let result = CoreDataValueConverter.convert(input!, to: .stringAttributeType) as? NSString 265 | XCTAssert(result == expectation, "") 266 | } 267 | 268 | /// Numbers should be convertible to Strings. 269 | func testNumberToStringConversion() { 270 | let input = 123455 271 | let result = CoreDataValueConverter.convert(input, to: .stringAttributeType) as? String 272 | XCTAssert(result == "123455", "") 273 | } 274 | 275 | /// Numbers should be convertible to NSDecimalNumbers 276 | func testNumberToDecimalConversion() { 277 | let integer = Int(7) 278 | var result = CoreDataValueConverter.convert(integer, to: .decimalAttributeType) as? NSDecimalNumber 279 | XCTAssert(result == NSDecimalNumber(value: integer), "Pass") 280 | 281 | let float = Float(7.070000171661375488) 282 | result = CoreDataValueConverter.convert(float, to: .decimalAttributeType) as? NSDecimalNumber 283 | XCTAssert(result == NSDecimalNumber(value: float), "Pass") 284 | 285 | let double = Double(7.070000171661375488) 286 | result = CoreDataValueConverter.convert(double, to: .decimalAttributeType) as? NSDecimalNumber 287 | XCTAssert(result == NSDecimalNumber(value: double), "Pass") 288 | } 289 | 290 | /// Numbers should be convertible to Doubles 291 | func testNumberToDoubleConversion() { 292 | let integer = Int(7) 293 | var result = CoreDataValueConverter.convert(integer, to: .doubleAttributeType) as? Double 294 | XCTAssert(result == Double(integer), "Pass") 295 | 296 | let float = Float(7) 297 | result = CoreDataValueConverter.convert(float, to: .doubleAttributeType) as? Double 298 | XCTAssert(result == Double(float), "Pass") 299 | } 300 | 301 | /// Numbers should be convertible to Floats 302 | func testNumberToFloatConversion() { 303 | let integer = Int(7) 304 | var result = CoreDataValueConverter.convert(integer, to: .floatAttributeType) as? Float 305 | XCTAssert(result == Float(integer), "Pass") 306 | 307 | let double = Double(7.07) 308 | result = CoreDataValueConverter.convert(double, to: .floatAttributeType) as? Float 309 | XCTAssert(result == Float(double), "Pass") 310 | } 311 | 312 | /// Numbers should be convertible to NSData 313 | func testNumberToDataConversion() { 314 | let input = 7 315 | 316 | var integer = Int(input) 317 | var result = CoreDataValueConverter.convert(integer, to: .binaryDataAttributeType) as? Data 318 | XCTAssert(result == Data(bytes: &integer, count: MemoryLayout.size), "Pass") 319 | 320 | var float = Float(input) 321 | result = CoreDataValueConverter.convert(float, to: .binaryDataAttributeType) as? Data 322 | XCTAssert(result == Data(bytes: &float, count: MemoryLayout.size), "Pass") 323 | 324 | var double = Double(input) 325 | result = CoreDataValueConverter.convert(double, to: .binaryDataAttributeType) as? Data 326 | XCTAssert(result == Data(bytes: &double, count: MemoryLayout.size), "Pass") 327 | 328 | } 329 | 330 | /// Numbers should be convertible to NSDates. 331 | func testNumberToDateConversion() { 332 | let now = Date() 333 | let expectation = Double(now.timeIntervalSince1970) 334 | let result = CoreDataValueConverter.convert(expectation, to: .dateAttributeType) as? Date 335 | let resultAsDouble = Double(result!.timeIntervalSince1970) 336 | XCTAssert(resultAsDouble == expectation, "") 337 | } 338 | 339 | /// Numbers should be convertible to Booleans. 340 | func testNumberToBooleanConversion() { 341 | var input = -1 342 | var result = CoreDataValueConverter.convert(input, to: .booleanAttributeType) as? NSNumber 343 | XCTAssert(result?.boolValue == nil, "") 344 | 345 | input = 1 346 | result = CoreDataValueConverter.convert(input, to: .booleanAttributeType) as? NSNumber 347 | XCTAssert(result?.boolValue == true, "") 348 | 349 | input = 0 350 | result = CoreDataValueConverter.convert(input, to: .booleanAttributeType) as? NSNumber 351 | XCTAssert(result?.boolValue == false, "") 352 | 353 | input = 99 354 | result = CoreDataValueConverter.convert(input, to: .booleanAttributeType) as? NSNumber 355 | XCTAssert(result == true, "Pass") 356 | } 357 | 358 | /// NSDates should be convertible to Strings. 359 | func testDateToStringConversion() { 360 | let now = Date() 361 | let expectation = CoreDataValueConverter.dateFormatter.string(from: now) 362 | let result = CoreDataValueConverter.convert(now, to: .stringAttributeType) as? String 363 | XCTAssert(result == expectation, "") 364 | } 365 | 366 | /// NSDates should be convertible to Decimals. 367 | func testDateToDecimalConversion() { 368 | let now = Date() 369 | let expectation = NSDecimalNumber(value: now.timeIntervalSince(Date(timeIntervalSince1970: 0)) as Double) 370 | let result = CoreDataValueConverter.convert(now, to: .decimalAttributeType) as! NSDecimalNumber 371 | XCTAssert(result.floatValue - expectation.floatValue < 5, "") 372 | } 373 | 374 | /// NSDates should be convertible to Doubles. 375 | func testDateToDoubleConversion() { 376 | let now = Date() 377 | let expectation = NSNumber(value: now.timeIntervalSince(Date(timeIntervalSince1970: 0)) as Double) 378 | let result = CoreDataValueConverter.convert(now, to: .doubleAttributeType) as! NSNumber 379 | XCTAssert(result.floatValue - expectation.floatValue < 5, "") 380 | } 381 | 382 | /// NSDates should be convertible to Floats. 383 | func testDateToFloatConversion() { 384 | let now = Date() 385 | let expectation = NSNumber(value: Float(now.timeIntervalSince(Date(timeIntervalSince1970: 0))) as Float) 386 | let result = CoreDataValueConverter.convert(now, to: .floatAttributeType) as! NSNumber 387 | XCTAssert(result.floatValue - expectation.floatValue < 5, "") 388 | } 389 | 390 | /// NSDates should be convertible to Ints. 391 | func testDateToIntConversion() { 392 | let now = Date() 393 | let expectation = NSNumber(value: Int(now.timeIntervalSince(Date(timeIntervalSince1970: 0))) as Int) 394 | let result = CoreDataValueConverter.convert(now, to: .integer32AttributeType) as! NSNumber 395 | XCTAssert(result.floatValue - expectation.floatValue < 5, "") 396 | } 397 | 398 | /// A variety of strings should be convertible to Booleans. 399 | func testStringToBooleanConversion() { 400 | var testString = "Yes" 401 | var result: NSNumber? 402 | 403 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 404 | XCTAssert(result?.boolValue == true, "") 405 | 406 | testString = "trUe" 407 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 408 | XCTAssert(result?.boolValue == true, "") 409 | 410 | testString = "1" 411 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 412 | XCTAssert(result?.boolValue == true, "") 413 | 414 | testString = "NO" 415 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 416 | XCTAssert(result?.boolValue == false, "") 417 | 418 | testString = "false" 419 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 420 | XCTAssert(result?.boolValue == false, "") 421 | 422 | testString = "0" 423 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 424 | XCTAssert(result?.boolValue == false, "") 425 | 426 | 427 | testString = "undefined" 428 | result = CoreDataValueConverter.convert(testString, to: .booleanAttributeType) as? NSNumber 429 | XCTAssert(result == nil, "") 430 | } 431 | 432 | /// Strings should be convertible to Integers. 433 | func testStringToIntConversion() { 434 | var input = "123" 435 | var result = CoreDataValueConverter.convert(input, to: .integer64AttributeType) as? Int 436 | XCTAssert(result == 123, "") 437 | 438 | input = "not an int" 439 | result = CoreDataValueConverter.convert(input, to: .integer64AttributeType) as? Int 440 | XCTAssert(result == nil, "") 441 | } 442 | 443 | /// NSStrings should be convertible to NSDecimalNumbers 444 | func testStringToDecimalConversion() { 445 | let expectation = NSDecimalNumber(value: 7.070000171661375488 as Float) 446 | let result = CoreDataValueConverter.convert("7.070000171661375488", to: .decimalAttributeType) as? NSDecimalNumber 447 | XCTAssert(result == expectation, "Pass") 448 | } 449 | 450 | /// NSStrings should be convertible to Doubles 451 | func testStringToDoubleConversion() { 452 | let expectation = Double(7.07) 453 | let result = CoreDataValueConverter.convert("7.07", to: .doubleAttributeType) as? Double 454 | XCTAssert(result == expectation, "Pass") 455 | } 456 | 457 | /// NSStrings should be convertible to Floats 458 | func testStringToFloatConversion() { 459 | let expectation = Float(7.07) 460 | let result = CoreDataValueConverter.convert("7.07", to: .floatAttributeType) as? Float 461 | XCTAssert(result == expectation, "Pass") 462 | } 463 | 464 | /// Strings should be convertible to Data objects. 465 | func testStringToDataConversion() { 466 | let input = "Long long Time ago" 467 | let expectedResult = input.data(using: String.Encoding.utf8)! 468 | let result = CoreDataValueConverter.convert(input, to: .binaryDataAttributeType) as? Data 469 | XCTAssert((result! == expectedResult) == true, "") 470 | } 471 | 472 | /// Strings should be convertible to NSDates. 473 | func testStringToDateConversion() { 474 | let now = Date() 475 | let nowAsString = CoreDataValueConverter.dateFormatter.string(from: now) 476 | let result = CoreDataValueConverter.convert(nowAsString, to: .dateAttributeType) as? Date 477 | let resultAsString = CoreDataValueConverter.dateFormatter.string(from: result!) 478 | XCTAssert(resultAsString == nowAsString, "") 479 | } 480 | 481 | // MARK: - Mapping - 482 | 483 | /// NSEntityDescriptions should be retrievable by name. 484 | func testEntityDescriptionFromString() { 485 | let expected = NSEntityDescription.entity(forEntityName: "Dandy_", in: Dandy.coordinator.mainContext) 486 | let result = NSEntityDescription.forEntity("Dandy_")! 487 | XCTAssert(expected == result, "Pass") 488 | } 489 | 490 | /// NSEntityDescriptions should be retrievable their model's underlying type. 491 | func testEntityDescriptionFromType() { 492 | let expected = NSEntityDescription.entity(forEntityName: "Dandy_", in: Dandy.coordinator.mainContext) 493 | let result = NSEntityDescription.forType(Dandy_.self)! 494 | XCTAssert(expected == result, "Pass") 495 | } 496 | 497 | /// NSEntityDescriptions should correctly report whether they represent unique models or not. 498 | func testUniquenessIdentification() { 499 | let dandy = NSEntityDescription.forType(Dandy_.self)! 500 | let plebian = NSEntityDescription.forType(Plebian.self)! 501 | XCTAssert(dandy.isUnique == true && plebian.isUnique == false, 502 | "Failed to correctly identify whether an NSEntityDescription describes a unique model.") 503 | } 504 | 505 | /// NSEntityDescriptions should correctly report the property that makes an object unique. 506 | func testPrimaryKeyIdentification() { 507 | let expected = "dandyID" 508 | let dandy = NSEntityDescription.forType(Dandy_.self)! 509 | let result = dandy.primaryKey! 510 | XCTAssert(expected == result, "Pass") 511 | } 512 | 513 | /// Primary keys should be inheritable. In this case, Flattery's primaryKey should be inherited from 514 | /// its parent, Gossip. 515 | func testPrimaryKeyInheritance() { 516 | let flattery = Dandy.insert(Flattery.self)! 517 | XCTAssert(flattery.entity.primaryKey == "secret", "Pass") 518 | } 519 | 520 | /// Children should override the primaryKey of their parents. In this case, Slander's uniqueConstraint should 521 | /// override its parent's, Gossip. 522 | func testPrimaryKeyOverride() { 523 | let slander = Dandy.insert(Slander.self)! 524 | XCTAssert(slander.entity.primaryKey == "statement", "Pass") 525 | } 526 | 527 | /// Children's userInfo should contain the userInfo of their parents. In this case, Slander's userInfo should 528 | /// contain a value from its parent, Gossip. 529 | func testUserInfoHierarchyCollection() { 530 | let slanderDescription = NSEntityDescription.forType(Slander.self)! 531 | let userInfo = slanderDescription.allUserInfo! 532 | XCTAssert((userInfo["testValue"] as! String) == "testKey", "Pass") 533 | } 534 | 535 | /// Children should override userInfo of their parents. In this case, Slander's mapping decoration should 536 | /// override its parent's, Gossip. 537 | func testUserInfoOverride() { 538 | let slanderDescription = NSEntityDescription.forType(Slander.self)! 539 | let userInfo = slanderDescription.allUserInfo! 540 | XCTAssert((userInfo[PRIMARY_KEY] as! String) == "statement", "Pass") 541 | } 542 | 543 | /// Entity descriptions with no specified mapping should read into mapping dictionaries with all "same name" mapping 544 | func testSameNameMap() { 545 | let entity = NSEntityDescription.forType(Material.self)! 546 | let expectedMap = [ 547 | "name": PropertyDescription(description: entity.allAttributes!["name"]!), 548 | "origin": PropertyDescription(description: entity.allAttributes!["origin"]!), 549 | "hats": PropertyDescription(description: entity.allRelationships!["hats"]!) 550 | ] 551 | let result = EntityMapper.map(entity) 552 | XCTAssert(result! == expectedMap, "Pass") 553 | } 554 | 555 | /// @mapping: @false should result in an exclusion from the map. Gossip's "secret" attribute has been specified 556 | /// as such. 557 | func testNOMappingKeywordResponse() { 558 | let entity = NSEntityDescription.forType(Gossip.self)! 559 | let expectedMap = [ 560 | "details": PropertyDescription(description: entity.allAttributes!["details"]!), 561 | "topic": PropertyDescription(description: entity.allAttributes!["topic"]!), 562 | // "secret": "unmapped" 563 | "purveyor": PropertyDescription(description: entity.allRelationships!["purveyor"]!) 564 | ] 565 | let result = EntityMapper.map(entity)! 566 | XCTAssert(result == expectedMap, "Pass") 567 | } 568 | 569 | /// If an alternate keypath is specified, that keypath should appear as a key in the map. Space's "spaceState" has been specified 570 | /// to map from "state." 571 | func testAlternateKeypathMappingResponse() { 572 | let entity = NSEntityDescription.forType(Space.self)! 573 | let expectedMap = [ 574 | "name": PropertyDescription(description: entity.allAttributes!["name"]!), 575 | "state": PropertyDescription(description: entity.allAttributes!["spaceState"]!) 576 | ] 577 | let result = EntityMapper.map(entity)! 578 | XCTAssert(result == expectedMap, "Pass") 579 | } 580 | 581 | // MARK: - Property description comparisons and caching - 582 | 583 | /// Comparisons of property descriptions should evaluate correctly. 584 | func testPropertyDescriptionComparison() { 585 | let entity = NSEntityDescription.forType(Space.self)! 586 | let name = PropertyDescription(description: entity.allAttributes!["name"]!) 587 | let secondName = PropertyDescription(description: entity.allAttributes!["name"]!) 588 | let state = PropertyDescription(description: entity.allAttributes!["spaceState"]!) 589 | XCTAssert(name == secondName, "Pass") 590 | XCTAssert(name != state, "Pass") 591 | } 592 | 593 | /// Simple initializations and initializations from NSCoding should yield valid objects. 594 | func testPropertyDescriptionInitialization() { 595 | let _ = PropertyDescription() 596 | 597 | let entity = NSEntityDescription.forType(Space.self)! 598 | let propertyDescription = PropertyDescription(description: entity.allAttributes!["name"]!) 599 | let pathArray = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true) 600 | let documentPath = pathArray.first! 601 | let archivePath = NSString(string: documentPath).appendingPathComponent("SPACE_NAME") 602 | NSKeyedArchiver.archiveRootObject(propertyDescription, toFile: archivePath) 603 | let unarchivedPropertyDescription = NSKeyedUnarchiver.unarchiveObject(withFile: archivePath) as! PropertyDescription 604 | XCTAssert(unarchivedPropertyDescription == propertyDescription, "Pass") 605 | } 606 | 607 | // MARK: - Map caching - 608 | 609 | /// The first access to an entity's map should result in that map's caching 610 | func testMapCaching() { 611 | if let entityDescription = NSEntityDescription.forType(Material.self) { 612 | EntityMapper.map(entityDescription) 613 | let entityCacheMap = EntityMapper.cachedEntityMap[String(describing: Material.self)]! 614 | XCTAssert(entityCacheMap.count > 0, "") 615 | } 616 | } 617 | 618 | /// When clean up is called, no cached maps should remain 619 | func testMapCacheCleanUp() { 620 | if let entityDescription = NSEntityDescription.forType(Material.self) { 621 | EntityMapper.map(entityDescription) 622 | let initialCacheCount = EntityMapper.cachedEntityMap.count 623 | EntityMapper.clearCache() 624 | let finalCacheCount = EntityMapper.cachedEntityMap.count 625 | XCTAssert(initialCacheCount == 1 && finalCacheCount == 0, "Pass") 626 | } 627 | } 628 | 629 | /// The creation of a new map should be performant 630 | func testPerformanceOfNewMapCreation() { 631 | self.measure { 632 | let entityDescription = NSEntityDescription.forType(Material.self) 633 | EntityMapper.map(entityDescription!) 634 | } 635 | } 636 | 637 | /// The fetching of a cached map should be performant, and more performant than the creation of a new map 638 | func testPerformanceOfCachedMapRetrieval() { 639 | let entityDescription = NSEntityDescription.forType(Material.self)! 640 | EntityMapper.map(entityDescription) 641 | self.measure { 642 | EntityMapper.map(entityDescription) 643 | } 644 | } 645 | // MARK: - Object building - 646 | 647 | /// Values should be mapped from json to an object's attributes. 648 | func testAttributeBuilding() { 649 | let space = Dandy.insert(Space.self)! 650 | let json: JSONObject = ["name": "nebulous", "state": "moderately cool"] 651 | ObjectFactory.build(space, from: json) 652 | XCTAssert(space.name == "nebulous" 653 | && space.spaceState == "moderately cool", 654 | "Pass") 655 | } 656 | 657 | /// Values should be mapped from json an object's relationships. 658 | func testRelationshipBuilding() { 659 | let gossip = Dandy.insert(Gossip.self)! 660 | let json = [ 661 | "details": "At Bo Peep, unusually cool towards Isabella Brown.", 662 | "topic": "John Keats", 663 | "purveyor": [ 664 | "id": 1, 665 | "name": "Lord Byron", 666 | ] 667 | ] as JSONObject 668 | ObjectFactory.build(gossip, from: json) 669 | 670 | let byron = gossip.purveyor! 671 | XCTAssert(gossip.details == "At Bo Peep, unusually cool towards Isabella Brown." 672 | && gossip.topic == "John Keats" 673 | && byron.dandyID == "1" 674 | && byron.name == "Lord Byron", 675 | "Pass") 676 | } 677 | 678 | /// Values should be recursively mapped from nested json objects. 679 | func testRecursiveObjectBuilding() { 680 | let gossip = Dandy.insert(Gossip.self)! 681 | let json = [ 682 | "details": "At Bo Peep, unusually cool towards Isabella Brown.", 683 | "topic": "John Keats", 684 | "purveyor": [ 685 | "id": "1", 686 | "name": "Lord Byron", 687 | "hats": [[ 688 | "name": "bowler", 689 | "style": "billycock", 690 | "material": [ 691 | "name": "felt", 692 | "origin": "Rome" 693 | ] 694 | ]] 695 | ] 696 | ] as JSONObject 697 | ObjectFactory.build(gossip, from: json) 698 | let byron = gossip.purveyor! 699 | let bowler = byron.hats!.anyObject() as! Hat 700 | let felt = bowler.primaryMaterial! 701 | XCTAssert(gossip.details == "At Bo Peep, unusually cool towards Isabella Brown." 702 | && gossip.topic == "John Keats" 703 | && byron.dandyID == "1" 704 | && byron.name == "Lord Byron" 705 | && bowler.name == "bowler" 706 | && bowler.styleDescription == "billycock" 707 | && felt.name == "felt" 708 | && felt.origin == "Rome", 709 | "Pass") 710 | } 711 | 712 | /// @mapping values that contain a keypath should allow access to json values via a keypath 713 | func testKeyPathBuilding() { 714 | let dandy = Dandy.insert(Dandy_.self)! 715 | let json = [ 716 | "id": "BAUD", 717 | "relatedDandies": [ 718 | "predecessor": [ 719 | "id": "BALZ", 720 | "name": "Honoré de Balzac" 721 | ] 722 | ] 723 | ] as JSONObject 724 | ObjectFactory.build(dandy, from: json) 725 | let balzac = dandy.predecessor! 726 | XCTAssert(balzac.dandyID == "BALZ" 727 | && balzac.name == "Honoré de Balzac" 728 | && balzac.successor!.dandyID == dandy.dandyID, 729 | "Pass") 730 | } 731 | 732 | /// Property values on an object should not be overwritten if no new values are specified. 733 | func testIgnoreUnkeyedAttributesWhenBuilding() { 734 | let space = Dandy.insert(Space.self)! 735 | space.setValue("exceptionally relaxed", forKey: "spaceState") 736 | let json: JSONObject = ["name": "nebulous" as AnyObject] 737 | ObjectFactory.build(space, from: json) 738 | XCTAssert(space.value(forKey: "spaceState") as! String == "exceptionally relaxed", "Pass") 739 | } 740 | 741 | /// Property values on an object should be overwritten if new values are specified. 742 | func testOverwritesKeyedAttributesWhenBuilding() { 743 | let space = Dandy.insert(Space.self)! 744 | space.setValue("exceptionally relaxed", forKey: "spaceState") 745 | let json: JSONObject = ["state": "significant excitement"] 746 | ObjectFactory.build(space, from: json) 747 | XCTAssert(space.value(forKey: "spaceState") as! String == "significant excitement", "Pass") 748 | } 749 | 750 | /// If a single json object is passed when attempting to build a toMany relationship, it should be 751 | /// rejected. 752 | func testSingleObjectToManyRelationshipRejection() { 753 | let dandy = Dandy.insert(Dandy_.self)! 754 | let json = [ 755 | "name": "bowler", 756 | "style": "billycock", 757 | "material": [ 758 | "name": "felt", 759 | "origin": "Rome" 760 | ] 761 | ] as JSONObject 762 | ObjectFactory.make(PropertyDescription(description: dandy.entity.allRelationships!["hats"]!), to: dandy, from: json) 763 | XCTAssert((dandy.value(forKey: "hats") as! NSSet).count == 0, "Pass") 764 | } 765 | 766 | /// Uniqueness should play no role in whether an object can be made or not. 767 | func testNonUniqueObjectMaking() { 768 | let json: JSONObject = ["name": "Passerby" as AnyObject] 769 | let plebian = ObjectFactory.make(Plebian.self, from: json) 770 | XCTAssert(plebian != nil, "Test failed: a non-unique object could not be made.") 771 | } 772 | 773 | /// If a json array is passed when attempting to build a toOne relationship, it should be 774 | /// rejected. 775 | func testArrayOfObjectToOneRelationshipRejection() { 776 | let gossip = Dandy.insert(Gossip.self)! 777 | let json = [ 778 | [ 779 | "id": "1", 780 | "name": "Lord Byron"], 781 | [ 782 | "id": "2", 783 | "name": "Oscar Wilde"], 784 | [ 785 | "id": "3", 786 | "name": "Andre 3000"] 787 | ] 788 | ObjectFactory.make(PropertyDescription(description: gossip.entity.allRelationships!["purveyor"]!), to: gossip, from: json) 789 | XCTAssert(gossip.value(forKey: "purveyor") == nil, "Pass") 790 | } 791 | 792 | /// NSOrderedSets should be created for ordered relationships. NSSets should be created for 793 | /// unordered relationships. 794 | func testOrderedRelationshipsBuilding() { 795 | let hat = Dandy.insert(Hat.self)! 796 | let json = [ 797 | [ 798 | "id": "1", 799 | "name": "Lord Byron"], 800 | [ 801 | "id": "2", 802 | "name": "Oscar Wilde"], 803 | [ 804 | "id": "3", 805 | "name": "Andre 3000"] 806 | ] 807 | ObjectFactory.make(PropertyDescription(description: hat.entity.allRelationships!["dandies"]!), to: hat, from: json) 808 | XCTAssert(hat.value(forKey: "dandies") is NSOrderedSet && hat.dandies!.count == 3, "Pass") 809 | } 810 | 811 | /// Nil or inconvertible json values should lead to nilled out relationships. 812 | func testRelationshipNilling() { 813 | let dandy = Dandy.insert(Dandy_.self)! 814 | let json = [ 815 | "id": 1, 816 | "name": "Lord Byron", 817 | "hats": [ 818 | [ 819 | "name": "bowler", 820 | "style": "billycock", 821 | "material": [ 822 | "name": "felt", 823 | "origin": "Rome" 824 | ] 825 | ] 826 | ] 827 | ] as JSONObject 828 | 829 | let nullNilling = [ 830 | "id": 1, 831 | "hats": "NULL" 832 | ] as JSONObject 833 | 834 | ObjectFactory.build(dandy, from: json) 835 | XCTAssert(dandy.hats!.count == 1, "After building from \(json), Lord Byron should have a single hat.") 836 | ObjectFactory.build(dandy, from: nullNilling) 837 | XCTAssert(dandy.hats?.count == 0, "After building hats from NULL values, Lord Byron should have no hats.") 838 | 839 | let nsNullNilling = [ 840 | "id": 1, 841 | "hats": NSNull() 842 | ] as JSONObject 843 | 844 | ObjectFactory.build(dandy, from: json) 845 | XCTAssert(dandy.hats!.count == 1, "After building from \(json), Lord Byron should have a single hat.") 846 | ObjectFactory.build(dandy, from: nsNullNilling) 847 | XCTAssert(dandy.hats?.count == 0, "After building hats from NULL values, Lord Byron should have no hats.") 848 | 849 | XCTAssert((dandy.value(forKey: "hats") as! NSSet).count == 0, "Pass") 850 | } 851 | 852 | // MARK: - Object factory via CoreDataDandy - 853 | 854 | /// json containing a valid primary key should result in unique, mapped objects. 855 | func testSimpleObjectConstructionFromJSON() { 856 | let json = ["name": "Passerby"] 857 | let plebian = Dandy.upsert(Plebian.self, from: json)! 858 | XCTAssert(plebian.value(forKey: "name") as! String == "Passerby") 859 | } 860 | 861 | /// json lacking a primary key should be rejected. A nil value should be returned and a warning 862 | /// emitted. 863 | func testUniqueObjectConstructionFromJSON() { 864 | let json = ["name": "Lord Byron"] 865 | let byron = Dandy.upsert(Dandy_.self, from: json) 866 | XCTAssert(byron == nil, "Pass") 867 | } 868 | 869 | /// json lacking a primary key should be rejected. A nil value should be returned and a warning 870 | /// emitted. 871 | func testRejectionOfJSONWithoutPrimaryKeyForUniqueObject() { 872 | let json = ["name": "Lord Byron"] 873 | let byron = Dandy.upsert(Dandy_.self, from: json) 874 | XCTAssert(byron == nil, "Pass") 875 | } 876 | 877 | /// An array of objects should be returned from a json array containing mappable objects. 878 | func testObjectArrayConstruction() { 879 | var json = [JSONObject]() 880 | for i in 0...9 { 881 | json.append(["id": String(i), "name": "Morty"]) 882 | } 883 | let dandies = Dandy.batchUpsert(Dandy_.self, from: json)! 884 | let countIsCorrect = dandies.count == 10 885 | var dandiesAreCorrect = true 886 | for i in 0...9 { 887 | let matchingDandies = (dandies.filter {$0.value(forKey: "dandyID")! as! String == String(i)}) 888 | if matchingDandies.count != 1 { 889 | dandiesAreCorrect = false 890 | break 891 | } 892 | } 893 | XCTAssert(countIsCorrect && dandiesAreCorrect, "Pass") 894 | } 895 | 896 | /// Objects that adopt `MappingFinalizer` should invoke `finalizeMappingForJSON(_:)` at the conclusion of its 897 | /// construction. 898 | /// 899 | /// Gossip's map appends "_FINALIZE" to its content. 900 | func testMappingFinalization() { 901 | let input = "A decisively excellent affair, if a bit tawdry." 902 | let expected = "\(input)_FINALIZED" 903 | let json: JSONObject = [ 904 | "id": "1" as AnyObject, 905 | "content": input as AnyObject 906 | ] 907 | let conclusion = ObjectFactory.make(Conclusion.self, from: json)! 908 | XCTAssert(conclusion.content == expected, "Pass") 909 | } 910 | 911 | // MARK: - Serialization tests - 912 | 913 | /// An object's attributes should be serializable into json. 914 | func testAttributeSerialization() { 915 | let hat = Dandy.insert(Hat.self)! 916 | hat.setValue("bowler", forKey: "name") 917 | hat.setValue("billycock", forKey: "styleDescription") 918 | let expected = [ 919 | "name": "bowler", 920 | "style": "billycock" 921 | ] 922 | let result = Serializer.serialize(hat) as! [String: String] 923 | XCTAssert(result == expected, "Pass") 924 | } 925 | 926 | /// Test nil attribute exclusion from serialized json. 927 | func testNilAttributeSerializationExclusion() { 928 | let hat = Dandy.insert(Hat.self)! 929 | hat.setValue("bowler", forKey: "name") 930 | hat.setValue(nil, forKey: "styleDescription") 931 | let expected = ["name": "bowler"] 932 | let result = Serializer.serialize(hat) as! [String: String] 933 | XCTAssert(result == expected, "Pass") 934 | } 935 | 936 | /// Relationships targeted for serialization should not be mapped to a helper array unless thay are nested. 937 | func testNestedRelationshipSerializationExclusion() { 938 | let relationships = ["hats", "gossip", "predecessor"] 939 | let result = Serializer.nestedSerializationTargets(for: "hats", including: relationships) 940 | XCTAssert(result == nil, "\(result) should have been nil: no nested keypaths were included in \(relationships).") 941 | } 942 | 943 | /// Nested relationships targeted for serialization should be correctly mapped to a helper array. 944 | func testNestedRelationshipSerializationTargeting() { 945 | let relationships = ["purveyor.successor", "purveyor.hats.material", "anomaly"] 946 | let expected = ["successor", "hats.material"] 947 | let result = Serializer.nestedSerializationTargets(for: "purveyor", including: relationships)! 948 | XCTAssert(result == expected, "Pass") 949 | } 950 | 951 | /// Unspecified relationships should return no result. 952 | func testNoMatchingRelationshipsSerializationTargeting() { 953 | let relationships = ["purveyor.successor", "purveyor.hats.material"] 954 | let result = Serializer.nestedSerializationTargets(for: "anomaly", including: relationships) 955 | XCTAssert(result == nil, "Pass") 956 | } 957 | 958 | /// An object's attributes and to-one relationships should be serializaable into json. 959 | func testToOneRelationshipSerialization() { 960 | let hat = Dandy.insert(Hat.self)! 961 | hat.setValue("bowler", forKey: "name") 962 | hat.setValue("billycock", forKey: "styleDescription") 963 | let felt = Dandy.insert(Material.self)! 964 | felt.setValue("felt", forKey: "name") 965 | felt.setValue("Rome", forKey: "origin") 966 | hat.setValue(felt, forKey: "primaryMaterial") 967 | let expected = [ 968 | "name": "bowler", 969 | "style": "billycock", 970 | "material": [ 971 | "name": "felt", 972 | "origin": "Rome" 973 | ] 974 | ] as JSONObject 975 | let result = Serializer.serialize(hat, including:["primaryMaterial"])! 976 | XCTAssert(result == expected, "Pass") 977 | } 978 | 979 | /// An array of NSManagedObject should be serializable into json. 980 | func testObjectArraySerialization() { 981 | let byron = Dandy.insert(Dandy_.self)! 982 | byron.name = "Lord Byron" 983 | byron.dandyID = "1" 984 | let wilde = Dandy.insert(Dandy_.self)! 985 | wilde.name = "Oscar Wilde" 986 | wilde.dandyID = "2" 987 | let andre = Dandy.insert(Dandy_.self)! 988 | andre.name = "Andre 3000" 989 | andre.dandyID = "3" 990 | let expected: [JSONObject] = [[ 991 | "id": "1", 992 | "name": "Lord Byron" 993 | ], [ 994 | "id": "2", 995 | "name": "Oscar Wilde"], [ 996 | "id": "3", 997 | "name": "Andre 3000" 998 | ] 999 | ] 1000 | let result = Serializer.serialize([byron, wilde, andre])! 1001 | XCTAssert(result == expected, "Pass") 1002 | } 1003 | 1004 | /// An object's attributes and to-many relationships should be serializaable into json. 1005 | func testToManyRelationshipSerialization() { 1006 | let byron = Dandy.insert(Dandy_.self)! 1007 | byron.name = "Lord Byron" 1008 | byron.dandyID = "1" 1009 | let bowler = Dandy.insert(Hat.self)! 1010 | bowler.name = "bowler" 1011 | bowler.styleDescription = "billycock" 1012 | let tyrolean = Dandy.insert(Hat.self)! 1013 | tyrolean.name = "tyrolean" 1014 | tyrolean.styleDescription = "alpine" 1015 | byron.hats = Set([bowler, tyrolean]) as NSSet 1016 | let expected = [ 1017 | "id": "1", 1018 | "name": "Lord Byron", 1019 | "hats": [ 1020 | [ 1021 | "name": "bowler", 1022 | "style": "billycock" 1023 | ], [ 1024 | "name": "tyrolean", 1025 | "style": "alpine" 1026 | ] 1027 | ] 1028 | ] as JSONObject 1029 | var result = Serializer.serialize(byron, including:["hats"])! 1030 | result["hats"] = (result["hats"] as! NSArray).sortedArray(using: [NSSortDescriptor(key: "name", ascending: true)]) 1031 | XCTAssert(result == expected, "Pass") 1032 | } 1033 | 1034 | /// An object's attributes and relationship tree should be serializaable into json. 1035 | func testNestedRelationshipSerialization() { 1036 | let gossip = Dandy.insert(Gossip.self)! 1037 | gossip.details = "At Bo Peep, unusually cool towards Isabella Brown." 1038 | gossip.topic = "John Keats" 1039 | let byron = Dandy.insert(Dandy_.self)! 1040 | byron.name = "Lord Byron" 1041 | byron.dandyID = "1" 1042 | let bowler = Dandy.insert(Hat.self)! 1043 | bowler.name = "bowler" 1044 | bowler.styleDescription = "billycock" 1045 | byron.hats = Set([bowler]) as NSSet 1046 | gossip.purveyor = byron 1047 | let expected = [ 1048 | "details": "At Bo Peep, unusually cool towards Isabella Brown.", 1049 | "topic": "John Keats", 1050 | "purveyor": [ 1051 | "id": "1", 1052 | "name": "Lord Byron", 1053 | "hats": [ 1054 | [ 1055 | "name": "bowler", 1056 | "style": "billycock", 1057 | ] 1058 | ] 1059 | ] 1060 | ] as JSONObject 1061 | let result = Serializer.serialize(gossip, including: ["purveyor.hats"]) as! [String: NSObject] 1062 | XCTAssert(result == expected, "Pass") 1063 | } 1064 | 1065 | // MARK: - Extension tests - 1066 | 1067 | /// Entries from one dictionary should add correctly to another dictionary of the same type 1068 | func testDictionaryEntryAddition() { 1069 | var balzac = ["name": "Honoré de Balzac"] 1070 | let profession = ["profession": "author"] 1071 | balzac.addEntriesFrom(profession) 1072 | XCTAssert(balzac["name"] == "Honoré de Balzac" && balzac["profession"] == "author", "Pass") 1073 | } 1074 | 1075 | /// Values in a dictionary should be accessible via keypath 1076 | func testValueForKeyPathExtraction() { 1077 | let hats = [[ 1078 | "name": "bowler", 1079 | "style": "billycock", 1080 | "material": [ 1081 | "name": "felt", 1082 | "origin": "Rome" 1083 | ] 1084 | ]] 1085 | 1086 | let gossip = [ 1087 | "details": "At Bo Peep, unusually cool towards Isabella Brown.", 1088 | "topic": "John Keats", 1089 | "purveyor": [ 1090 | "id": "1", 1091 | "name": "Lord Byron", 1092 | "hats": hats 1093 | ] 1094 | ] as JSONObject 1095 | 1096 | let value: [JSONObject]? = _value(at: "purveyor.hats", of: gossip) 1097 | XCTAssert(value! == hats, "Pass") 1098 | } 1099 | 1100 | /// Directories that exist should be reported as existing. 1101 | func testDirectoryExistenceEvaluation() { 1102 | let applications = FileManager.default.urls(for: .applicationDirectory, in: .userDomainMask).last! 1103 | var components = applications.pathComponents 1104 | components.removeLast() 1105 | let root = URL(string: NSString.path(withComponents: components))! 1106 | XCTAssert(FileManager.directoryExists(at: root) == true, "Incorrectly evaluated existence of Application directory") 1107 | } 1108 | 1109 | /// Directories that do not exists should be reported as non-existent. 1110 | func testDirectoryInexistenceEvaluation() { 1111 | let url = URL(string: "file://lord-byron/diary/screeds/creditors")! 1112 | XCTAssert(FileManager.directoryExists(at: url) == false, "Incorrectly evaluated existence of nonsense directory") 1113 | } 1114 | 1115 | /// One should be able to create directories. 1116 | func testDirectoryCreation() { 1117 | let applications = FileManager.default.urls(for: .applicationDirectory, in: .userDomainMask).last! 1118 | var components = applications.pathComponents 1119 | components.removeLast() 1120 | let root = URL(string: "file://" + NSString.path(withComponents: components))! 1121 | 1122 | let directory = root.appendingPathComponent("lord-byron", isDirectory: true) 1123 | 1124 | if FileManager.directoryExists(at: directory) { 1125 | // Clean up previous executions of the test if trace of them remains. 1126 | try! FileManager.default.removeItem(at: directory) 1127 | } 1128 | 1129 | try! FileManager.createDirectory(at: directory) 1130 | XCTAssert(FileManager.directoryExists(at: directory) == true, "Failed to create directory") 1131 | } 1132 | 1133 | // MARK: - Warning emission tests - 1134 | 1135 | /// Dandy should format log messages consistently. 1136 | func testMessageFormatting() { 1137 | let warning = "Failed to serialize object Dandy including relationships hats" 1138 | let log = format(warning) 1139 | XCTAssert(log == "(CoreDataDandy) warning: " + warning, "Pass") 1140 | } 1141 | 1142 | /// Dandy should format NSErrors into log messages consistently. 1143 | func testWarningErrorEmission() { 1144 | let error = NSError(domain: "DANDY_FETCH_ERROR", code: 1, userInfo: nil) 1145 | let warning = "Failed to serialize object Dandy including relationships hats" 1146 | let log = format(warning, with: error) 1147 | XCTAssert(log == "(CoreDataDandy) warning: " + warning + " Error:\n" + error.description, "Pass") 1148 | } 1149 | } 1150 | 1151 | // 1152 | /// For testing purposes only, json comparators. 1153 | func ==(lhs: JSONObject, rhs: JSONObject) -> Bool { 1154 | // Dictionaries of unequal counts are not equal 1155 | if lhs.count != rhs.count { return false } 1156 | // Dictionaries that are equal must share all keys and paired values 1157 | for (key, lhValue) in lhs { 1158 | if let rhValue = rhs[key] { 1159 | switch (lhValue, rhValue) { 1160 | case let (l, r) where l is String && r is String: 1161 | return (l as! String) == (r as! String) 1162 | case let (l, r) where l is Bool && r is Bool: 1163 | return (l as! Bool) == (r as! Bool) 1164 | case let (l, r) where l is Double && r is Double: 1165 | return (l as! Double) == (r as! Double) 1166 | case let (l, r) where l is JSONObject && r is JSONObject: 1167 | return (l as! JSONObject) == (r as! JSONObject) 1168 | case let (l, r) where l is NSObject && r is NSObject: 1169 | return (l as! NSObject) == (r as! NSObject) 1170 | default: 1171 | return false 1172 | } 1173 | } else { 1174 | return false 1175 | } 1176 | } 1177 | return true 1178 | } 1179 | 1180 | fileprivate func !=(lhs: JSONObject, rhs: JSONObject) -> Bool { 1181 | return !(lhs == rhs) 1182 | } 1183 | 1184 | 1185 | fileprivate func ==(lhs: [JSONObject], rhs: [JSONObject]) -> Bool { 1186 | if lhs.count != rhs.count { 1187 | return false 1188 | } 1189 | 1190 | let paired = zip(lhs, rhs) 1191 | for (l, r) in paired { 1192 | if l != r { 1193 | return false 1194 | } 1195 | } 1196 | 1197 | return true 1198 | } 1199 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/DandyModel.xcdatamodeld/DandyModel.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Conclusion+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conclusion+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Conclusion) 13 | public class Conclusion: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Conclusion+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conclusion+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Conclusion { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Conclusion"); 17 | } 18 | 19 | @NSManaged public var content: String? 20 | @NSManaged public var id: String? 21 | 22 | } 23 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Conclusion+Finalization.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Conclusion+Finalization.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 10/31/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | extension Conclusion: MappingFinalizer { 12 | public func finalizeMapping(of json: JSONObject) { 13 | if var content = content { 14 | content += "_FINALIZED" 15 | self.content = content 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Dandy_+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dandy_+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Dandy_) 13 | public class Dandy_: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Dandy_+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Dandy_+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Dandy_ { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Dandy_"); 17 | } 18 | 19 | @NSManaged public var bio: String? 20 | @NSManaged public var dandyID: String? 21 | @NSManaged public var dateOfBirth: NSDate? 22 | @NSManaged public var name: String? 23 | @NSManaged public var gossip: NSSet? 24 | @NSManaged public var hats: NSSet? 25 | @NSManaged public var predecessor: Dandy_? 26 | @NSManaged public var successor: Dandy_? 27 | 28 | } 29 | 30 | // MARK: Generated accessors for gossip 31 | extension Dandy_ { 32 | 33 | @objc(addGossipObject:) 34 | @NSManaged public func addToGossip(_ value: Gossip) 35 | 36 | @objc(removeGossipObject:) 37 | @NSManaged public func removeFromGossip(_ value: Gossip) 38 | 39 | @objc(addGossip:) 40 | @NSManaged public func addToGossip(_ values: NSSet) 41 | 42 | @objc(removeGossip:) 43 | @NSManaged public func removeFromGossip(_ values: NSSet) 44 | 45 | } 46 | 47 | // MARK: Generated accessors for hats 48 | extension Dandy_ { 49 | 50 | @objc(addHatsObject:) 51 | @NSManaged public func addToHats(_ value: Hat) 52 | 53 | @objc(removeHatsObject:) 54 | @NSManaged public func removeFromHats(_ value: Hat) 55 | 56 | @objc(addHats:) 57 | @NSManaged public func addToHats(_ values: NSSet) 58 | 59 | @objc(removeHats:) 60 | @NSManaged public func removeFromHats(_ values: NSSet) 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Flattery+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flattery+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Flattery) 13 | public class Flattery: Gossip { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Flattery+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Flattery+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Flattery { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Flattery"); 17 | } 18 | 19 | @NSManaged public var ambition: String? 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Gossip+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gossip+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Gossip) 13 | public class Gossip: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Gossip+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Gossip+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Gossip { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Gossip"); 17 | } 18 | 19 | @NSManaged public var details: String? 20 | @NSManaged public var secret: String? 21 | @NSManaged public var topic: String? 22 | @NSManaged public var purveyor: Dandy_? 23 | 24 | } 25 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Hat+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hat+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Hat) 13 | public class Hat: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Hat+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Hat+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Hat { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Hat"); 17 | } 18 | 19 | @NSManaged public var name: String? 20 | @NSManaged public var styleDescription: String? 21 | @NSManaged public var dandies: NSOrderedSet? 22 | @NSManaged public var primaryMaterial: Material? 23 | 24 | } 25 | 26 | // MARK: Generated accessors for dandies 27 | extension Hat { 28 | 29 | @objc(insertObject:inDandiesAtIndex:) 30 | @NSManaged public func insertIntoDandies(_ value: Dandy_, at idx: Int) 31 | 32 | @objc(removeObjectFromDandiesAtIndex:) 33 | @NSManaged public func removeFromDandies(at idx: Int) 34 | 35 | @objc(insertDandies:atIndexes:) 36 | @NSManaged public func insertIntoDandies(_ values: [Dandy_], at indexes: NSIndexSet) 37 | 38 | @objc(removeDandiesAtIndexes:) 39 | @NSManaged public func removeFromDandies(at indexes: NSIndexSet) 40 | 41 | @objc(replaceObjectInDandiesAtIndex:withObject:) 42 | @NSManaged public func replaceDandies(at idx: Int, with value: Dandy_) 43 | 44 | @objc(replaceDandiesAtIndexes:withDandies:) 45 | @NSManaged public func replaceDandies(at indexes: NSIndexSet, with values: [Dandy_]) 46 | 47 | @objc(addDandiesObject:) 48 | @NSManaged public func addToDandies(_ value: Dandy_) 49 | 50 | @objc(removeDandiesObject:) 51 | @NSManaged public func removeFromDandies(_ value: Dandy_) 52 | 53 | @objc(addDandies:) 54 | @NSManaged public func addToDandies(_ values: NSOrderedSet) 55 | 56 | @objc(removeDandies:) 57 | @NSManaged public func removeFromDandies(_ values: NSOrderedSet) 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Material+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Material+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Material) 13 | public class Material: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Material+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Material+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Material { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Material"); 17 | } 18 | 19 | @NSManaged public var name: String? 20 | @NSManaged public var origin: String? 21 | @NSManaged public var hats: Hat? 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Plebian+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plebian+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Plebian) 13 | public class Plebian: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Plebian+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Plebian+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Plebian { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Plebian"); 17 | } 18 | 19 | @NSManaged public var name: String? 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Slander+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Slander+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Slander) 13 | public class Slander: Gossip { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Slander+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Slander+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Slander { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Slander"); 17 | } 18 | 19 | @NSManaged public var statement: String? 20 | 21 | } 22 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Space+CoreDataClass.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Space+CoreDataClass.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | @objc(Space) 13 | public class Space: NSManagedObject { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /Tests/CoreDataDandyTests/Models/Space+CoreDataProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Space+CoreDataProperties.swift 3 | // CoreDataDandyTests 4 | // 5 | // Created by Noah Blake on 11/10/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import CoreData 11 | 12 | 13 | extension Space { 14 | 15 | @nonobjc public class func fetchRequest() -> NSFetchRequest { 16 | return NSFetchRequest(entityName: "Space"); 17 | } 18 | 19 | @NSManaged public var name: String? 20 | @NSManaged public var spaceState: String? 21 | 22 | } 23 | -------------------------------------------------------------------------------- /header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ml-archive/CoreDataDandy/95f5bd7b09794790f6a03c21eb02575a1066ea56/header.png --------------------------------------------------------------------------------