├── .gitignore ├── .gitmodules ├── APIModel.podspec ├── APIModel.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── ApiModel.xcscheme ├── APIModel.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── CHANGELOG.md ├── Info.plist ├── LICENSE ├── Podfile ├── Podfile.lock ├── README.md ├── Source ├── ApiCall.swift ├── ApiConfig.swift ├── ApiConfigurable.swift ├── ApiForm.swift ├── ApiId.swift ├── ApiManager.swift ├── ApiModel.swift ├── ApiParser.swift ├── ApiRequest.swift ├── ApiResponse.swift ├── ApiRoutes.swift ├── ArrayTransform.swift ├── BoolTransform.swift ├── DefaultTransform.swift ├── DoubleTransform.swift ├── FloatTransform.swift ├── FormDataEncoding.swift ├── IntTransform.swift ├── JSONParser.swift ├── ModelTransform.swift ├── NSDateTransform.swift ├── Object+ApiModel.swift ├── Object+JSONDictionary.swift ├── PercentageTransform.swift ├── StringTransform.swift ├── Transform.swift ├── TransformChain.swift ├── Utils.swift └── vendor │ └── Pluralize.swift └── Tests ├── ApiFormTests.swift ├── ApiManagerRequestTests.swift ├── ApiManagerResponseTests.swift ├── ArrayTransformTests.swift ├── Feed.swift ├── Info.plist ├── ModelTransformTests.swift ├── NSDateTransformTests.swift ├── Post.swift ├── RootNamespaceTests.swift ├── post_with_error.json └── posts.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | .DS_Store 3 | */build/* 4 | *.pbxuser 5 | !default.pbxuser 6 | *.mode1v3 7 | !default.mode1v3 8 | *.mode2v3 9 | !default.mode2v3 10 | *.perspectivev3 11 | !default.perspectivev3 12 | xcuserdata 13 | profile 14 | *.moved-aside 15 | DerivedData 16 | .idea/ 17 | *.hmap 18 | *.xccheckout 19 | 20 | #CocoaPods 21 | Pods -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Source/SwiftyJSON"] 2 | path = Source/SwiftyJSON 3 | url = git@github.com:SwiftyJSON/SwiftyJSON.git 4 | [submodule "Source/Alamofire"] 5 | path = Source/Alamofire 6 | url = git@github.com:Alamofire/Alamofire.git 7 | -------------------------------------------------------------------------------- /APIModel.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "APIModel" 3 | s.module_name = "ApiModel" 4 | s.version = "1.0.0" 5 | s.summary = "Easy API integrations using Realm and Swift" 6 | s.swift_version = '4.2' 7 | 8 | s.description = <<-DESC 9 | Easy get up and running with any API, with maximum flexibility, 10 | intuitive boilerplate and a very declarative aproach to API integrations. 11 | DESC 12 | 13 | s.homepage = "https://github.com/erkie/ApiModel" 14 | 15 | s.license = "MIT" 16 | s.author = { "Erik Rothoff Andersson" => "erik.rothoff@gmail.com" } 17 | s.ios.deployment_target = '8.0' 18 | s.source = { git: "https://github.com/erkie/ApiModel.git", tag: s.version } 19 | s.source_files = "Source/**/*" 20 | 21 | s.requires_arc = true 22 | s.dependency "Alamofire", "~> 4.7" 23 | s.dependency "SwiftyJSON", "~> 4.2" 24 | s.dependency "RealmSwift", "~> 3.13" 25 | end 26 | -------------------------------------------------------------------------------- /APIModel.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 03088EE91C36B54200F02288 /* ApiManagerRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03088EE81C36B54200F02288 /* ApiManagerRequestTests.swift */; }; 11 | 030C6B361B010BE100F72ECC /* ApiModel.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 030C6B351B010BE100F72ECC /* ApiModel.podspec */; }; 12 | 0326C5161AFB7B79005FC057 /* Pluralize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0326C5121AFB7B79005FC057 /* Pluralize.swift */; }; 13 | 033DEF1F1B1CDE1D00266453 /* ApiParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033DEF1E1B1CDE1D00266453 /* ApiParser.swift */; }; 14 | 033DEF221B1CDEBE00266453 /* JSONParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033DEF211B1CDEBE00266453 /* JSONParser.swift */; }; 15 | 033E23EC1AF6284E00F1C171 /* ApiRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E23EB1AF6284E00F1C171 /* ApiRequest.swift */; }; 16 | 033E23EF1AF628FF00F1C171 /* ApiResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E23EE1AF628FF00F1C171 /* ApiResponse.swift */; }; 17 | 033E23F91AF62EB600F1C171 /* README.md in Sources */ = {isa = PBXBuildFile; fileRef = 033E23F81AF62EB600F1C171 /* README.md */; }; 18 | 033E791C1B7C9F78008E2A4D /* RootNamespaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 033E791B1B7C9F78008E2A4D /* RootNamespaceTests.swift */; }; 19 | 033E79231B7C9FBB008E2A4D /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7A81AAF82630085F048 /* Utils.swift */; }; 20 | 0370B7A91AAF82630085F048 /* ApiManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7921AAF82630085F048 /* ApiManager.swift */; }; 21 | 0370B7AA1AAF82630085F048 /* ApiCall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7931AAF82630085F048 /* ApiCall.swift */; }; 22 | 0370B7AB1AAF82630085F048 /* ApiConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7941AAF82630085F048 /* ApiConfig.swift */; }; 23 | 0370B7AC1AAF82630085F048 /* ApiId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7951AAF82630085F048 /* ApiId.swift */; }; 24 | 0370B7AD1AAF82630085F048 /* ApiForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7961AAF82630085F048 /* ApiForm.swift */; }; 25 | 0370B7AF1AAF82630085F048 /* ArrayTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7981AAF82630085F048 /* ArrayTransform.swift */; }; 26 | 0370B7B01AAF82630085F048 /* BoolTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7991AAF82630085F048 /* BoolTransform.swift */; }; 27 | 0370B7B21AAF82630085F048 /* IntTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B79B1AAF82630085F048 /* IntTransform.swift */; }; 28 | 0370B7B31AAF82630085F048 /* ApiModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B79C1AAF82630085F048 /* ApiModel.swift */; }; 29 | 0370B7B41AAF82630085F048 /* ModelTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B79D1AAF82630085F048 /* ModelTransform.swift */; }; 30 | 0370B7B51AAF82630085F048 /* NSDateTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B79E1AAF82630085F048 /* NSDateTransform.swift */; }; 31 | 0370B7B61AAF82630085F048 /* PercentageTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B79F1AAF82630085F048 /* PercentageTransform.swift */; }; 32 | 0370B7BD1AAF82630085F048 /* StringTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7A61AAF82630085F048 /* StringTransform.swift */; }; 33 | 0370B7BE1AAF82630085F048 /* Transform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7A71AAF82630085F048 /* Transform.swift */; }; 34 | 0370B7BF1AAF82630085F048 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7A81AAF82630085F048 /* Utils.swift */; }; 35 | 0370B7C01AAF82D50085F048 /* Object+JSONDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0370B7A21AAF82630085F048 /* Object+JSONDictionary.swift */; }; 36 | 038436CB1BB691A400DCCCF0 /* FormDataEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038436CA1BB691A400DCCCF0 /* FormDataEncoding.swift */; }; 37 | 038436CD1BB69D4300DCCCF0 /* ApiConfigurable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038436CC1BB69D4300DCCCF0 /* ApiConfigurable.swift */; }; 38 | 038CAB191B17C21D00440808 /* ApiRoutes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 038CAB181B17C21D00440808 /* ApiRoutes.swift */; }; 39 | 03B4078E1C3BB57000E46AD3 /* NSDateTransformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03B4078D1C3BB57000E46AD3 /* NSDateTransformTests.swift */; }; 40 | 03CA2D901C503F51008F0AF1 /* ModelTransformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CA2D8F1C503F51008F0AF1 /* ModelTransformTests.swift */; }; 41 | 03CA2D931C50469D008F0AF1 /* ArrayTransformTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CA2D921C50469D008F0AF1 /* ArrayTransformTests.swift */; }; 42 | 03CA2D951C50D3DD008F0AF1 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03CA2D941C50D3DD008F0AF1 /* Feed.swift */; }; 43 | 03E79CEC1BAD5CE9001258A2 /* DefaultTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E79CEA1BAD5CE9001258A2 /* DefaultTransform.swift */; }; 44 | 03E79CED1BAD5CE9001258A2 /* FloatTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E79CEB1BAD5CE9001258A2 /* FloatTransform.swift */; }; 45 | 03E79CEF1BAD5D20001258A2 /* TransformChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03E79CEE1BAD5D20001258A2 /* TransformChain.swift */; }; 46 | 2052AD4F1C4850A900C54C6A /* ApiManagerResponseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2052AD4E1C4850A900C54C6A /* ApiManagerResponseTests.swift */; }; 47 | 2052AD511C485AB900C54C6A /* ApiFormTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2052AD501C485AB900C54C6A /* ApiFormTests.swift */; }; 48 | 2052AD531C485C6800C54C6A /* posts.json in Resources */ = {isa = PBXBuildFile; fileRef = 2052AD521C485C6800C54C6A /* posts.json */; }; 49 | 207552BC1C484AD4009D519A /* Post.swift in Sources */ = {isa = PBXBuildFile; fileRef = 207552BB1C484AD4009D519A /* Post.swift */; }; 50 | 20A279241C4EA51200120C5F /* post_with_error.json in Resources */ = {isa = PBXBuildFile; fileRef = 20A279231C4EA51200120C5F /* post_with_error.json */; }; 51 | 6D47F25F1AF786C8007DEF9A /* DoubleTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D47F25E1AF786C8007DEF9A /* DoubleTransform.swift */; }; 52 | 6DE1A3261AFB6035002CB25E /* Object+ApiModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DE1A3251AFB6035002CB25E /* Object+ApiModel.swift */; }; 53 | 7A531057CB015DCF5408E0BA /* Pods_Tests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 64E2004F1D9A8AB8700E796F /* Pods_Tests.framework */; }; 54 | 8BBF780458939A519667201D /* Pods_ApiModel.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4AA430C8F42F556B8C7DE206 /* Pods_ApiModel.framework */; }; 55 | /* End PBXBuildFile section */ 56 | 57 | /* Begin PBXContainerItemProxy section */ 58 | 033E791E1B7C9F78008E2A4D /* PBXContainerItemProxy */ = { 59 | isa = PBXContainerItemProxy; 60 | containerPortal = 03C60D831AAB5B9A00FE99EA /* Project object */; 61 | proxyType = 1; 62 | remoteGlobalIDString = 0366A4821AAF66ED00506F50; 63 | remoteInfo = ApiModel; 64 | }; 65 | /* End PBXContainerItemProxy section */ 66 | 67 | /* Begin PBXFileReference section */ 68 | 03088EE81C36B54200F02288 /* ApiManagerRequestTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiManagerRequestTests.swift; sourceTree = ""; }; 69 | 030C6B351B010BE100F72ECC /* ApiModel.podspec */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = ApiModel.podspec; sourceTree = SOURCE_ROOT; }; 70 | 0326C5121AFB7B79005FC057 /* Pluralize.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pluralize.swift; sourceTree = ""; }; 71 | 033DEF1E1B1CDE1D00266453 /* ApiParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiParser.swift; sourceTree = ""; }; 72 | 033DEF211B1CDEBE00266453 /* JSONParser.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONParser.swift; sourceTree = ""; }; 73 | 033E23EB1AF6284E00F1C171 /* ApiRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiRequest.swift; sourceTree = ""; }; 74 | 033E23EE1AF628FF00F1C171 /* ApiResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiResponse.swift; sourceTree = ""; }; 75 | 033E23F21AF62A0D00F1C171 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ../Info.plist; sourceTree = ""; }; 76 | 033E23F81AF62EB600F1C171 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 77 | 033E79171B7C9F78008E2A4D /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 78 | 033E791A1B7C9F78008E2A4D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 79 | 033E791B1B7C9F78008E2A4D /* RootNamespaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootNamespaceTests.swift; sourceTree = ""; }; 80 | 0366A4831AAF66ED00506F50 /* ApiModel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApiModel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 81 | 0370B7921AAF82630085F048 /* ApiManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiManager.swift; sourceTree = ""; }; 82 | 0370B7931AAF82630085F048 /* ApiCall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiCall.swift; sourceTree = ""; }; 83 | 0370B7941AAF82630085F048 /* ApiConfig.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiConfig.swift; sourceTree = ""; }; 84 | 0370B7951AAF82630085F048 /* ApiId.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiId.swift; sourceTree = ""; }; 85 | 0370B7961AAF82630085F048 /* ApiForm.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiForm.swift; sourceTree = ""; }; 86 | 0370B7981AAF82630085F048 /* ArrayTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayTransform.swift; sourceTree = ""; }; 87 | 0370B7991AAF82630085F048 /* BoolTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoolTransform.swift; sourceTree = ""; }; 88 | 0370B79B1AAF82630085F048 /* IntTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IntTransform.swift; sourceTree = ""; }; 89 | 0370B79C1AAF82630085F048 /* ApiModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiModel.swift; sourceTree = ""; }; 90 | 0370B79D1AAF82630085F048 /* ModelTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelTransform.swift; sourceTree = ""; }; 91 | 0370B79E1AAF82630085F048 /* NSDateTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateTransform.swift; sourceTree = ""; }; 92 | 0370B79F1AAF82630085F048 /* PercentageTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PercentageTransform.swift; sourceTree = ""; }; 93 | 0370B7A21AAF82630085F048 /* Object+JSONDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Object+JSONDictionary.swift"; sourceTree = ""; }; 94 | 0370B7A61AAF82630085F048 /* StringTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringTransform.swift; sourceTree = ""; }; 95 | 0370B7A71AAF82630085F048 /* Transform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Transform.swift; sourceTree = ""; }; 96 | 0370B7A81AAF82630085F048 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; 97 | 038436CA1BB691A400DCCCF0 /* FormDataEncoding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FormDataEncoding.swift; sourceTree = ""; }; 98 | 038436CC1BB69D4300DCCCF0 /* ApiConfigurable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiConfigurable.swift; sourceTree = ""; }; 99 | 038CAB181B17C21D00440808 /* ApiRoutes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiRoutes.swift; sourceTree = ""; }; 100 | 03B4078D1C3BB57000E46AD3 /* NSDateTransformTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSDateTransformTests.swift; sourceTree = ""; }; 101 | 03CA2D8F1C503F51008F0AF1 /* ModelTransformTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModelTransformTests.swift; sourceTree = ""; }; 102 | 03CA2D921C50469D008F0AF1 /* ArrayTransformTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayTransformTests.swift; sourceTree = ""; }; 103 | 03CA2D941C50D3DD008F0AF1 /* Feed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 104 | 03E79CEA1BAD5CE9001258A2 /* DefaultTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultTransform.swift; sourceTree = ""; }; 105 | 03E79CEB1BAD5CE9001258A2 /* FloatTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatTransform.swift; sourceTree = ""; }; 106 | 03E79CEE1BAD5D20001258A2 /* TransformChain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransformChain.swift; sourceTree = ""; }; 107 | 15DD6784BB53A88083A028FE /* Pods-ApiModel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApiModel.release.xcconfig"; path = "Pods/Target Support Files/Pods-ApiModel/Pods-ApiModel.release.xcconfig"; sourceTree = ""; }; 108 | 1B3FE5D12D9ECCDE7D73DA8F /* Pods-Tests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.debug.xcconfig"; sourceTree = ""; }; 109 | 2052AD4E1C4850A900C54C6A /* ApiManagerResponseTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiManagerResponseTests.swift; sourceTree = ""; }; 110 | 2052AD501C485AB900C54C6A /* ApiFormTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiFormTests.swift; sourceTree = ""; }; 111 | 2052AD521C485C6800C54C6A /* posts.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = posts.json; sourceTree = ""; }; 112 | 207552BB1C484AD4009D519A /* Post.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Post.swift; sourceTree = ""; }; 113 | 20A279231C4EA51200120C5F /* post_with_error.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = post_with_error.json; sourceTree = ""; }; 114 | 4AA430C8F42F556B8C7DE206 /* Pods_ApiModel.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ApiModel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 115 | 64E2004F1D9A8AB8700E796F /* Pods_Tests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Tests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 116 | 6D47F25E1AF786C8007DEF9A /* DoubleTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DoubleTransform.swift; sourceTree = ""; }; 117 | 6DE1A3251AFB6035002CB25E /* Object+ApiModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 4; lastKnownFileType = sourcecode.swift; path = "Object+ApiModel.swift"; sourceTree = ""; tabWidth = 4; usesTabs = 0; }; 118 | 93D75BA7711BF4E883630D4D /* Pods-Tests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Tests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Tests/Pods-Tests.release.xcconfig"; sourceTree = ""; }; 119 | E53515FDDFD8CF2C3B02F011 /* Pods-ApiModel.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApiModel.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApiModel/Pods-ApiModel.debug.xcconfig"; sourceTree = ""; }; 120 | /* End PBXFileReference section */ 121 | 122 | /* Begin PBXFrameworksBuildPhase section */ 123 | 033E79141B7C9F78008E2A4D /* Frameworks */ = { 124 | isa = PBXFrameworksBuildPhase; 125 | buildActionMask = 2147483647; 126 | files = ( 127 | 7A531057CB015DCF5408E0BA /* Pods_Tests.framework in Frameworks */, 128 | ); 129 | runOnlyForDeploymentPostprocessing = 0; 130 | }; 131 | 0366A47F1AAF66ED00506F50 /* Frameworks */ = { 132 | isa = PBXFrameworksBuildPhase; 133 | buildActionMask = 2147483647; 134 | files = ( 135 | 8BBF780458939A519667201D /* Pods_ApiModel.framework in Frameworks */, 136 | ); 137 | runOnlyForDeploymentPostprocessing = 0; 138 | }; 139 | /* End PBXFrameworksBuildPhase section */ 140 | 141 | /* Begin PBXGroup section */ 142 | 0326C5101AFB7B79005FC057 /* Vendor */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | 0326C5121AFB7B79005FC057 /* Pluralize.swift */, 146 | ); 147 | name = Vendor; 148 | path = vendor; 149 | sourceTree = ""; 150 | }; 151 | 033DEF201B1CDE2400266453 /* Parsers */ = { 152 | isa = PBXGroup; 153 | children = ( 154 | 033DEF211B1CDEBE00266453 /* JSONParser.swift */, 155 | ); 156 | name = Parsers; 157 | sourceTree = ""; 158 | }; 159 | 033E23F41AF62AD600F1C171 /* Realm+ApiModel */ = { 160 | isa = PBXGroup; 161 | children = ( 162 | 6DE1A3251AFB6035002CB25E /* Object+ApiModel.swift */, 163 | 0370B7A21AAF82630085F048 /* Object+JSONDictionary.swift */, 164 | ); 165 | name = "Realm+ApiModel"; 166 | sourceTree = ""; 167 | }; 168 | 033E23F71AF62C3800F1C171 /* Transforms */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | 0370B79D1AAF82630085F048 /* ModelTransform.swift */, 172 | 0370B7A71AAF82630085F048 /* Transform.swift */, 173 | 03E79CEA1BAD5CE9001258A2 /* DefaultTransform.swift */, 174 | 03E79CEB1BAD5CE9001258A2 /* FloatTransform.swift */, 175 | 0370B7A61AAF82630085F048 /* StringTransform.swift */, 176 | 0370B7981AAF82630085F048 /* ArrayTransform.swift */, 177 | 0370B7991AAF82630085F048 /* BoolTransform.swift */, 178 | 0370B79B1AAF82630085F048 /* IntTransform.swift */, 179 | 0370B79E1AAF82630085F048 /* NSDateTransform.swift */, 180 | 0370B79F1AAF82630085F048 /* PercentageTransform.swift */, 181 | 6D47F25E1AF786C8007DEF9A /* DoubleTransform.swift */, 182 | 03E79CEE1BAD5D20001258A2 /* TransformChain.swift */, 183 | ); 184 | name = Transforms; 185 | sourceTree = ""; 186 | }; 187 | 033E23FC1AF62EC700F1C171 /* Podspec */ = { 188 | isa = PBXGroup; 189 | children = ( 190 | 030C6B351B010BE100F72ECC /* ApiModel.podspec */, 191 | 033E23F81AF62EB600F1C171 /* README.md */, 192 | ); 193 | name = Podspec; 194 | path = Source; 195 | sourceTree = ""; 196 | }; 197 | 033E79181B7C9F78008E2A4D /* Tests */ = { 198 | isa = PBXGroup; 199 | children = ( 200 | 207552BA1C484AC9009D519A /* Model */, 201 | 2052AD501C485AB900C54C6A /* ApiFormTests.swift */, 202 | 2052AD4E1C4850A900C54C6A /* ApiManagerResponseTests.swift */, 203 | 033E791B1B7C9F78008E2A4D /* RootNamespaceTests.swift */, 204 | 03088EE81C36B54200F02288 /* ApiManagerRequestTests.swift */, 205 | 03B4078C1C3BB55F00E46AD3 /* Transforms */, 206 | 033E79191B7C9F78008E2A4D /* Supporting Files */, 207 | ); 208 | path = Tests; 209 | sourceTree = ""; 210 | }; 211 | 033E79191B7C9F78008E2A4D /* Supporting Files */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 033E791A1B7C9F78008E2A4D /* Info.plist */, 215 | 2052AD521C485C6800C54C6A /* posts.json */, 216 | 20A279231C4EA51200120C5F /* post_with_error.json */, 217 | ); 218 | name = "Supporting Files"; 219 | sourceTree = ""; 220 | }; 221 | 0366A4491AAF668F00506F50 /* Products */ = { 222 | isa = PBXGroup; 223 | children = ( 224 | 0366A4831AAF66ED00506F50 /* ApiModel.framework */, 225 | 033E79171B7C9F78008E2A4D /* Tests.xctest */, 226 | ); 227 | name = Products; 228 | sourceTree = ""; 229 | }; 230 | 037D7F341AAF6E2500AC3D32 /* Supporting Files */ = { 231 | isa = PBXGroup; 232 | children = ( 233 | 033E23F21AF62A0D00F1C171 /* Info.plist */, 234 | ); 235 | name = "Supporting Files"; 236 | sourceTree = ""; 237 | }; 238 | 038436C91BB6919600DCCCF0 /* Parameter Encodings */ = { 239 | isa = PBXGroup; 240 | children = ( 241 | 038436CA1BB691A400DCCCF0 /* FormDataEncoding.swift */, 242 | ); 243 | name = "Parameter Encodings"; 244 | sourceTree = ""; 245 | }; 246 | 03B4078C1C3BB55F00E46AD3 /* Transforms */ = { 247 | isa = PBXGroup; 248 | children = ( 249 | 03B4078D1C3BB57000E46AD3 /* NSDateTransformTests.swift */, 250 | 03CA2D8F1C503F51008F0AF1 /* ModelTransformTests.swift */, 251 | 03CA2D921C50469D008F0AF1 /* ArrayTransformTests.swift */, 252 | ); 253 | name = Transforms; 254 | sourceTree = ""; 255 | }; 256 | 03C60D821AAB5B9A00FE99EA = { 257 | isa = PBXGroup; 258 | children = ( 259 | 033E23FC1AF62EC700F1C171 /* Podspec */, 260 | 03C60D891AAB5C0100FE99EA /* Source */, 261 | 033E79181B7C9F78008E2A4D /* Tests */, 262 | 0366A4491AAF668F00506F50 /* Products */, 263 | A44984974C386E0D5FA4BA09 /* Pods */, 264 | E6CB43CB627BBEFF0EEB10D9 /* Frameworks */, 265 | ); 266 | sourceTree = ""; 267 | }; 268 | 03C60D891AAB5C0100FE99EA /* Source */ = { 269 | isa = PBXGroup; 270 | children = ( 271 | 0370B79C1AAF82630085F048 /* ApiModel.swift */, 272 | 0370B7921AAF82630085F048 /* ApiManager.swift */, 273 | 033E23EB1AF6284E00F1C171 /* ApiRequest.swift */, 274 | 033E23EE1AF628FF00F1C171 /* ApiResponse.swift */, 275 | 0370B7931AAF82630085F048 /* ApiCall.swift */, 276 | 0370B7941AAF82630085F048 /* ApiConfig.swift */, 277 | 0370B7951AAF82630085F048 /* ApiId.swift */, 278 | 0370B7961AAF82630085F048 /* ApiForm.swift */, 279 | 038CAB181B17C21D00440808 /* ApiRoutes.swift */, 280 | 038436CC1BB69D4300DCCCF0 /* ApiConfigurable.swift */, 281 | 033DEF1E1B1CDE1D00266453 /* ApiParser.swift */, 282 | 0370B7A81AAF82630085F048 /* Utils.swift */, 283 | 038436C91BB6919600DCCCF0 /* Parameter Encodings */, 284 | 033DEF201B1CDE2400266453 /* Parsers */, 285 | 033E23F71AF62C3800F1C171 /* Transforms */, 286 | 033E23F41AF62AD600F1C171 /* Realm+ApiModel */, 287 | 037D7F341AAF6E2500AC3D32 /* Supporting Files */, 288 | 0326C5101AFB7B79005FC057 /* Vendor */, 289 | ); 290 | path = Source; 291 | sourceTree = ""; 292 | }; 293 | 207552BA1C484AC9009D519A /* Model */ = { 294 | isa = PBXGroup; 295 | children = ( 296 | 207552BB1C484AD4009D519A /* Post.swift */, 297 | 03CA2D941C50D3DD008F0AF1 /* Feed.swift */, 298 | ); 299 | name = Model; 300 | sourceTree = ""; 301 | }; 302 | A44984974C386E0D5FA4BA09 /* Pods */ = { 303 | isa = PBXGroup; 304 | children = ( 305 | E53515FDDFD8CF2C3B02F011 /* Pods-ApiModel.debug.xcconfig */, 306 | 15DD6784BB53A88083A028FE /* Pods-ApiModel.release.xcconfig */, 307 | 1B3FE5D12D9ECCDE7D73DA8F /* Pods-Tests.debug.xcconfig */, 308 | 93D75BA7711BF4E883630D4D /* Pods-Tests.release.xcconfig */, 309 | ); 310 | name = Pods; 311 | sourceTree = ""; 312 | }; 313 | E6CB43CB627BBEFF0EEB10D9 /* Frameworks */ = { 314 | isa = PBXGroup; 315 | children = ( 316 | 4AA430C8F42F556B8C7DE206 /* Pods_ApiModel.framework */, 317 | 64E2004F1D9A8AB8700E796F /* Pods_Tests.framework */, 318 | ); 319 | name = Frameworks; 320 | sourceTree = ""; 321 | }; 322 | /* End PBXGroup section */ 323 | 324 | /* Begin PBXHeadersBuildPhase section */ 325 | 0366A4801AAF66ED00506F50 /* Headers */ = { 326 | isa = PBXHeadersBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | /* End PBXHeadersBuildPhase section */ 333 | 334 | /* Begin PBXNativeTarget section */ 335 | 033E79161B7C9F78008E2A4D /* Tests */ = { 336 | isa = PBXNativeTarget; 337 | buildConfigurationList = 033E79201B7C9F78008E2A4D /* Build configuration list for PBXNativeTarget "Tests" */; 338 | buildPhases = ( 339 | C4219475390F33872F75E9B1 /* [CP] Check Pods Manifest.lock */, 340 | 033E79131B7C9F78008E2A4D /* Sources */, 341 | 033E79141B7C9F78008E2A4D /* Frameworks */, 342 | 033E79151B7C9F78008E2A4D /* Resources */, 343 | 036A2C70E212862A2313FAAE /* [CP] Embed Pods Frameworks */, 344 | ); 345 | buildRules = ( 346 | ); 347 | dependencies = ( 348 | 033E791F1B7C9F78008E2A4D /* PBXTargetDependency */, 349 | ); 350 | name = Tests; 351 | productName = Tests; 352 | productReference = 033E79171B7C9F78008E2A4D /* Tests.xctest */; 353 | productType = "com.apple.product-type.bundle.unit-test"; 354 | }; 355 | 0366A4821AAF66ED00506F50 /* ApiModel */ = { 356 | isa = PBXNativeTarget; 357 | buildConfigurationList = 0366A4961AAF66ED00506F50 /* Build configuration list for PBXNativeTarget "ApiModel" */; 358 | buildPhases = ( 359 | 04721B9197875F4BBDD8B71A /* [CP] Check Pods Manifest.lock */, 360 | 0366A47E1AAF66ED00506F50 /* Sources */, 361 | 0366A47F1AAF66ED00506F50 /* Frameworks */, 362 | 0366A4811AAF66ED00506F50 /* Resources */, 363 | 0366A4801AAF66ED00506F50 /* Headers */, 364 | ); 365 | buildRules = ( 366 | ); 367 | dependencies = ( 368 | ); 369 | name = ApiModel; 370 | productName = RealmApiModel; 371 | productReference = 0366A4831AAF66ED00506F50 /* ApiModel.framework */; 372 | productType = "com.apple.product-type.framework"; 373 | }; 374 | /* End PBXNativeTarget section */ 375 | 376 | /* Begin PBXProject section */ 377 | 03C60D831AAB5B9A00FE99EA /* Project object */ = { 378 | isa = PBXProject; 379 | attributes = { 380 | LastSwiftMigration = 0700; 381 | LastSwiftUpdateCheck = 0700; 382 | LastUpgradeCheck = 1100; 383 | TargetAttributes = { 384 | 033E79161B7C9F78008E2A4D = { 385 | CreatedOnToolsVersion = 6.4; 386 | LastSwiftMigration = 1100; 387 | }; 388 | 0366A4821AAF66ED00506F50 = { 389 | CreatedOnToolsVersion = 6.1.1; 390 | LastSwiftMigration = 1100; 391 | }; 392 | }; 393 | }; 394 | buildConfigurationList = 03C60D861AAB5B9A00FE99EA /* Build configuration list for PBXProject "ApiModel" */; 395 | compatibilityVersion = "Xcode 3.2"; 396 | developmentRegion = English; 397 | hasScannedForEncodings = 0; 398 | knownRegions = ( 399 | English, 400 | en, 401 | ); 402 | mainGroup = 03C60D821AAB5B9A00FE99EA; 403 | productRefGroup = 0366A4491AAF668F00506F50 /* Products */; 404 | projectDirPath = ""; 405 | projectRoot = ""; 406 | targets = ( 407 | 0366A4821AAF66ED00506F50 /* ApiModel */, 408 | 033E79161B7C9F78008E2A4D /* Tests */, 409 | ); 410 | }; 411 | /* End PBXProject section */ 412 | 413 | /* Begin PBXResourcesBuildPhase section */ 414 | 033E79151B7C9F78008E2A4D /* Resources */ = { 415 | isa = PBXResourcesBuildPhase; 416 | buildActionMask = 2147483647; 417 | files = ( 418 | 20A279241C4EA51200120C5F /* post_with_error.json in Resources */, 419 | 2052AD531C485C6800C54C6A /* posts.json in Resources */, 420 | ); 421 | runOnlyForDeploymentPostprocessing = 0; 422 | }; 423 | 0366A4811AAF66ED00506F50 /* Resources */ = { 424 | isa = PBXResourcesBuildPhase; 425 | buildActionMask = 2147483647; 426 | files = ( 427 | 030C6B361B010BE100F72ECC /* ApiModel.podspec in Resources */, 428 | ); 429 | runOnlyForDeploymentPostprocessing = 0; 430 | }; 431 | /* End PBXResourcesBuildPhase section */ 432 | 433 | /* Begin PBXShellScriptBuildPhase section */ 434 | 036A2C70E212862A2313FAAE /* [CP] Embed Pods Frameworks */ = { 435 | isa = PBXShellScriptBuildPhase; 436 | buildActionMask = 2147483647; 437 | files = ( 438 | ); 439 | inputPaths = ( 440 | "${PODS_ROOT}/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh", 441 | "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework", 442 | "${BUILT_PRODUCTS_DIR}/Realm/Realm.framework", 443 | "${BUILT_PRODUCTS_DIR}/RealmSwift/RealmSwift.framework", 444 | "${BUILT_PRODUCTS_DIR}/SwiftyJSON/SwiftyJSON.framework", 445 | "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework", 446 | ); 447 | name = "[CP] Embed Pods Frameworks"; 448 | outputPaths = ( 449 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Alamofire.framework", 450 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Realm.framework", 451 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RealmSwift.framework", 452 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftyJSON.framework", 453 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework", 454 | ); 455 | runOnlyForDeploymentPostprocessing = 0; 456 | shellPath = /bin/sh; 457 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Tests/Pods-Tests-frameworks.sh\"\n"; 458 | showEnvVarsInLog = 0; 459 | }; 460 | 04721B9197875F4BBDD8B71A /* [CP] Check Pods Manifest.lock */ = { 461 | isa = PBXShellScriptBuildPhase; 462 | buildActionMask = 2147483647; 463 | files = ( 464 | ); 465 | inputPaths = ( 466 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 467 | "${PODS_ROOT}/Manifest.lock", 468 | ); 469 | name = "[CP] Check Pods Manifest.lock"; 470 | outputPaths = ( 471 | "$(DERIVED_FILE_DIR)/Pods-ApiModel-checkManifestLockResult.txt", 472 | ); 473 | runOnlyForDeploymentPostprocessing = 0; 474 | shellPath = /bin/sh; 475 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 476 | showEnvVarsInLog = 0; 477 | }; 478 | C4219475390F33872F75E9B1 /* [CP] Check Pods Manifest.lock */ = { 479 | isa = PBXShellScriptBuildPhase; 480 | buildActionMask = 2147483647; 481 | files = ( 482 | ); 483 | inputPaths = ( 484 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 485 | "${PODS_ROOT}/Manifest.lock", 486 | ); 487 | name = "[CP] Check Pods Manifest.lock"; 488 | outputPaths = ( 489 | "$(DERIVED_FILE_DIR)/Pods-Tests-checkManifestLockResult.txt", 490 | ); 491 | runOnlyForDeploymentPostprocessing = 0; 492 | shellPath = /bin/sh; 493 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 494 | showEnvVarsInLog = 0; 495 | }; 496 | /* End PBXShellScriptBuildPhase section */ 497 | 498 | /* Begin PBXSourcesBuildPhase section */ 499 | 033E79131B7C9F78008E2A4D /* Sources */ = { 500 | isa = PBXSourcesBuildPhase; 501 | buildActionMask = 2147483647; 502 | files = ( 503 | 033E791C1B7C9F78008E2A4D /* RootNamespaceTests.swift in Sources */, 504 | 03CA2D951C50D3DD008F0AF1 /* Feed.swift in Sources */, 505 | 033E79231B7C9FBB008E2A4D /* Utils.swift in Sources */, 506 | 03088EE91C36B54200F02288 /* ApiManagerRequestTests.swift in Sources */, 507 | 207552BC1C484AD4009D519A /* Post.swift in Sources */, 508 | 03CA2D931C50469D008F0AF1 /* ArrayTransformTests.swift in Sources */, 509 | 03CA2D901C503F51008F0AF1 /* ModelTransformTests.swift in Sources */, 510 | 2052AD511C485AB900C54C6A /* ApiFormTests.swift in Sources */, 511 | 2052AD4F1C4850A900C54C6A /* ApiManagerResponseTests.swift in Sources */, 512 | 03B4078E1C3BB57000E46AD3 /* NSDateTransformTests.swift in Sources */, 513 | ); 514 | runOnlyForDeploymentPostprocessing = 0; 515 | }; 516 | 0366A47E1AAF66ED00506F50 /* Sources */ = { 517 | isa = PBXSourcesBuildPhase; 518 | buildActionMask = 2147483647; 519 | files = ( 520 | 0370B7C01AAF82D50085F048 /* Object+JSONDictionary.swift in Sources */, 521 | 033DEF1F1B1CDE1D00266453 /* ApiParser.swift in Sources */, 522 | 0370B7AA1AAF82630085F048 /* ApiCall.swift in Sources */, 523 | 0370B7B41AAF82630085F048 /* ModelTransform.swift in Sources */, 524 | 0370B7AC1AAF82630085F048 /* ApiId.swift in Sources */, 525 | 033E23EF1AF628FF00F1C171 /* ApiResponse.swift in Sources */, 526 | 0370B7B31AAF82630085F048 /* ApiModel.swift in Sources */, 527 | 6D47F25F1AF786C8007DEF9A /* DoubleTransform.swift in Sources */, 528 | 0370B7B21AAF82630085F048 /* IntTransform.swift in Sources */, 529 | 033E23F91AF62EB600F1C171 /* README.md in Sources */, 530 | 03E79CED1BAD5CE9001258A2 /* FloatTransform.swift in Sources */, 531 | 038436CD1BB69D4300DCCCF0 /* ApiConfigurable.swift in Sources */, 532 | 0370B7BD1AAF82630085F048 /* StringTransform.swift in Sources */, 533 | 033DEF221B1CDEBE00266453 /* JSONParser.swift in Sources */, 534 | 0370B7BE1AAF82630085F048 /* Transform.swift in Sources */, 535 | 0326C5161AFB7B79005FC057 /* Pluralize.swift in Sources */, 536 | 0370B7A91AAF82630085F048 /* ApiManager.swift in Sources */, 537 | 0370B7B51AAF82630085F048 /* NSDateTransform.swift in Sources */, 538 | 0370B7AB1AAF82630085F048 /* ApiConfig.swift in Sources */, 539 | 038CAB191B17C21D00440808 /* ApiRoutes.swift in Sources */, 540 | 0370B7BF1AAF82630085F048 /* Utils.swift in Sources */, 541 | 0370B7AF1AAF82630085F048 /* ArrayTransform.swift in Sources */, 542 | 6DE1A3261AFB6035002CB25E /* Object+ApiModel.swift in Sources */, 543 | 0370B7B01AAF82630085F048 /* BoolTransform.swift in Sources */, 544 | 038436CB1BB691A400DCCCF0 /* FormDataEncoding.swift in Sources */, 545 | 0370B7AD1AAF82630085F048 /* ApiForm.swift in Sources */, 546 | 033E23EC1AF6284E00F1C171 /* ApiRequest.swift in Sources */, 547 | 03E79CEC1BAD5CE9001258A2 /* DefaultTransform.swift in Sources */, 548 | 03E79CEF1BAD5D20001258A2 /* TransformChain.swift in Sources */, 549 | 0370B7B61AAF82630085F048 /* PercentageTransform.swift in Sources */, 550 | ); 551 | runOnlyForDeploymentPostprocessing = 0; 552 | }; 553 | /* End PBXSourcesBuildPhase section */ 554 | 555 | /* Begin PBXTargetDependency section */ 556 | 033E791F1B7C9F78008E2A4D /* PBXTargetDependency */ = { 557 | isa = PBXTargetDependency; 558 | target = 0366A4821AAF66ED00506F50 /* ApiModel */; 559 | targetProxy = 033E791E1B7C9F78008E2A4D /* PBXContainerItemProxy */; 560 | }; 561 | /* End PBXTargetDependency section */ 562 | 563 | /* Begin XCBuildConfiguration section */ 564 | 033E79211B7C9F78008E2A4D /* Debug */ = { 565 | isa = XCBuildConfiguration; 566 | baseConfigurationReference = 1B3FE5D12D9ECCDE7D73DA8F /* Pods-Tests.debug.xcconfig */; 567 | buildSettings = { 568 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 569 | ALWAYS_SEARCH_USER_PATHS = NO; 570 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 571 | CLANG_CXX_LIBRARY = "libc++"; 572 | CLANG_WARN_BOOL_CONVERSION = YES; 573 | CLANG_WARN_CONSTANT_CONVERSION = YES; 574 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 575 | CLANG_WARN_EMPTY_BODY = YES; 576 | CLANG_WARN_ENUM_CONVERSION = YES; 577 | CLANG_WARN_INT_CONVERSION = YES; 578 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 579 | CLANG_WARN_UNREACHABLE_CODE = YES; 580 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 581 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 582 | ENABLE_STRICT_OBJC_MSGSEND = YES; 583 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 584 | GCC_C_LANGUAGE_STANDARD = gnu99; 585 | GCC_DYNAMIC_NO_PIC = NO; 586 | GCC_NO_COMMON_BLOCKS = YES; 587 | GCC_OPTIMIZATION_LEVEL = 0; 588 | GCC_PREPROCESSOR_DEFINITIONS = ( 589 | "DEBUG=1", 590 | "$(inherited)", 591 | ); 592 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 593 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 594 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 595 | GCC_WARN_UNDECLARED_SELECTOR = YES; 596 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 597 | GCC_WARN_UNUSED_FUNCTION = YES; 598 | GCC_WARN_UNUSED_VARIABLE = YES; 599 | INFOPLIST_FILE = Tests/Info.plist; 600 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 601 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 602 | MTL_ENABLE_DEBUG_INFO = YES; 603 | OTHER_LDFLAGS = "$(inherited)"; 604 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootof.ApiModel.$(PRODUCT_NAME:rfc1034identifier)"; 605 | PRODUCT_NAME = "$(TARGET_NAME)"; 606 | SDKROOT = iphoneos; 607 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 608 | SWIFT_VERSION = 5.0; 609 | }; 610 | name = Debug; 611 | }; 612 | 033E79221B7C9F78008E2A4D /* Release */ = { 613 | isa = XCBuildConfiguration; 614 | baseConfigurationReference = 93D75BA7711BF4E883630D4D /* Pods-Tests.release.xcconfig */; 615 | buildSettings = { 616 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 617 | ALWAYS_SEARCH_USER_PATHS = NO; 618 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 619 | CLANG_CXX_LIBRARY = "libc++"; 620 | CLANG_WARN_BOOL_CONVERSION = YES; 621 | CLANG_WARN_CONSTANT_CONVERSION = YES; 622 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 623 | CLANG_WARN_EMPTY_BODY = YES; 624 | CLANG_WARN_ENUM_CONVERSION = YES; 625 | CLANG_WARN_INT_CONVERSION = YES; 626 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 627 | CLANG_WARN_UNREACHABLE_CODE = YES; 628 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 629 | COPY_PHASE_STRIP = NO; 630 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 631 | ENABLE_NS_ASSERTIONS = NO; 632 | ENABLE_STRICT_OBJC_MSGSEND = YES; 633 | FRAMEWORK_SEARCH_PATHS = "$(inherited)"; 634 | GCC_C_LANGUAGE_STANDARD = gnu99; 635 | GCC_NO_COMMON_BLOCKS = YES; 636 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 637 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 638 | GCC_WARN_UNDECLARED_SELECTOR = YES; 639 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 640 | GCC_WARN_UNUSED_FUNCTION = YES; 641 | GCC_WARN_UNUSED_VARIABLE = YES; 642 | INFOPLIST_FILE = Tests/Info.plist; 643 | IPHONEOS_DEPLOYMENT_TARGET = 9.3; 644 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 645 | MTL_ENABLE_DEBUG_INFO = NO; 646 | OTHER_LDFLAGS = "$(inherited)"; 647 | PRODUCT_BUNDLE_IDENTIFIER = "com.rootof.ApiModel.$(PRODUCT_NAME:rfc1034identifier)"; 648 | PRODUCT_NAME = "$(TARGET_NAME)"; 649 | SDKROOT = iphoneos; 650 | SWIFT_VERSION = 5.0; 651 | VALIDATE_PRODUCT = YES; 652 | }; 653 | name = Release; 654 | }; 655 | 0366A4971AAF66ED00506F50 /* Debug */ = { 656 | isa = XCBuildConfiguration; 657 | baseConfigurationReference = E53515FDDFD8CF2C3B02F011 /* Pods-ApiModel.debug.xcconfig */; 658 | buildSettings = { 659 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 660 | ALWAYS_SEARCH_USER_PATHS = NO; 661 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 662 | CLANG_CXX_LIBRARY = "libc++"; 663 | CLANG_ENABLE_MODULES = YES; 664 | CLANG_ENABLE_OBJC_ARC = YES; 665 | CLANG_WARN_BOOL_CONVERSION = YES; 666 | CLANG_WARN_CONSTANT_CONVERSION = YES; 667 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 668 | CLANG_WARN_EMPTY_BODY = YES; 669 | CLANG_WARN_ENUM_CONVERSION = YES; 670 | CLANG_WARN_INT_CONVERSION = YES; 671 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 672 | CLANG_WARN_UNREACHABLE_CODE = YES; 673 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 674 | CODE_SIGN_IDENTITY = ""; 675 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 676 | COPY_PHASE_STRIP = NO; 677 | CURRENT_PROJECT_VERSION = 1; 678 | DEFINES_MODULE = YES; 679 | DYLIB_COMPATIBILITY_VERSION = 1; 680 | DYLIB_CURRENT_VERSION = 1; 681 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 682 | ENABLE_STRICT_OBJC_MSGSEND = YES; 683 | FRAMEWORK_SEARCH_PATHS = ( 684 | "$(inherited)", 685 | "$(PROJECT_DIR)/Source", 686 | ); 687 | GCC_C_LANGUAGE_STANDARD = gnu99; 688 | GCC_DYNAMIC_NO_PIC = NO; 689 | GCC_OPTIMIZATION_LEVEL = 0; 690 | GCC_PREPROCESSOR_DEFINITIONS = ( 691 | "DEBUG=1", 692 | "$(inherited)", 693 | ); 694 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 695 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 696 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 697 | GCC_WARN_UNDECLARED_SELECTOR = YES; 698 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 699 | GCC_WARN_UNUSED_FUNCTION = YES; 700 | GCC_WARN_UNUSED_VARIABLE = YES; 701 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 702 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 703 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 704 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 705 | MTL_ENABLE_DEBUG_INFO = YES; 706 | ONLY_ACTIVE_ARCH = YES; 707 | OTHER_LDFLAGS = "$(inherited)"; 708 | PRODUCT_BUNDLE_IDENTIFIER = com.rootof.ApiModel; 709 | PRODUCT_NAME = "$(TARGET_NAME)"; 710 | SDKROOT = iphoneos; 711 | SKIP_INSTALL = YES; 712 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 713 | SWIFT_VERSION = 5.0; 714 | TARGETED_DEVICE_FAMILY = "1,2"; 715 | VALID_ARCHS = "arm64 armv7 armv7s"; 716 | VERSIONING_SYSTEM = "apple-generic"; 717 | VERSION_INFO_PREFIX = ""; 718 | }; 719 | name = Debug; 720 | }; 721 | 0366A4981AAF66ED00506F50 /* Release */ = { 722 | isa = XCBuildConfiguration; 723 | baseConfigurationReference = 15DD6784BB53A88083A028FE /* Pods-ApiModel.release.xcconfig */; 724 | buildSettings = { 725 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 726 | ALWAYS_SEARCH_USER_PATHS = NO; 727 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 728 | CLANG_CXX_LIBRARY = "libc++"; 729 | CLANG_ENABLE_MODULES = YES; 730 | CLANG_ENABLE_OBJC_ARC = YES; 731 | CLANG_WARN_BOOL_CONVERSION = YES; 732 | CLANG_WARN_CONSTANT_CONVERSION = YES; 733 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 734 | CLANG_WARN_EMPTY_BODY = YES; 735 | CLANG_WARN_ENUM_CONVERSION = YES; 736 | CLANG_WARN_INT_CONVERSION = YES; 737 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 738 | CLANG_WARN_UNREACHABLE_CODE = YES; 739 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 740 | CODE_SIGN_IDENTITY = ""; 741 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 742 | COPY_PHASE_STRIP = YES; 743 | CURRENT_PROJECT_VERSION = 1; 744 | DEFINES_MODULE = YES; 745 | DYLIB_COMPATIBILITY_VERSION = 1; 746 | DYLIB_CURRENT_VERSION = 1; 747 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 748 | ENABLE_NS_ASSERTIONS = NO; 749 | ENABLE_STRICT_OBJC_MSGSEND = YES; 750 | FRAMEWORK_SEARCH_PATHS = ( 751 | "$(inherited)", 752 | "$(PROJECT_DIR)/Source", 753 | ); 754 | GCC_C_LANGUAGE_STANDARD = gnu99; 755 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 756 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 757 | GCC_WARN_UNDECLARED_SELECTOR = YES; 758 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 759 | GCC_WARN_UNUSED_FUNCTION = YES; 760 | GCC_WARN_UNUSED_VARIABLE = YES; 761 | INFOPLIST_FILE = "$(SRCROOT)/Info.plist"; 762 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 763 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 764 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 765 | MTL_ENABLE_DEBUG_INFO = NO; 766 | OTHER_LDFLAGS = "$(inherited)"; 767 | PRODUCT_BUNDLE_IDENTIFIER = com.rootof.ApiModel; 768 | PRODUCT_NAME = "$(TARGET_NAME)"; 769 | SDKROOT = iphoneos; 770 | SKIP_INSTALL = YES; 771 | SWIFT_VERSION = 5.0; 772 | TARGETED_DEVICE_FAMILY = "1,2"; 773 | VALIDATE_PRODUCT = YES; 774 | VALID_ARCHS = "arm64 armv7 armv7s"; 775 | VERSIONING_SYSTEM = "apple-generic"; 776 | VERSION_INFO_PREFIX = ""; 777 | }; 778 | name = Release; 779 | }; 780 | 03C60D871AAB5B9A00FE99EA /* Debug */ = { 781 | isa = XCBuildConfiguration; 782 | buildSettings = { 783 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 784 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 785 | CLANG_ENABLE_MODULES = YES; 786 | CLANG_ENABLE_OBJC_ARC = YES; 787 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 788 | CLANG_WARN_BOOL_CONVERSION = YES; 789 | CLANG_WARN_COMMA = YES; 790 | CLANG_WARN_CONSTANT_CONVERSION = YES; 791 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 792 | CLANG_WARN_EMPTY_BODY = YES; 793 | CLANG_WARN_ENUM_CONVERSION = YES; 794 | CLANG_WARN_INFINITE_RECURSION = YES; 795 | CLANG_WARN_INT_CONVERSION = YES; 796 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 797 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 798 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 799 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 800 | CLANG_WARN_STRICT_PROTOTYPES = YES; 801 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 802 | CLANG_WARN_UNREACHABLE_CODE = YES; 803 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 804 | CODE_SIGN_IDENTITY = "iPhone Developer"; 805 | COPY_PHASE_STRIP = NO; 806 | DEFINES_MODULE = YES; 807 | ENABLE_STRICT_OBJC_MSGSEND = YES; 808 | ENABLE_TESTABILITY = YES; 809 | GCC_NO_COMMON_BLOCKS = YES; 810 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 811 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 812 | GCC_WARN_UNDECLARED_SELECTOR = YES; 813 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 814 | GCC_WARN_UNUSED_FUNCTION = YES; 815 | GCC_WARN_UNUSED_VARIABLE = YES; 816 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 817 | ONLY_ACTIVE_ARCH = YES; 818 | SDKROOT = iphoneos; 819 | TARGETED_DEVICE_FAMILY = "1,2"; 820 | VALID_ARCHS = "arm64 armv7 armv7s"; 821 | VERSIONING_SYSTEM = "apple-generic"; 822 | }; 823 | name = Debug; 824 | }; 825 | 03C60D881AAB5B9A00FE99EA /* Release */ = { 826 | isa = XCBuildConfiguration; 827 | buildSettings = { 828 | CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES; 829 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 830 | CLANG_ENABLE_MODULES = YES; 831 | CLANG_ENABLE_OBJC_ARC = YES; 832 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 833 | CLANG_WARN_BOOL_CONVERSION = YES; 834 | CLANG_WARN_COMMA = YES; 835 | CLANG_WARN_CONSTANT_CONVERSION = YES; 836 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 837 | CLANG_WARN_EMPTY_BODY = YES; 838 | CLANG_WARN_ENUM_CONVERSION = YES; 839 | CLANG_WARN_INFINITE_RECURSION = YES; 840 | CLANG_WARN_INT_CONVERSION = YES; 841 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 842 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 843 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 844 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 845 | CLANG_WARN_STRICT_PROTOTYPES = YES; 846 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 847 | CLANG_WARN_UNREACHABLE_CODE = YES; 848 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 849 | CODE_SIGN_IDENTITY = "iPhone Distribution"; 850 | DEFINES_MODULE = YES; 851 | ENABLE_STRICT_OBJC_MSGSEND = YES; 852 | GCC_NO_COMMON_BLOCKS = YES; 853 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 854 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 855 | GCC_WARN_UNDECLARED_SELECTOR = YES; 856 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 857 | GCC_WARN_UNUSED_FUNCTION = YES; 858 | GCC_WARN_UNUSED_VARIABLE = YES; 859 | IPHONEOS_DEPLOYMENT_TARGET = 8.0; 860 | SDKROOT = iphoneos; 861 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 862 | TARGETED_DEVICE_FAMILY = "1,2"; 863 | VALID_ARCHS = "arm64 armv7 armv7s"; 864 | VERSIONING_SYSTEM = "apple-generic"; 865 | }; 866 | name = Release; 867 | }; 868 | /* End XCBuildConfiguration section */ 869 | 870 | /* Begin XCConfigurationList section */ 871 | 033E79201B7C9F78008E2A4D /* Build configuration list for PBXNativeTarget "Tests" */ = { 872 | isa = XCConfigurationList; 873 | buildConfigurations = ( 874 | 033E79211B7C9F78008E2A4D /* Debug */, 875 | 033E79221B7C9F78008E2A4D /* Release */, 876 | ); 877 | defaultConfigurationIsVisible = 0; 878 | defaultConfigurationName = Release; 879 | }; 880 | 0366A4961AAF66ED00506F50 /* Build configuration list for PBXNativeTarget "ApiModel" */ = { 881 | isa = XCConfigurationList; 882 | buildConfigurations = ( 883 | 0366A4971AAF66ED00506F50 /* Debug */, 884 | 0366A4981AAF66ED00506F50 /* Release */, 885 | ); 886 | defaultConfigurationIsVisible = 0; 887 | defaultConfigurationName = Release; 888 | }; 889 | 03C60D861AAB5B9A00FE99EA /* Build configuration list for PBXProject "ApiModel" */ = { 890 | isa = XCConfigurationList; 891 | buildConfigurations = ( 892 | 03C60D871AAB5B9A00FE99EA /* Debug */, 893 | 03C60D881AAB5B9A00FE99EA /* Release */, 894 | ); 895 | defaultConfigurationIsVisible = 0; 896 | defaultConfigurationName = Release; 897 | }; 898 | /* End XCConfigurationList section */ 899 | }; 900 | rootObject = 03C60D831AAB5B9A00FE99EA /* Project object */; 901 | } 902 | -------------------------------------------------------------------------------- /APIModel.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /APIModel.xcodeproj/xcshareddata/xcschemes/ApiModel.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /APIModel.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /APIModel.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.0 4 | 5 | Arbitrarily bump version to 1.0.0. API is stable and it doesn't make sense to have it in production and still be on the `0.X` versioning scheme. 6 | 7 | - Upgrade to Swift 4.2 8 | - Upgrade to Alamofire 4.7 9 | - Upgrade to SwiftyJSON 4.2 10 | - Upgrade to RealmSwift 3.13 11 | 12 | ## 0.14.0 13 | 14 | - Upgrade to Swift 4.0 15 | - Upgrade to RealmSwift 3.0.1 16 | - Upgrade to Alamofire 4.5 17 | - Don't log headers by default since it might contain sensitive information 18 | 19 | ## 0.13.0 20 | 21 | Breaking changes: 22 | 23 | - Change Rails-style callback methods to include the response `ApiModelResponse` instance in the callback parameters. This affects: 24 | - Api.find 25 | - Api.findArray 26 | - Api.update 27 | - Api.create 28 | 29 | This makes it possible to get the error messages from the request, but also lets the caller know more about the request. 30 | 31 | For example, with the new parameters: 32 | 33 | ### New way 34 | 35 | ```swift 36 | Api.find { post, response in 37 | if let post = post { 38 | print("Got post \(post.title)") 39 | } else if let errors = response.errorMessages { 40 | print("got errors: \(errors.join("\n"))") 41 | } 42 | } 43 | ``` 44 | 45 | ### Old way 46 | 47 | ```swift 48 | Api.find { post in 49 | if let post = post { 50 | print("Got post \(post.title)") 51 | } else { 52 | print("error?") 53 | } 54 | } 55 | ``` 56 | 57 | ## 0.12.0 58 | 59 | Breaking changes: 60 | 61 | - Update to Realm 1.0.0 🎈🎈🎈 62 | 63 | ## 0.11.0 64 | 65 | Breaking changes: 66 | 67 | - Changed the interface for `Transform`. The `perform`-method now includes a second parameter: 68 | 69 | ```swift 70 | func perform(value: AnyObject?, realm: Realm?) -> AnyObject? 71 | ``` 72 | 73 | This will be the Realm object that will be associated with any newly created objects. For example in `ModelTransform` or `ArrayTransform`. 74 | 75 | Dependency upgrade: 76 | 77 | - This release bumps Realm to version `0.98.4`, which should not break your app installs. 78 | 79 | ## 0.10.2 80 | 81 | - Fixes #22. Code for `rootNamespace` had been broken since a bad rebase. Included tests to verify fix. 82 | 83 | ## 0.10.1 84 | 85 | - Update to `RealmSwift 0.96.3` 86 | 87 | ## 0.10.0 88 | Breaking changes: 89 | 90 | - Rename `API` to `ApiManager` 91 | - Rename `api()` to `apiManager()` 92 | - Rename `ApiForm` to `Api` 93 | - Rename `ApiFormResponse` to `ApiModelResponse` 94 | - Rename `ApiConfiguration` to `ApiConfig` 95 | - Upgrade `RealmSwift` to `0.96` which might be breaking for your app. [Please read the Realm blog post for more details](https://realm.io/news/realm-objc-swift-0.96.0-beta/) 96 | - Upgrade `Alamofire` to `3.0.0` which might require upgrades of your app. 97 | 98 | ## 0.9.0 99 | 100 | (This version was never released to cocoapods.org) 101 | 102 | - Implement file uploads. Please se `README.md` for more details. 103 | 104 | ## 0.8.3 105 | - Fix crash caused by SwiftyJSON when running on device 106 | 107 | ## 0.8.2 108 | - Fix for when server responds with an unparseable response and with a non-200 status code 109 | 110 | ## 0.8.1 111 | - When checking for array responses also check if root is an array 112 | 113 | ## 0.8.0 114 | - Introduce `ApiConfiguration.rootNamespace` for namespaced responses 115 | 116 | Behind the scenes: 117 | - Write initial tests for root namespace related methods 118 | 119 | ## 0.7.0 120 | - Upgrade RealmSwift to 0.94.0 121 | - Add `encoding` to `ApiConfiguration` and make `.URL` default encoding 122 | 123 | ## 0.6.0 124 | - Add `responseData` and `rawResponse` to `ApiFormResponse` 125 | - Introduce concept of parameter encoding (URL encoding, JSON encoding, etc) 126 | - Upgrade Alamofire to 1.3 127 | 128 | ## 0.5.3 129 | Fixed: 130 | - Treat nil values as false in `BoolTransform` 131 | 132 | ## 0.5.2 133 | Fixed: 134 | - If a request path contains a full URL do not prefix with configurated host 135 | 136 | ## 0.5.1 137 | - Also recognize error responses when error messages are an array 138 | 139 | ## 0.5.0 140 | Breaking changes: 141 | - Rename ApiResource to ApiRoutes 142 | - Rename entire project to ApiModel instead of APIModel 143 | - Remove legacy ApiForm.load method in favor of ApiForm.find 144 | - Rename ApiForm.post to ApiForm.create, and create a corresponding ApiForm.update 145 | 146 | Fixed: 147 | - Create concept of response parsers, with JSONParser as default parser. 148 | - Add basic request logging that is enabled by default 149 | - Create ApiForm.get/post/put/delete methods for more intuitive REST calling 150 | - Add a destroy method on ApiForm with parameters 151 | - Refactor ApiForm internally for more code reuse 152 | - Making it possible to set the path and namespace for .findArray 153 | - Create an ApiFormResponse that is returned by most methods of ApiForm. It contains the parsed objects and other metadata about the request and response 154 | - Correctly deal with pluralized namespaces on save 155 | 156 | ## 0.4.0 157 | - Change `ToArray(realmArray: myArray).get()` to `toArray(myArray)` 158 | 159 | ## 0.3.0 160 | 161 | - Upgrade to `Realm 0.92`, meaning using `import RealmSwift` 162 | - Create `.xcodeproj` to compile standalone `.framework` 163 | - Add method to retrieve an API url for an object. `Object#apiUrlForResource` 164 | - Fix a crash when `primaryKey` wasn't implemented 165 | 166 | ## 0.2.0 167 | 168 | - More stuff 169 | 170 | ## 0.1.0 171 | 172 | - Got it working 173 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | $(PRODUCT_BUNDLE_IDENTIFIER) 7 | CFBundleShortVersionString 8 | 0.8.3 9 | CFBundleVersion 10 | 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rootof Creations HB 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. -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | 3 | source 'https://github.com/CocoaPods/Specs.git' 4 | 5 | use_frameworks! 6 | 7 | def realm_dep 8 | pod 'RealmSwift', '~> 3.14' 9 | end 10 | 11 | target :ApiModel do 12 | realm_dep 13 | 14 | pod 'Alamofire', '~> 4.9' 15 | pod 'SwiftyJSON', '~> 5.0' 16 | end 17 | 18 | target :Tests do 19 | realm_dep 20 | 21 | pod 'Alamofire', '~> 4.9' 22 | pod 'SwiftyJSON', '~> 5.0' 23 | pod 'OHHTTPStubs/Swift', '~> 6.0.0' 24 | end 25 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Alamofire (4.9.0) 3 | - OHHTTPStubs/Core (6.0.0) 4 | - OHHTTPStubs/Default (6.0.0): 5 | - OHHTTPStubs/Core 6 | - OHHTTPStubs/JSON 7 | - OHHTTPStubs/NSURLSession 8 | - OHHTTPStubs/OHPathHelpers 9 | - OHHTTPStubs/JSON (6.0.0): 10 | - OHHTTPStubs/Core 11 | - OHHTTPStubs/NSURLSession (6.0.0): 12 | - OHHTTPStubs/Core 13 | - OHHTTPStubs/OHPathHelpers (6.0.0) 14 | - OHHTTPStubs/Swift (6.0.0): 15 | - OHHTTPStubs/Default 16 | - Realm (3.18.0): 17 | - Realm/Headers (= 3.18.0) 18 | - Realm/Headers (3.18.0) 19 | - RealmSwift (3.18.0): 20 | - Realm (= 3.18.0) 21 | - SwiftyJSON (5.0.0) 22 | 23 | DEPENDENCIES: 24 | - Alamofire (~> 4.9) 25 | - OHHTTPStubs/Swift (~> 6.0.0) 26 | - RealmSwift (~> 3.14) 27 | - SwiftyJSON (~> 5.0) 28 | 29 | SPEC REPOS: 30 | https://github.com/cocoapods/specs.git: 31 | - Alamofire 32 | - OHHTTPStubs 33 | - Realm 34 | - RealmSwift 35 | - SwiftyJSON 36 | 37 | SPEC CHECKSUMS: 38 | Alamofire: afc3e7c6db61476cb45cdd23fed06bad03bbc321 39 | OHHTTPStubs: 752f9b11fd810a15162d50f11c06ff94f8e012eb 40 | Realm: 7b3d04740facf45e958a514b466a32b04f1113d6 41 | RealmSwift: c817b5f374668cf08f649afb05f55d92a787649c 42 | SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 43 | 44 | PODFILE CHECKSUM: 6f3b8818918f3e17cd473ae441bcb7a73e8a32e8 45 | 46 | COCOAPODS: 1.7.5 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApiModel 2 | 3 | Interact with REST apis using realm.io to represent objects. The goal of `ApiModel` is to be easy to setup, easy to grasp, and fun to use. Boilerplate should be kept to a minimum, and also intuitive to set up. 4 | 5 | This project is very much inspired by [@idlefingers'](https://github.com/idlefingers) excellent [api-model](https://github.com/izettle/api-model). 6 | 7 | ## Getting started 8 | 9 | Add `APIModel` to your `Podfile`, and run `pod install`: 10 | 11 | ```ruby 12 | pod 'APIModel', '~> 0.13.0' 13 | ``` 14 | 15 | The key part is to implement the `ApiModel` protocol. 16 | 17 | ```swift 18 | import RealmSwift 19 | import ApiModel 20 | 21 | class Post: Object, ApiModel { 22 | // Standard Realm boilerplate 23 | dynamic var id = "" 24 | dynamic var title = "" 25 | dynamic var contents = "" 26 | dynamic lazy var createdAt = NSDate() 27 | 28 | override class func primaryKey() -> String { 29 | return "id" 30 | } 31 | 32 | // Define the standard namespace this class usually resides in JSON responses 33 | // MUST BE singular ie `post` not `posts` 34 | class func apiNamespace() -> String { 35 | return "post" 36 | } 37 | 38 | // Define where and how to get these. Routes are assumed to use Rails style REST (index, show, update, destroy) 39 | class func apiRoutes() -> ApiRoutes { 40 | return ApiRoutes( 41 | index: "/posts.json", 42 | show: "/post/:id:.json" 43 | ) 44 | } 45 | 46 | // Define how it is converted from JSON responses into Realm objects. A host of transforms are available 47 | // See section "Transforms" in README. They are super easy to create as well! 48 | class func fromJSONMapping() -> JSONMapping { 49 | return [ 50 | "id": ApiIdTransform(), 51 | "title": StringTransform(), 52 | "contents": StringTransform(), 53 | "createdAt": NSDateTransform() 54 | ] 55 | } 56 | 57 | // Define how this object is to be serialized back into a server response format 58 | func JSONDictionary() -> [String:Any] { 59 | return [ 60 | "id": id, 61 | "title": email, 62 | "contents": contents, 63 | "created_at": createdAt 64 | ] 65 | } 66 | } 67 | ``` 68 | 69 | ## Table of Contents 70 | 71 | * [ApiModel](#apimodel) 72 | * [Getting started](#getting-started) 73 | * [Table of Contents](#table-of-contents) 74 | * [Configuring the API](#configuring-the-api) 75 | * [Global and Model-local configurations](#global-and-model-local-configurations) 76 | * [Interacting with APIs](#interacting-with-apis) 77 | * [Basic REST verbs](#basic-rest-verbs) 78 | * [Fetching objects](#fetching-objects) 79 | * [Storing objects](#storing-objects) 80 | * [Transforms](#transforms) 81 | * [Hooks](#hooks) 82 | * [URLs](#urls) 83 | * [Dealing with IDs](#dealing-with-ids) 84 | * [Namespaces and envelopes](#namespaces-and-envelopes) 85 | * [Caching and storage](#caching-and-storage) 86 | * [File uploads](#file-uploads) 87 | * [FileUpload](#fileupload) 88 | * [Thanks to](#thanks-to) 89 | * [License](#license) 90 | 91 | ## Configuring the API 92 | 93 | To represent the API itself, you have to create an object of the `ApiManager` class. This holds a `ApiConfiguration` object defining the host URL for all requests. After it has been created it can be accessed from the `func apiManager() -> ApiManager` singleton function. 94 | 95 | To set it up: 96 | 97 | ```swift 98 | // Put this somewhere in your AppDelegate or together with other initialization code 99 | var apiConfig = ApiConfig(host: "https://service.io/api/v1/") 100 | 101 | ApiSingleton.setInstance(ApiManager(configuration: apiConfig)) 102 | ``` 103 | 104 | If you would like to disable request logging, you can do so by setting `requestLogging` to `false`: 105 | 106 | ```swift 107 | apiConfig.requestLogging = false 108 | ``` 109 | 110 | If you would like ApiModel to use a NSURLSessionConfiguration, you can set it like in the following example: 111 | 112 | ```swift 113 | let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() 114 | configuration.timeoutIntervalForRequest = 15 // shorten default timeout 115 | configuration.timeoutIntervalForResource = 15 // shorten default timeout 116 | 117 | ApiSingleton.setInstance(ApiManager(config: ApiConfig(host: "http://feed.myapi.com", urlSessionConfig:configuration))) 118 | 119 | // or 120 | //... 121 | apiConfig.urlSessionConfig = configuration 122 | ``` 123 | 124 | ### Global and Model-local configurations 125 | 126 | For the most part an API is consistent across endpoints, however in the real world, conventions usually differ wildly. The global configuration is the one set by calling `ApiSingleton.setInstance(ApiManager(configuration: apiConfig))`. 127 | 128 | To have a model-local configuration a model needs to implement the `ApiConfigurable` protocol, which consists of a single method: 129 | 130 | ```swift 131 | public protocol ApiConfigurable { 132 | static func apiConfig(config: ApiConfig) -> ApiConfig 133 | } 134 | ``` 135 | 136 | Input is the root base configuration and output is the model's own config object. The object passed in is a copy of the root configuration, so you are free to modify that object without any side-effects. 137 | 138 | ```swift 139 | static func apiConfig(config: ApiConfig) -> ApiConfig { 140 | config.encoding = ApiRequest.FormDataEncoding 141 | return config 142 | } 143 | ``` 144 | 145 | ## Interacting with APIs 146 | 147 | The base of `ApiModel` is the `Api` wrapper class. This class wraps an `Object` type and takes care of fetching objects, saving objects and dealing with validation errors. 148 | 149 | ### Basic REST verbs 150 | 151 | `ApiModel` supports querying API's using basic HTTP verbs. 152 | 153 | ```swift 154 | // GET call without parameters 155 | Api.get("/v1/posts.json") { response in 156 | println("response.isSuccessful: \(response.isSuccessful)") 157 | println("Response as an array: \(response.array)") 158 | println("Response as a dictionary: \(response.dictionary)") 159 | println("Response errors?: \(response.errors)") 160 | } 161 | 162 | // Other supported methods: 163 | 164 | Api.get(path, parameters: [String:AnyObject]) { response // ... 165 | Api.post(path, parameters: [String:AnyObject]) { response // ... 166 | Api.put(path, parameters: [String:AnyObject]) { response // ... 167 | Api.delete(path, parameters: [String:AnyObject]) { response // ... 168 | 169 | // no parameters 170 | 171 | Api.get(path) { response // ... 172 | Api.post(path) { response // ... 173 | Api.put(path) { response // ... 174 | Api.delete(path) { response // ... 175 | 176 | // You can also pass in custom `ApiConfig` into each of the above mentioned methods: 177 | Api.get(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ... 178 | Api.post(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ... 179 | Api.put(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ... 180 | Api.delete(path, parameters: [String:AnyObject], apiConfig: ApiConfig) { response // ... 181 | ``` 182 | 183 | Most of the time you'll want to use the `ActiveRecord`-style verbs `index/show/create/update` for interacting with a REST API, as described below. 184 | 185 | ### Fetching objects 186 | 187 | Using the `index` of a REST resource: 188 | 189 | `GET /posts.json` 190 | ```swift 191 | Api.findArray { posts, response in 192 | for post in posts { 193 | println("... \(post.title)") 194 | } 195 | } 196 | ``` 197 | 198 | Using the `show` of a REST resource: 199 | 200 | `GET /user.json` 201 | ```swift 202 | Api.find { user, response in 203 | if let user = user { 204 | println("User is: \(user.email)") 205 | } else { 206 | println("Error loading user") 207 | } 208 | } 209 | ``` 210 | 211 | ### Storing objects 212 | 213 | ```swift 214 | var post = Post() 215 | post.title = NSLocalizedString("Hello world - A prologue", comment: "") 216 | post.contents = "Hello!" 217 | post.createdAt = NSDate() 218 | 219 | var form = Api(model: post) 220 | form.save { _ in 221 | if form.hasErrors { 222 | println("Could not save:") 223 | for error in form.errorMessages { 224 | println("... \(error)") 225 | } 226 | } else { 227 | println("Saved! Post #\(post.id)") 228 | } 229 | } 230 | ``` 231 | 232 | `Api` will know that the object is not persisted, since it does not have an `id` set (or which ever field is defined as `primaryKey` in Realm). So a `POST` request will be made as follows: 233 | 234 | `POST /posts.json` 235 | ```json 236 | { 237 | "post": { 238 | "title": "Hello world - A prologue", 239 | "contents": "Hello!", 240 | "created_at": "2015-03-08T14:19:31-01:00" 241 | } 242 | } 243 | ``` 244 | 245 | If the response is successful, the attributes returned by the server will be updated on the model. 246 | 247 | `200 OK` 248 | ```json 249 | { 250 | "post": { 251 | "id": 1 252 | } 253 | } 254 | ``` 255 | 256 | The errors are expected to be in the format: 257 | 258 | `400 BAD REQUEST` 259 | ```json 260 | { 261 | "post": { 262 | "errors": { 263 | "contents": [ 264 | "must be longer than 140 characters" 265 | ] 266 | } 267 | } 268 | } 269 | ``` 270 | 271 | And this will make it possible to access the errors as follows: 272 | 273 | ```swift 274 | form.errors["contents"] // -> [String] 275 | // or 276 | form.errorMessages // -> [String] 277 | ``` 278 | 279 | ## Transforms 280 | 281 | Transforms are used to convert attributes from JSON responses to rich types. The easiest way to explain is to show a simple transform. 282 | 283 | `ApiModel` comes with a host of standard transforms. An example is the `IntTransform`: 284 | 285 | ```swift 286 | import RealmSwift 287 | 288 | class IntTransform: Transform { 289 | func perform(value: AnyObject?, realm: Realm?) -> AnyObject { 290 | if let asInt = value?.integerValue { 291 | return asInt 292 | } else { 293 | return 0 294 | } 295 | } 296 | } 297 | ``` 298 | 299 | This takes an object and attempts to convert it into an integer. If that fails, it returns the default value 0. 300 | 301 | Transforms can be quite complex, and even convert nested models. For example: 302 | 303 | ```swift 304 | class User: Object, ApiModel { 305 | dynamic var id = ApiId() 306 | dynamic var email = "" 307 | let posts = List() 308 | 309 | static func fromJSONMapping() -> JSONMapping { 310 | return [ 311 | "posts": ArrayTransform() 312 | ] 313 | } 314 | } 315 | 316 | Api.find { user, response in 317 | println("User: \(user.email)") 318 | for post in user.posts { 319 | println("\(post.title)") 320 | } 321 | } 322 | ``` 323 | 324 | Default transforms are: 325 | 326 | - StringTransform 327 | - IntTransform 328 | - FloatTransform 329 | - DoubleTransform 330 | - BoolTransform 331 | - NSDateTransform 332 | - ModelTransform 333 | - ArrayTransform 334 | - PercentageTransform 335 | 336 | However, it is really easy to define your own. Go nuts! 337 | 338 | #### NSDateTransform 339 | 340 | Date and time parsing is always a bit complex and has a lot of subtle nuances. The `NSDateTransform` takes a string and tries to convert it into an `NSDate` object. If it can't it returns `nil`. 341 | 342 | Dates come in plenty of different formats. The default format `ApiModel` and `NSDateTransform` uses is called [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) 343 | 344 | ```swift 345 | class Post: Object, APIModel { 346 | class func fromJSONMapping() -> JSONMapping { 347 | return [ 348 | "createdAt": NSDateTransform() 349 | ] 350 | } 351 | } 352 | 353 | // Example of a valid string: 354 | // "2015-12-30T12:12:33.000Z" 355 | ``` 356 | 357 | In the real world you will come across many wildly different date formats and many APIs will have different opinions on how to represent a date. Therefor you can also pass in a custom format string into `NSDateTransform`: 358 | 359 | ```swift 360 | class Post: Object, APIModel { 361 | class func fromJSONMapping() -> JSONMapping { 362 | return [ 363 | "createdAt": NSDateTransform(dateFormat: "yyyy-MM-dd") 364 | ] 365 | } 366 | } 367 | 368 | // Example of a valid string: 369 | // "2015-12-30" 370 | ``` 371 | 372 | [For a complete reference on date format specifiers, visit the unicode reference.](http://unicode.org/reports/tr35/tr35-6.html#Date_Format_Patterns) 373 | 374 | ##### A note on time zones 375 | 376 | Internally `NSDate` stores the date as seconds from a specific reference date. That means it will not store any information about the time zone, __even if one is provided by the date string__. For example, let's assume this string is returned from the API: `2015-12-30T18:12:33.000-05:00`. `NSDate` only uses the provided offset to offset the resulting time correctly, then it is thrown away. 377 | 378 | What you need to do as an app developer is to make sure to always pass in the correct time zone when you wish to display the timestamp. Normally using an `NSDateFormatter`. By default `NSDateFormatter` uses the user time zone, so you should never need to worry about that. Check the following example for reference: 379 | 380 | ```swift 381 | let dateString = "2015-12-30T18:12:33.000-05:00" 382 | 383 | // Parse as ISO 8601 384 | let dateFormatter = NSDateFormatter() 385 | dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 386 | 387 | let date = dateFormatter.dateFromString(dateString)! 388 | 389 | let outputFormatter = NSDateFormatter() 390 | outputFormatter.dateFormat = "yyyy-MM-dd HH:mm" 391 | 392 | // prints "2015-12-30 23:12" 393 | outputFormatter.timeZone = NSTimeZone(abbreviation: "GMT") 394 | print(outputFormatter.stringFromDate(date)) 395 | 396 | // prints "2015-12-31 00:12" 397 | outputFormatter.timeZone = NSTimeZone(abbreviation: "Europe/Stockholm") // +02:00 398 | print(outputFormatter.stringFromDate(date)) 399 | 400 | // prints "2015-12-30 18:12" 401 | outputFormatter.timeZone = NSTimeZone(abbreviation: "EST") // -05:00 SAME AS INPUT 402 | print(outputFormatter.stringFromDate(date)) 403 | ``` 404 | 405 | Rule of thumb: You should only think about time zones when displaying `NSDate`s. 406 | 407 | ## Hooks 408 | 409 | `ApiModel` uses [Alamofire](https://github.com/alamofire/alamofire) for sending and receiving requests. To hook into this, the `API` class currently has `before`- and `after`-hooks that you can use to modify or log the requests. An example of sending user credentials with each request: 410 | 411 | ```swift 412 | // Put this somewhere in your AppDelegate or together with other initialization code 413 | api().beforeRequest { request in 414 | if let loginToken = User.loginToken() { 415 | request.parameters["access_token"] = loginToken 416 | } 417 | } 418 | ``` 419 | 420 | There is also a `afterRequest` which passes in a `ApiRequest` and `ApiResponse`: 421 | 422 | ```swift 423 | api().afterRequest { request, response in 424 | println("... Got: \(response.status)") 425 | println("... \(request)") 426 | println("... \(response)") 427 | } 428 | ``` 429 | 430 | ## URLs 431 | 432 | Given the setup for the `Post` model above, if you wanted to get the full url with replacements for the show resource (like `https://service.io/api/v1/posts/123.json`), you can use: 433 | 434 | ```swift 435 | post.apiUrlForRoute(Post.apiRoutes().show) 436 | // NOT IMPLEMENTED YET BECAUSE LIMITATIONS IN SWIFT: post.apiUrlForResource(.Show) 437 | ``` 438 | 439 | ## Dealing with IDs 440 | 441 | As a consumer of an API, you never want to make assumptions about the ID structure used for their models. Do not use `Int` or anything similar for ID types, strings are to be recommended. Therefor `ApiModel` defines a typealias to `String`, called ApiId. There is also an `ApiIdTransform` available for IDs. 442 | 443 | ## Namespaces and envelopes 444 | 445 | Some API's wrap all their responses in an "envelope", a container that is generic for all responses. For example an API might wrap all response data within a `data`-property of the root JSON: 446 | 447 | ```json 448 | { 449 | "data": { 450 | "user": { ... } 451 | } 452 | } 453 | ``` 454 | 455 | To deal with this gracefully there is a configuration option on the `ApiConfig` class called `rootNamespace`. This is a dot-separated path that is traversed for each response. To deal with the above example you would simply: 456 | 457 | ```swift 458 | let config = ApiConfig() 459 | config.rootNamespace = "data" 460 | ``` 461 | 462 | It can also be more complex, for example if the envelope looked something like this: 463 | 464 | ```json 465 | { 466 | "JsonResponseEnvelope": { 467 | "SuccessFullJsonResponse": { 468 | "SoapResponseContainer": { 469 | "EnterpriseBeanData": { 470 | "user": { ... } 471 | } 472 | } 473 | } 474 | } 475 | } 476 | ``` 477 | 478 | This would then convert into the `rootNamespace`: 479 | 480 | ```swift 481 | let config = ApiConfig() 482 | config.rootNamespace = "JsonResponseEnvelope.SuccessFullJsonResponse.SoapResponseContainer.EnterpriseBeanData" 483 | ``` 484 | 485 | ## Caching and storage 486 | 487 | It is up to you to cache and store the results of any calls. ApiModel does not do that for you, and will not do that, since strategies vary wildly depending on needs. 488 | 489 | ## File uploads 490 | 491 | Just as the `JSONDictionary` can return a dictionary of parameters to be sent to the server, it can also contain `NSData` values that can be uploaded to a server. For example you could convert `UIImage`s to `NSData` and upload them for profile images. 492 | 493 | The standard way of uploading files on the web is using the content-type `multipart/form-data`, which is slightly different from JSON. If you have a model that should be able to support file uploads, you can configure the model to encode it's `JSONDictionary` into `multipart/form-data`. 494 | 495 | The following example illustrates a `UserAvatar` model: 496 | 497 | ```swift 498 | import RealmSwift 499 | import ApiModel 500 | import UIKit 501 | 502 | class UserAvatar: Object, ApiModel, ApiConfigurable { 503 | dynamic var userId = ApiId() 504 | dynamic var url = String() // generated on the server 505 | 506 | var imageData: NSData? 507 | 508 | class func apiConfig(config: ApiConfig) -> ApiConfig { 509 | // ApiRequest.FormDataEncoding is where the magic happens 510 | // It tells ApiModel to encode everything with `multipart/form-data` 511 | config.encoding = ApiRequest.FormDataEncoding 512 | return config 513 | } 514 | 515 | // Important because the `imageData` property cannot be represented by Realm 516 | override class func ignoredProperties() -> [String] { 517 | return ["imageData"] 518 | } 519 | 520 | override class func primaryKey() -> String { 521 | return "userId" 522 | } 523 | 524 | class func apiNamespace() -> String { 525 | return "user_avatar" 526 | } 527 | 528 | class func apiRoutes() -> ApiRoutes { 529 | return ApiRoutes.resource("/user/avatar.json") 530 | } 531 | 532 | class func fromJSONMapping() -> JSONMapping { 533 | return [ 534 | "userId": ApiIdTransform(), 535 | "url": StringTransform() 536 | ] 537 | } 538 | 539 | func JSONDictionary() -> [String:Any] { 540 | return [ 541 | "image": FileUpload(fileName: "avatar.jpg", mimeType: "image/jpg", data: imageData!) 542 | ] 543 | } 544 | } 545 | 546 | func upload() { 547 | let image = UIImage(named: "me.jpg")! 548 | 549 | let userAvatar = UserAvatar() 550 | userAvatar.userId = "1" 551 | userAvatar.imageData = UIImageJPEGRepresentation(image, 1)! 552 | 553 | Api(model: userAvatar).save { form in 554 | if form.hasErrors { 555 | print("Could not upload file: " + form.errorMessages.joinWithSeparator("\n")) 556 | } else { 557 | print("File uploaded! URL: \(userAvatar.url)") 558 | } 559 | } 560 | } 561 | ``` 562 | 563 | ### FileUpload 564 | 565 | You can upload any file this way, not only images. Any NSData can be uploaded. The default mime type for uploaded files is `application/octet-stream`. If you need to configure this there is a special object you need to construct called `FileUpload`. 566 | 567 | The constructor is illustrated above, and takes the file name the server receives, mimetype of the file, and the data. 568 | 569 | ```swift 570 | FileUpload(fileName: "document.pdf", mimeType: "application/pdf", data: documentData) 571 | ``` 572 | 573 | This should be put in the `JSONDictionary` dictionary. ApiModel will detect it and encode it accordingly. 574 | 575 | # Thanks to 576 | 577 | - [idlefingers](https://www.github.com/idlefingers) 578 | - [Pluralize.swift](https://github.com/joshualat/Pluralize.swift) 579 | 580 | # License 581 | 582 | The MIT License (MIT) 583 | 584 | Copyright (c) 2015 Rootof Creations HB 585 | 586 | 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: 587 | 588 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 589 | 590 | 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. 591 | -------------------------------------------------------------------------------- /Source/ApiCall.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Alamofire 3 | 4 | public typealias RequestParameters = [String:Any] 5 | 6 | open class ApiCall { 7 | open var method: Alamofire.HTTPMethod 8 | open var path: String 9 | open var parameters: RequestParameters = [:] 10 | open var namespace: String = "" 11 | 12 | public required init(method: Alamofire.HTTPMethod, path: String) { 13 | self.method = method 14 | self.path = path 15 | } 16 | 17 | public convenience init(method: Alamofire.HTTPMethod, path: String, parameters: RequestParameters, namespace: String) { 18 | self.init(method: method, path: path) 19 | self.parameters = parameters 20 | self.namespace = namespace 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Source/ApiConfig.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | 3 | open class ApiConfig { 4 | open var host: String = "" 5 | open var parser: ApiParser = JSONParser() 6 | open var encoding: ParameterEncoding = URLEncoding.default 7 | open var requestLogging: Bool = true 8 | open var rootNamespace = "" 9 | open var urlSessionConfig: URLSessionConfiguration? 10 | 11 | public required init() { 12 | } 13 | 14 | public convenience init(host: String, urlSessionConfig: URLSessionConfiguration) { 15 | self.init() 16 | self.host = host 17 | self.urlSessionConfig = urlSessionConfig 18 | } 19 | 20 | public convenience init(host: String) { 21 | self.init() 22 | self.host = host 23 | } 24 | 25 | public convenience init(apiConfig: ApiConfig) { 26 | self.init() 27 | self.host = apiConfig.host 28 | self.parser = apiConfig.parser 29 | self.encoding = apiConfig.encoding 30 | self.requestLogging = apiConfig.requestLogging 31 | self.rootNamespace = apiConfig.rootNamespace 32 | self.urlSessionConfig = apiConfig.urlSessionConfig 33 | } 34 | 35 | open func copy() -> ApiConfig { 36 | return ApiConfig(apiConfig: self) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Source/ApiConfigurable.swift: -------------------------------------------------------------------------------- 1 | public protocol ApiConfigurable { 2 | static func apiConfig(_ config: ApiConfig) -> ApiConfig 3 | } 4 | -------------------------------------------------------------------------------- /Source/ApiForm.swift: -------------------------------------------------------------------------------- 1 | import RealmSwift 2 | import Alamofire 3 | 4 | public enum ApiModelStatus { 5 | case none 6 | case successful(Int) 7 | case unauthorized(Int) 8 | case invalid(Int) 9 | case serverError(Int) 10 | 11 | init(statusCode: Int) { 12 | if statusCode >= 200 && statusCode <= 299 { 13 | self = .successful(statusCode) 14 | } else if statusCode == 401 { 15 | self = .unauthorized(statusCode) 16 | } else if statusCode >= 400 && statusCode <= 499 { 17 | self = .invalid(statusCode) 18 | } else if statusCode >= 500 && statusCode <= 599 { 19 | self = .serverError(statusCode) 20 | } else { 21 | self = .none 22 | } 23 | } 24 | } 25 | 26 | open class ApiModelResponse where ModelType:ApiModel { 27 | open var responseData: [String:Any]? 28 | open var responseObject: [String:Any]? 29 | open var responseArray: [Any]? 30 | open var errors: [String:[String]]? 31 | open var rawResponse: ApiResponse? 32 | 33 | open var isSuccessful: Bool { 34 | for (_, errorsForKey) in errors ?? [:] { 35 | if !errorsForKey.isEmpty { 36 | return false 37 | } 38 | } 39 | return true 40 | } 41 | 42 | open var responseStatus: ApiModelStatus { 43 | if let status = rawResponse?.status { 44 | return ApiModelStatus(statusCode: status) 45 | } else { 46 | return .none 47 | } 48 | } 49 | 50 | public var errorMessages: [String]? { 51 | if let errors = errors { 52 | var messages: [String] = [] 53 | for nestedErrors in errors.values { 54 | messages.append(contentsOf: nestedErrors) 55 | } 56 | return messages 57 | } else { 58 | return nil 59 | } 60 | } 61 | 62 | var _object: ModelType? 63 | var _parsedObject = false 64 | 65 | open var object: ModelType? { 66 | if _parsedObject { 67 | return _object 68 | } 69 | 70 | _parsedObject = true 71 | 72 | if let responseObject = responseObject { 73 | _object = fromApi(responseObject) 74 | } 75 | 76 | return _object 77 | } 78 | 79 | var _array: [ModelType]? = nil 80 | var _parsedArray = false 81 | 82 | open var array: [ModelType]? { 83 | if _parsedArray { 84 | return _array 85 | } 86 | _parsedArray = true 87 | 88 | if let arrayData = responseArray { 89 | _array = [] 90 | 91 | for modelData in arrayData { 92 | if let modelDictionary = modelData as? [String:Any] { 93 | _array!.append(fromApi(modelDictionary)) 94 | } 95 | } 96 | } 97 | 98 | return _array 99 | } 100 | 101 | func fromApi(_ apiResponse: [String:Any]) -> ModelType { 102 | let newModel = ModelType() 103 | newModel.updateFromDictionary(apiResponse) 104 | return newModel 105 | } 106 | } 107 | 108 | open class Api where ModelType:ApiModel { 109 | public typealias ResponseCallback = (ApiModelResponse) -> Void 110 | 111 | open var apiConfig: ApiConfig 112 | open var status: ApiModelStatus = .none 113 | open var errors: [String:[String]] = [:] 114 | open var model: ModelType 115 | 116 | open var errorMessages:[String] { 117 | var errorString: [String] = [] 118 | for (key, errorsForProperty) in errors { 119 | for message in errorsForProperty { 120 | if key == "base" { 121 | errorString.append(message) 122 | } else { 123 | errorString.append("\(key.capitalized) \(message)") 124 | } 125 | } 126 | } 127 | return errorString 128 | } 129 | 130 | open var hasErrors: Bool { 131 | return !errors.isEmpty 132 | } 133 | 134 | public required init(model: ModelType, apiConfig: ApiConfig) { 135 | self.model = model 136 | self.apiConfig = apiConfig 137 | } 138 | 139 | public convenience init(model: ModelType) { 140 | self.init(model: model, apiConfig: type(of: self).apiConfigForType()) 141 | } 142 | 143 | public static func apiConfigForType() -> ApiConfig { 144 | if let configurable = ModelType.self as? ApiConfigurable.Type { 145 | return configurable.apiConfig(apiManager().config.copy()) 146 | } else { 147 | return apiManager().config 148 | } 149 | } 150 | 151 | open func updateFromForm(_ formParameters: NSDictionary) { 152 | model.modifyStoredObject { 153 | self.model.updateFromDictionary(formParameters as! [String:Any]) 154 | } 155 | } 156 | 157 | open func updateFromResponse(_ response: ApiModelResponse) { 158 | if let statusCode = response.rawResponse?.status { 159 | self.status = ApiModelStatus(statusCode: statusCode) 160 | } 161 | 162 | if let responseObject = response.responseObject { 163 | model.modifyStoredObject { 164 | self.model.updateFromDictionary(responseObject) 165 | } 166 | } 167 | 168 | if let errors = response.errors { 169 | self.errors = errors 170 | } 171 | } 172 | 173 | // api-model style methods 174 | 175 | open class func performWithMethod(_ method: Alamofire.HTTPMethod, path: String, parameters: RequestParameters, apiConfig: ApiConfig, callback: ResponseCallback?) { 176 | let call = ApiCall(method: method, path: path, parameters: parameters, namespace: ModelType.apiNamespace()) 177 | perform(call, apiConfig: apiConfig, callback: callback) 178 | } 179 | 180 | // GET 181 | open class func get(_ path: String, parameters: RequestParameters, apiConfig: ApiConfig, callback: ResponseCallback?) { 182 | performWithMethod(.get, path: path, parameters: parameters, apiConfig: apiConfig, callback: callback) 183 | } 184 | 185 | open class func get(_ path: String, parameters: RequestParameters, callback: ResponseCallback?) { 186 | get(path, parameters: parameters, apiConfig: apiConfigForType(), callback: callback) 187 | } 188 | 189 | open class func get(_ path: String, callback: ResponseCallback?) { 190 | get(path, parameters: [:], callback: callback) 191 | } 192 | 193 | // POST 194 | open class func post(_ path: String, parameters: RequestParameters, apiConfig: ApiConfig, callback: ResponseCallback?) { 195 | performWithMethod(.post, path: path, parameters: parameters, apiConfig: apiConfig, callback: callback) 196 | } 197 | 198 | open class func post(_ path: String, parameters: RequestParameters, callback: ResponseCallback?) { 199 | post(path, parameters: parameters, apiConfig: apiConfigForType(), callback: callback) 200 | } 201 | 202 | open class func post(_ path: String, callback: ResponseCallback?) { 203 | post(path, parameters: [:], callback: callback) 204 | } 205 | 206 | // DELETE 207 | open class func delete(_ path: String, parameters: RequestParameters, apiConfig: ApiConfig, callback: ResponseCallback?) { 208 | performWithMethod(.delete, path: path, parameters: parameters, apiConfig: apiConfig, callback: callback) 209 | } 210 | 211 | open class func delete(_ path: String, parameters: RequestParameters, callback: ResponseCallback?) { 212 | delete(path, parameters: parameters, apiConfig: apiConfigForType(), callback: callback) 213 | } 214 | 215 | open class func delete(_ path: String, callback: ResponseCallback?) { 216 | delete(path, parameters: [:], callback: callback) 217 | } 218 | 219 | // PUT 220 | open class func put(_ path: String, parameters: RequestParameters, apiConfig: ApiConfig, callback: ResponseCallback?) { 221 | performWithMethod(.put, path: path, parameters: parameters, apiConfig: apiConfig, callback: callback) 222 | } 223 | 224 | open class func put(_ path: String, parameters: RequestParameters, callback: ResponseCallback?) { 225 | put(path, parameters: parameters, apiConfig: apiConfigForType(), callback: callback) 226 | } 227 | 228 | open class func put(_ path: String, callback: ResponseCallback?) { 229 | put(path, parameters: [:], callback: callback) 230 | } 231 | 232 | // active record (rails) style methods 233 | 234 | open class func find(_ callback: @escaping (ModelType?, ApiModelResponse) -> Void) { 235 | get(ModelType.apiRoutes().index) { response in 236 | callback(response.object, response) 237 | } 238 | } 239 | 240 | open class func findArray(_ callback: @escaping ([ModelType], ApiModelResponse) -> Void) { 241 | findArray(ModelType.apiRoutes().index, callback: callback) 242 | } 243 | 244 | open class func findArray(_ path: String, callback: @escaping ([ModelType], ApiModelResponse) -> Void) { 245 | get(path) { response in 246 | callback(response.array ?? [], response) 247 | } 248 | } 249 | 250 | open class func create(_ parameters: RequestParameters, callback: @escaping (ModelType?, ApiModelResponse) -> Void) { 251 | post(ModelType.apiRoutes().create, parameters: parameters) { response in 252 | callback(response.object, response) 253 | } 254 | } 255 | 256 | open class func update(_ parameters: RequestParameters, callback: @escaping (ModelType?, ApiModelResponse) -> Void) { 257 | put(ModelType.apiRoutes().update, parameters: parameters) { response in 258 | callback(response.object, response) 259 | } 260 | } 261 | 262 | open func save(_ callback: @escaping (Api) -> Void) { 263 | let parameters: [String: Any] = [ 264 | ModelType.apiNamespace(): model.JSONDictionary() as Any 265 | ] 266 | 267 | let responseCallback: ResponseCallback = { response in 268 | self.updateFromResponse(response) 269 | callback(self) 270 | } 271 | 272 | if model.isApiSaved() { 273 | type(of: self).put(model.apiRouteWithReplacements(ModelType.apiRoutes().update), parameters: parameters as RequestParameters, callback: responseCallback) 274 | } else { 275 | type(of: self).post(model.apiRouteWithReplacements(ModelType.apiRoutes().create), parameters: parameters as RequestParameters, callback: responseCallback) 276 | } 277 | } 278 | 279 | open func destroy(_ callback: @escaping (Api) -> Void) { 280 | destroy([:], callback: callback) 281 | } 282 | 283 | open func destroy(_ parameters: RequestParameters, callback: @escaping (Api) -> Void) { 284 | type(of: self).delete(model.apiRouteWithReplacements(ModelType.apiRoutes().destroy), parameters: parameters) { response in 285 | self.updateFromResponse(response) 286 | callback(self) 287 | } 288 | } 289 | 290 | open class func perform(_ call: ApiCall, apiConfig: ApiConfig, callback: ResponseCallback?) { 291 | apiManager().request( 292 | call.method, 293 | path: call.path, 294 | parameters: call.parameters, 295 | apiConfig: apiConfig 296 | ) { data, error in 297 | let response = ApiModelResponse() 298 | response.rawResponse = data 299 | 300 | if let errors = self.errorFromResponse(nil, error: error) { 301 | response.errors = errors 302 | } 303 | 304 | if let data: Any = data?.parsedResponse { 305 | response.responseData = data as? [String:Any] 306 | 307 | if let responseObject = self.objectFromResponseForNamespace(data, namespace: call.namespace) { 308 | response.responseObject = responseObject 309 | 310 | if let errors = self.errorFromResponse(responseObject, error: error) { 311 | response.errors = errors 312 | } 313 | } else if let arrayData = self.arrayFromResponseForNamespace(data, namespace: call.namespace) { 314 | response.responseArray = arrayData 315 | } 316 | } 317 | 318 | callback?(response) 319 | } 320 | } 321 | 322 | fileprivate class func objectFromResponseForNamespace(_ data: Any, namespace: String) -> [String:Any]? { 323 | if let asMap = data as? [String:Any] { 324 | return (asMap[namespace] as? [String:Any]) ?? (asMap[namespace.pluralize()] as? [String:Any]) 325 | } else { 326 | return nil 327 | } 328 | } 329 | 330 | fileprivate class func arrayFromResponseForNamespace(_ data: Any, namespace: String) -> [Any]? { 331 | if let asMap = data as? [String:Any] { 332 | return (asMap[namespace] as? [Any]) ?? (asMap[namespace.pluralize()] as? [Any]) 333 | } else { 334 | return nil 335 | } 336 | } 337 | 338 | fileprivate class func errorFromResponse(_ response: [String:Any]?, error: ApiResponseError?) -> [String:[String]]? { 339 | if let errors = response?["errors"] as? [String:[String]] { 340 | return errors 341 | } else if let errors = response?["errors"] as? [String] { 342 | return ["base": errors] 343 | } else if error != nil { 344 | return ["base": ["An unexpected server error occurred"]] 345 | } else { 346 | return nil 347 | } 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /Source/ApiId.swift: -------------------------------------------------------------------------------- 1 | public typealias ApiId = String 2 | 3 | open class ApiIdTransform: StringTransform { 4 | } 5 | 6 | // Since we have decided to store all IDs as strings we need to sometimes convert API responses to IDs. 7 | public func convertToApiId(_ anything: Any?) -> ApiId? { 8 | if let intId = anything as? Int { 9 | return String(intId) 10 | } else if let stringId = anything as? String { 11 | return stringId 12 | } else { 13 | return nil 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Source/ApiManager.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Alamofire 3 | 4 | open class ApiManager { 5 | open var config: ApiConfig 6 | 7 | open var beforeRequestHooks: [((ApiRequest) -> Void)] = [] 8 | open var afterRequestHooks: [((ApiRequest, ApiResponse) -> Void)] = [] 9 | 10 | fileprivate var alamoFireManager : Alamofire.SessionManager 11 | 12 | public init(config: ApiConfig) { 13 | self.config = config 14 | 15 | if let sessionConfig = config.urlSessionConfig { 16 | self.alamoFireManager = Alamofire.SessionManager(configuration: sessionConfig) 17 | }else{ 18 | self.alamoFireManager = Alamofire.SessionManager.default 19 | } 20 | 21 | beforeRequest { request in 22 | if self.config.requestLogging { 23 | request.userInfo["requestStartedAt"] = Date() as Any? 24 | 25 | print("ApiModel: \(request.method) \(request.path)") 26 | } 27 | } 28 | 29 | afterRequest { request, response in 30 | if self.config.requestLogging { 31 | let duration: String 32 | if let requestStartedAt = request.userInfo["requestStartedAt"] as? Date { 33 | let formatter = NumberFormatter() 34 | formatter.minimumFractionDigits = 2 35 | formatter.maximumFractionDigits = 2 36 | formatter.minimumIntegerDigits = 1 37 | 38 | let requestDuration = Date().timeIntervalSince(requestStartedAt) 39 | duration = formatter.string(from: requestDuration as NSNumber) ?? "\(requestDuration)" 40 | } else { 41 | duration = "?" 42 | } 43 | 44 | print("ApiModel: \(request.method) \(request.path) finished in \(duration) seconds with status \(response.status ?? 0)") 45 | 46 | if let error = response.error { 47 | print("... Error \(error.description())") 48 | } 49 | } 50 | } 51 | } 52 | 53 | open func request( 54 | _ method: Alamofire.HTTPMethod, 55 | path: String, 56 | parameters: [String: Any] = [:], 57 | headers: [String: String] = [:], 58 | apiConfig: ApiConfig, 59 | responseHandler: @escaping (ApiResponse?, ApiResponseError?) -> Void 60 | ) { 61 | let parser = apiConfig.parser 62 | 63 | let request = ApiRequest( 64 | config: apiConfig, 65 | method: method, 66 | path: path 67 | ) 68 | 69 | request.parameters = parameters 70 | request.headers = headers 71 | 72 | for hook in beforeRequestHooks { 73 | hook(request) 74 | } 75 | 76 | performRequest(request) { response in 77 | parser.parse(response.responseBody ?? "") { parsedResponse in 78 | let (finalResponse, errors) = self.handleResponse( 79 | response, 80 | parsedResponse: parsedResponse, 81 | apiConfig: apiConfig 82 | ) 83 | 84 | responseHandler(finalResponse, errors) 85 | } 86 | } 87 | } 88 | 89 | open func handleResponse( 90 | _ response: ApiResponse, 91 | parsedResponse: Any?, 92 | apiConfig: ApiConfig 93 | ) -> (ApiResponse?, ApiResponseError?) { 94 | // if response is either nil or NSNull and the request was not 200 it is an error 95 | if (parsedResponse == nil || (parsedResponse as? NSNull) != nil) && !response.isSuccessful { 96 | response.error = ApiResponseError.badRequest(code: response.status ?? 0) 97 | } 98 | 99 | if response.isInvalid { 100 | response.error = ApiResponseError.invalidRequest(code: response.status ?? 0) 101 | } 102 | 103 | response.parsedResponse = parsedResponse 104 | if let nestedResponse = parsedResponse as? [String:Any], !apiConfig.rootNamespace.isEmpty { 105 | response.parsedResponse = fetchPathFromDictionary(apiConfig.rootNamespace, dictionary: nestedResponse) 106 | } else { 107 | response.parsedResponse = parsedResponse 108 | } 109 | 110 | return (response, response.error) 111 | } 112 | 113 | func performRequest(_ request: ApiRequest, responseHandler: @escaping (ApiResponse) -> Void) { 114 | let response = ApiResponse(request: request) 115 | 116 | self.alamoFireManager.request( 117 | request.url, 118 | method: request.method, 119 | parameters: request.parameters, 120 | encoding: request.encoding, 121 | headers: request.headers 122 | ) 123 | .responseString { alamofireResponse in 124 | response.responseBody = alamofireResponse.result.value 125 | if let error = alamofireResponse.result.error { 126 | response.error = ApiResponseError.serverError(error) 127 | } 128 | response.status = alamofireResponse.response?.statusCode 129 | 130 | for hook in self.afterRequestHooks { 131 | hook(request, response) 132 | } 133 | 134 | responseHandler(response) 135 | } 136 | } 137 | 138 | open func beforeRequest(_ hook: @escaping ((ApiRequest) -> Void)) { 139 | beforeRequestHooks.insert(hook, at: 0) 140 | } 141 | 142 | open func afterRequest(_ hook: @escaping ((ApiRequest, ApiResponse) -> Void)) { 143 | afterRequestHooks.append(hook) 144 | } 145 | } 146 | 147 | public struct ApiSingleton { 148 | public static var instance: ApiManager = ApiManager(config: ApiConfig()) 149 | 150 | public static func setInstance(_ apiInstance: ApiManager) { 151 | instance = apiInstance 152 | } 153 | } 154 | 155 | public func apiManager() -> ApiManager { 156 | return ApiSingleton.instance 157 | } 158 | -------------------------------------------------------------------------------- /Source/ApiModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public typealias JSONMapping = [String:Transform] 4 | 5 | public protocol ApiModel { 6 | static func apiNamespace() -> String 7 | static func apiRoutes() -> ApiRoutes 8 | static func fromJSONMapping() -> JSONMapping 9 | func JSONDictionary() -> [String:Any] 10 | } 11 | -------------------------------------------------------------------------------- /Source/ApiParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiParser.swift 3 | // ApiModel 4 | // 5 | // Created by Erik Rothoff Andersson on 01/06/15. 6 | // 7 | // 8 | 9 | import Foundation 10 | 11 | public protocol ApiParser { 12 | func parse(_ responseString: String, completionHandler: @escaping (AnyObject?) -> Void) 13 | } 14 | -------------------------------------------------------------------------------- /Source/ApiRequest.swift: -------------------------------------------------------------------------------- 1 | import Alamofire 2 | 3 | open class ApiRequest { 4 | open var config: ApiConfig 5 | open var method: Alamofire.HTTPMethod 6 | open var path: String 7 | open var parameters: [String:Any] = [:] 8 | open var headers: [String:String] = [:] 9 | open var userInfo: [String:Any] = [:] 10 | 11 | open var encoding: ParameterEncoding { 12 | return config.encoding 13 | } 14 | 15 | public init(config: ApiConfig, method: Alamofire.HTTPMethod, path: String) { 16 | self.config = config 17 | self.method = method 18 | self.path = path 19 | } 20 | 21 | open var url: String { 22 | if NSURL(string: path)?.scheme?.isEmpty ?? true { 23 | return config.host + path 24 | } else { 25 | return path 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Source/ApiResponse.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ApiResponseError: Error { 4 | case none 5 | case parseError 6 | case badRequest(code: Int) 7 | case invalidRequest(code: Int) 8 | case serverError(Error) 9 | 10 | func description() -> String { 11 | switch self { 12 | case .none: 13 | return "" 14 | case .parseError: 15 | return "An error occurred when parsing the response" 16 | case .badRequest(let code): 17 | return "Bad request according from server. HTTP Code: \(code)" 18 | case .invalidRequest(let code): 19 | return "Server could not parse request. HTTP Code: \(code)" 20 | case .serverError(let res): 21 | let err = (res as NSError).description 22 | return "A server error occurred. \(err)" 23 | } 24 | 25 | } 26 | } 27 | 28 | open class ApiResponse { 29 | open var request: ApiRequest 30 | open var responseBody: String? 31 | open var error: ApiResponseError? 32 | open var status: Int? 33 | open var parsedResponse: Any? 34 | 35 | open var isSuccessful: Bool { 36 | if let status = status { 37 | return status >= 200 && status <= 299 38 | } else { 39 | return false 40 | } 41 | } 42 | 43 | open var isInvalid: Bool { 44 | if let status = status { 45 | return status >= 400 && status <= 499 46 | } else { 47 | return true 48 | } 49 | } 50 | 51 | public init(request: ApiRequest) { 52 | self.request = request 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Source/ApiRoutes.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum ApiRoutesAction { 4 | case index 5 | case show 6 | case create 7 | case update 8 | case destroy 9 | } 10 | 11 | 12 | open class ApiRoutes { 13 | open var index: String 14 | open var show: String 15 | open var create: String 16 | 17 | open var update: String { 18 | get { 19 | return show 20 | } 21 | } 22 | 23 | open var destroy: String { 24 | get { 25 | return show 26 | } 27 | } 28 | 29 | open class func resource(_ resourcePath: String) -> ApiRoutes { 30 | return ApiRoutes(index: resourcePath, show: resourcePath) 31 | } 32 | 33 | public init(index: String, show: String, create: String) { 34 | self.index = index 35 | self.show = show 36 | self.create = create 37 | } 38 | 39 | public convenience init(index: String, show: String) { 40 | self.init(index: index, show: show, create: index) 41 | } 42 | 43 | public convenience init() { 44 | self.init(index: "NO RESOURCE DEFINED", show: "NO RESOURCE DEFINED", create: "NO RESOURCE DEFINED") 45 | } 46 | 47 | open func getAction(_ resourceAction: ApiRoutesAction) -> String { 48 | switch resourceAction { 49 | case .index: 50 | return index 51 | case .create: 52 | return create 53 | case .show: 54 | return show 55 | case .update: 56 | return update 57 | case .destroy: 58 | return destroy 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Source/ArrayTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class ArrayTransform: Transform where T:ApiModel { 5 | let nestedModelTransform = ModelTransform() 6 | 7 | public init() {} 8 | 9 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 10 | var models: [T] = [] 11 | 12 | if let values = value as? [Any] { 13 | for value in values { 14 | if let nestedData = value as? [String:Any], 15 | let model = nestedModelTransform.perform(nestedData as AnyObject, realm: realm) as? T 16 | { 17 | models.append(model) 18 | } 19 | } 20 | } 21 | 22 | return models 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Source/BoolTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class BoolTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let stringValue = (value as AnyObject?)?.stringValue { 9 | switch stringValue.lowercased() { 10 | case "true": return true 11 | case "1": return true 12 | case "false": return false 13 | case "0": return false 14 | default: return false 15 | } 16 | } 17 | 18 | if let integerValue = (value as AnyObject?)?.int64Value { 19 | if integerValue == 0 { 20 | return false as AnyObject 21 | } else { 22 | return true as AnyObject 23 | } 24 | } 25 | 26 | if value == nil { 27 | return false as AnyObject 28 | } 29 | 30 | return true as AnyObject 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Source/DefaultTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class DefaultTransform: Transform { 5 | var defaultValue: Any 6 | 7 | public init(defaultValue: Any) { 8 | self.defaultValue = defaultValue 9 | } 10 | 11 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 12 | if let value = value { 13 | return value 14 | } else { 15 | return defaultValue 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/DoubleTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class DoubleTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let doubleValue = value as? Double { 9 | return doubleValue 10 | } else if let stringValue = value as? String { 11 | return (stringValue as NSString).doubleValue 12 | } else { 13 | return 0 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/FloatTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class FloatTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let asFloat = (value as AnyObject?)?.floatValue { 9 | return asFloat 10 | } else { 11 | return 0 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/FormDataEncoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FormDataEncoding.swift 3 | // ApiModel 4 | // 5 | // Created by Erik Rothoff Andersson on 2015-26-09. 6 | // 7 | // 8 | 9 | import Foundation 10 | import Alamofire 11 | 12 | open class FileUpload { 13 | open var fileName: String 14 | open var mimeType: String 15 | open var data: Data 16 | 17 | public init(fileName: String, mimeType: String, data: Data) { 18 | self.fileName = fileName 19 | self.mimeType = mimeType 20 | self.data = data 21 | } 22 | } 23 | 24 | open class FormDataEncoding: ParameterEncoding { 25 | public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest { 26 | var request = try urlRequest.asURLRequest() 27 | 28 | let formData = MultipartFormData() 29 | 30 | addParametersToData(parameters ?? [:], formData: formData) 31 | 32 | let fullData = try formData.encode() 33 | 34 | request.setValue(formData.contentType, forHTTPHeaderField: "Content-Type") 35 | request.setValue(String(formData.contentLength), forHTTPHeaderField: "Content-Length") 36 | 37 | request.httpBody = fullData 38 | request.httpShouldHandleCookies = true 39 | 40 | return request 41 | } 42 | } 43 | 44 | func addParametersToData(_ parameters: [String:Any], formData: MultipartFormData, keyPrefix: String = "") { 45 | for (key, value) in parameters { 46 | let formKey = keyPrefix.isEmpty ? key : "\(keyPrefix)[\(key)]" 47 | 48 | // FileUpload 49 | if let fileUpload = value as? FileUpload { 50 | formData.append(fileUpload.data, withName: formKey, fileName: fileUpload.fileName, mimeType: fileUpload.mimeType) 51 | // NSData 52 | } else if let valueData = value as? Data { 53 | formData.append(valueData, withName: formKey, fileName: "data.dat", mimeType: "application/octet-stream") 54 | // Nested hash 55 | } else if let nestedParameters = value as? [String:AnyObject] { 56 | addParametersToData(nestedParameters, formData: formData, keyPrefix: formKey) 57 | // Nested array 58 | } else if let arrayData = value as? [AnyObject] { 59 | var asHash: [String:AnyObject] = [:] 60 | 61 | for (index, arrayValue) in arrayData.enumerated() { 62 | asHash[String(index)] = arrayValue 63 | } 64 | 65 | addParametersToData(asHash, formData: formData, keyPrefix: formKey) 66 | // Anything else, cast it to a string 67 | } else if let dataString = String(describing: value).data(using: String.Encoding.utf8) { 68 | formData.append(dataString, withName: key) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Source/IntTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class IntTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let asInt = (value as AnyObject).int64Value { 9 | return asInt 10 | } else { 11 | return 0 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/JSONParser.swift: -------------------------------------------------------------------------------- 1 | // 2 | // JSONParser.swift 3 | // ApiModel 4 | // 5 | // Created by Erik Rothoff Andersson on 01/06/15. 6 | // 7 | // 8 | 9 | import Foundation 10 | import SwiftyJSON 11 | 12 | open class JSONParser: ApiParser { 13 | open func parse(_ responseString: String, completionHandler: @escaping (AnyObject?) -> Void) { 14 | DispatchQueue.global(qos: DispatchQoS.QoSClass.default).async(execute: { 15 | 16 | var responseJSON: JSON 17 | if responseString.isEmpty { 18 | responseJSON = JSON.null 19 | } else { 20 | if let data = (responseString as NSString).data(using: String.Encoding.utf8.rawValue) { 21 | do { 22 | responseJSON = try SwiftyJSON.JSON(data: data) 23 | } catch { 24 | responseJSON = JSON.null 25 | } 26 | } else { 27 | responseJSON = JSON.null 28 | } 29 | } 30 | 31 | DispatchQueue.main.async(execute: { 32 | if let dictionary = responseJSON.dictionaryObject { 33 | completionHandler(dictionary as AnyObject?) 34 | } else if let array = responseJSON.arrayObject { 35 | completionHandler(array as AnyObject?) 36 | } else { 37 | completionHandler(nil) 38 | } 39 | }) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Source/ModelTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class ModelTransform: Transform where T:ApiModel { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let value = value as? [String:Any] { 9 | let model: T 10 | 11 | if let pk = T.primaryKey(), 12 | let pkValue = convertToApiId(value[pk]), 13 | let realm = realm, 14 | let alreadyPersistedModel = realm.object(ofType: T.self, forPrimaryKey: pkValue as Any) 15 | { 16 | model = alreadyPersistedModel 17 | } else { 18 | model = T() 19 | } 20 | 21 | let mapping = T.fromJSONMapping() 22 | updateRealmObjectFromDictionaryWithMapping(model, data: value, mapping: mapping, originRealm: realm) 23 | 24 | // If a realm is passed in we need to make sure to add the model in an update: true transaction, otherwise issues might happen down the road if the same object is present multiple times in a large nested response 25 | if let realm = realm { 26 | realm.add(model, update: .all) 27 | } 28 | 29 | return model 30 | } else { 31 | return T() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Source/NSDateTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | let standardTimeZone = TimeZone(secondsFromGMT: 0) 5 | 6 | open class DateTransform: Transform { 7 | var dateFormatters: [DateFormatter] = [] 8 | 9 | public init() { 10 | // ISO 8601 dates with time and zone 11 | let iso8601Formatter = DateFormatter() 12 | iso8601Formatter.timeZone = standardTimeZone 13 | iso8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ" 14 | 15 | dateFormatters.append(iso8601Formatter) 16 | } 17 | 18 | public init(dateFormat: String) { 19 | let userDefinedDateFormatter = DateFormatter() 20 | userDefinedDateFormatter.timeZone = standardTimeZone 21 | userDefinedDateFormatter.dateFormat = dateFormat 22 | 23 | dateFormatters.insert(userDefinedDateFormatter, at: 0) 24 | } 25 | 26 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 27 | if let dateValue = value as? Date { 28 | return dateValue 29 | } 30 | 31 | if let stringValue = value as? String { 32 | for formatter in dateFormatters { 33 | if let date = formatter.date(from: stringValue) { 34 | return date 35 | } 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Source/Object+ApiModel.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | extension Object { 5 | public class func localId() -> ApiId { 6 | return "APIMODELLOCAL-\(NSUUID().uuidString)" 7 | } 8 | 9 | public var isLocal: Bool { 10 | if let pk = type(of: self).primaryKey() { 11 | if let id = self[pk] as? NSString { 12 | return id.range(of: "APIMODELLOCAL-").location == 0 13 | } 14 | } 15 | 16 | return false 17 | } 18 | 19 | public var unlocalId: ApiId { 20 | if isLocal { 21 | return "" 22 | } else if let pk = type(of: self).primaryKey(), 23 | let id = convertToApiId(self[pk] as AnyObject) 24 | { 25 | return id 26 | } else { 27 | return "" 28 | } 29 | } 30 | 31 | public func isApiSaved() -> Bool { 32 | if let pk = type(of: self).primaryKey(), 33 | let idValue = convertToApiId(self[pk] as AnyObject) 34 | { 35 | return !idValue.isEmpty 36 | } else { 37 | return false 38 | } 39 | } 40 | 41 | public func modifyStoredObject(_ modifyingBlock: () -> ()) { 42 | if let realm = realm { 43 | // This is up for discussion, ideally the user should handle this, but in the short term would require too much error logic 44 | try! realm.write(modifyingBlock) 45 | } else { 46 | modifyingBlock() 47 | } 48 | } 49 | 50 | public func saveStoredObject() { 51 | modifyStoredObject {} 52 | } 53 | 54 | public func removeEmpty(_ fieldsToRemoveIfEmpty: [String], data: [String:AnyObject]) -> [String:AnyObject] { 55 | var data = data 56 | for field in fieldsToRemoveIfEmpty { 57 | if data[field] == nil { 58 | data.removeValue(forKey: field) 59 | } else if let value = data[field] as? String, value.isEmpty { 60 | data.removeValue(forKey: field) 61 | } 62 | } 63 | return data 64 | } 65 | 66 | public func apiRouteWithReplacements(_ url: String) -> String { 67 | var pieces = url.components(separatedBy: ":") 68 | 69 | var pathComponents: [String] = [] 70 | while pieces.count > 0 { 71 | pathComponents.append(pieces.remove(at: 0)) 72 | if pieces.count == 0 { 73 | break 74 | } 75 | 76 | let methodName = pieces.remove(at: 0) 77 | if let value = self[methodName] { 78 | pathComponents.append((value as AnyObject).description) 79 | } 80 | } 81 | 82 | return pathComponents.joined(separator: "") 83 | } 84 | 85 | public func apiUrlForRoute(_ resource: String) -> String { 86 | return "\(apiManager().config.host)\(apiRouteWithReplacements(resource))" 87 | } 88 | 89 | public func apiUrlForRoute(_ resource: ApiRoutesAction) -> String { 90 | let apiRoutes = type(of: (type(of: self) as! ApiModel)).apiRoutes() 91 | return apiUrlForRoute(apiRoutes.getAction(resource)) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Source/Object+JSONDictionary.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | func camelizedString(_ string: String) -> String { 5 | var items: [String] = string.components(separatedBy: "_") 6 | var camelCase = items.remove(at: 0) 7 | for item: String in items { 8 | camelCase += item.capitalized 9 | } 10 | return camelCase 11 | } 12 | 13 | func updateRealmObjectFromDictionaryWithMapping(_ realmObject: Object, data: [String:Any], mapping: JSONMapping, originRealm: Realm?) { 14 | for (var key, value) in data { 15 | key = camelizedString(key) 16 | 17 | if let transform = mapping[key] { 18 | var optionalValue: AnyObject? = value as AnyObject? 19 | 20 | if (value as AnyObject).isKind(of: NSNull.self) { 21 | optionalValue = nil 22 | } 23 | 24 | let transformedValue = transform.perform(optionalValue, realm: originRealm) 25 | 26 | if let primaryKey = type(of: realmObject).primaryKey(), 27 | let modelsPrimaryKey = convertToApiId(realmObject[primaryKey] as AnyObject?), 28 | let responsePrimaryKey = convertToApiId(transformedValue), key == primaryKey && !modelsPrimaryKey.isEmpty 29 | { 30 | if modelsPrimaryKey == responsePrimaryKey { 31 | continue 32 | } else { 33 | print("APIMODEL WARNING: Api responded with different ID than stored. Changing this crashes Realm. Skipping (Tried to change \(modelsPrimaryKey) to \(responsePrimaryKey))") 34 | continue 35 | } 36 | } 37 | 38 | realmObject[key] = transformedValue 39 | } 40 | } 41 | } 42 | 43 | extension Object { 44 | public func updateFromForm(_ data: NSDictionary) { 45 | let mapping = type(of: (self as! ApiModel)).fromJSONMapping() 46 | updateFromDictionaryWithMapping(data as! [String:AnyObject], mapping: mapping) 47 | } 48 | 49 | // We need to pass in JSONMapping manually because of problems in Swift 50 | // It's impossible to cast Objects to "Object that conforms to ApiModel" currently... 51 | public func updateFromForm(_ data: NSDictionary, mapping: JSONMapping) { 52 | updateFromDictionaryWithMapping(data as! [String:AnyObject], mapping: mapping) 53 | } 54 | 55 | public func updateFromDictionary(_ data: [String:Any]) { 56 | let mapping = type(of: (self as! ApiModel)).fromJSONMapping() 57 | updateRealmObjectFromDictionaryWithMapping(self, data: data, mapping: mapping, originRealm: realm) 58 | } 59 | 60 | public func updateFromDictionaryWithMapping(_ data: [String:AnyObject], mapping: JSONMapping) { 61 | updateRealmObjectFromDictionaryWithMapping(self, data: data, mapping: mapping, originRealm: realm) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Source/PercentageTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class PercentageTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if let intValue = value as? Int { 9 | return Float(intValue) / Float(100.0) 10 | } 11 | if let stringValue = value as? String { 12 | return (stringValue as NSString).floatValue / Float(100.0) 13 | } 14 | return nil 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Source/StringTransform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class StringTransform: Transform { 5 | public init() {} 6 | 7 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 8 | if value is String { 9 | return value! 10 | } else if let intValue = value as? Int { 11 | return String(intValue) 12 | } else if let stringValue = (value as AnyObject?)?.stringValue { 13 | return stringValue 14 | } else { 15 | return "" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Source/Transform.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | public protocol Transform { 5 | func perform(_ value: Any?, realm: Realm?) -> Any? 6 | } 7 | -------------------------------------------------------------------------------- /Source/TransformChain.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | 4 | open class TransformChain: Transform { 5 | open var transforms: [Transform] = [] 6 | 7 | public init(transforms: Transform...) { 8 | self.transforms = transforms 9 | } 10 | 11 | open func perform(_ value: Any?, realm: Realm?) -> Any? { 12 | return transforms.reduce(value!) { $1.perform($0, realm: realm) } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Source/Utils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import RealmSwift 3 | import SwiftyJSON 4 | 5 | public func toArray(_ realmArray: List) -> [T] { 6 | var retArray: [T] = [] 7 | for obj in realmArray { 8 | retArray.append(obj) 9 | } 10 | return retArray 11 | } 12 | 13 | public func toArray(_ realmResult: Results) -> [T] { 14 | var retArray: [T] = [] 15 | for obj in realmResult { 16 | retArray.append(obj) 17 | } 18 | return retArray 19 | } 20 | 21 | // Traverse a nested dictionary using dot-notation path 22 | public func fetchPathFromDictionary(_ namespace: String, dictionary: [String:Any]) -> Any? { 23 | var pieces = namespace.components(separatedBy: ".") 24 | 25 | var current: [String:Any] = dictionary 26 | 27 | while !pieces.isEmpty { 28 | let piece = pieces.remove(at: 0) 29 | if pieces.isEmpty { 30 | return current[piece] 31 | } 32 | 33 | if let nextDictionary = current[piece] as? [String:Any] { 34 | current = nextDictionary 35 | } else { 36 | return nil 37 | } 38 | } 39 | 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /Source/vendor/Pluralize.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pluralize.swift 3 | // link: 4 | // https://github.com/joshualat/Pluralize.swift 5 | // 6 | // usage: 7 | // "Tooth".pluralize 8 | // "Nutrtion".pluralize 9 | // "House".pluralize(count: 1) 10 | // "Person".pluralize(count: 2, with: "Persons") 11 | // 12 | // Copyright (c) 2014 Joshua Arvin Lat 13 | // 14 | // MIT License 15 | // 16 | // Permission is hereby granted, free of charge, to any person obtaining 17 | // a copy of this software and associated documentation files (the 18 | // "Software"), to deal in the Software without restriction, including 19 | // without limitation the rights to use, copy, modify, merge, publish, 20 | // distribute, sublicense, and/or sell copies of the Software, and to 21 | // permit persons to whom the Software is furnished to do so, subject to 22 | // the following conditions: 23 | // 24 | // The above copyright notice and this permission notice shall be 25 | // included in all copies or substantial portions of the Software. 26 | // 27 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 28 | // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 29 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 30 | // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 31 | // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 32 | // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 33 | // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | 35 | import Foundation 36 | 37 | public class Pluralize { 38 | var uncountables:[String] = [] 39 | var rules:[(rule: String, template: String)] = [] 40 | 41 | public init() { 42 | uncountables = [ 43 | "access", "accommodation", "adulthood", "advertising", "advice", 44 | "aggression", "aid", "air", "alcohol", "anger", "applause", 45 | "arithmetic", "art", "assistance", "athletics", "attention", 46 | "bacon", "baggage", "ballet", "beauty", "beef", "beer", "biology", 47 | "botany", "bread", "butter", "carbon", "cash", "chaos", "cheese", 48 | "chess", "childhood", "clothing", "coal", "coffee", "commerce", 49 | "compassion", "comprehension", "content", "corruption", "cotton", 50 | "courage", "currency", "dancing", "danger", "data", "delight", 51 | "dignity", "dirt", "distribution", "dust", "economics", "education", 52 | "electricity", "employment", "engineering", "envy", "equipment", 53 | "ethics", "evidence", "evolution", "faith", "fame", "fish", "flour", "flu", 54 | "food", "freedom", "fuel", "fun", "furniture", "garbage", "garlic", 55 | "genetics", "gold", "golf", "gossip", "grammar", "gratitude", "grief", 56 | "ground", "guilt", "gymnastics", "hair", "happiness", "hardware", 57 | "harm", "hate", "hatred", "health", "heat", "height", "help", "homework", 58 | "honesty", "honey", "hospitality", "housework", "humour", "hunger", 59 | "hydrogen", "ice", "ice", "cream", "importance", "inflation", "information", 60 | "injustice", "innocence", "iron", "irony", "jealousy", "jelly", "judo", 61 | "karate", "kindness", "knowledge", "labour", "lack", "laughter", "lava", 62 | "leather", "leisure", "lightning", "linguistics", "litter", "livestock", 63 | "logic", "loneliness", "luck", "luggage", "machinery", "magic", 64 | "management", "mankind", "marble", "mathematics", "mayonnaise", 65 | "measles", "meat", "methane", "milk", "money", "mud", "music", "nature", 66 | "news", "nitrogen", "nonsense", "nurture", "nutrition", "obedience", 67 | "obesity", "oil", "oxygen", "passion", "pasta", "patience", "permission", 68 | "physics", "poetry", "pollution", "poverty", "power", "pronunciation", 69 | "psychology", "publicity", "quartz", "racism", "rain", "relaxation", 70 | "reliability", "research", "respect", "revenge", "rice", "rubbish", 71 | "rum", "salad", "satire", "seaside", "shame", "shopping", "silence", 72 | "sleep", "smoke", "smoking", "snow", "soap", "software", "soil", 73 | "sorrow", "soup", "speed", "spelling", "steam", "stuff", "stupidity", 74 | "sunshine", "symmetry", "tennis", "thirst", "thunder", "toast", 75 | "tolerance", "toys", "traffic", "transporation", "travel", "trust", "understanding", 76 | "unemployment", "unity", "validity", "veal", "vengeance", "violence"] 77 | 78 | add(rule: "$", with:"$1s") 79 | add(rule: "s$", with:"$1ses") 80 | add(rule: "(t|r|l|b)y$", with:"$1ies") 81 | add(rule: "x$", with:"$1xes") 82 | add(rule: "(sh|zz|ss)$", with:"$1es") 83 | add(rule: "(ax)is", with: "$1es") 84 | add(rule: "(cact|nucle|alumn|bacill|fung|radi|stimul|syllab)us$", with:"$1i") 85 | add(rule: "(corp)us$", with:"$1ora") 86 | add(rule: "sis$", with:"$1ses") 87 | add(rule: "ch$", with:"$1ches") 88 | add(rule: "o$", with:"$1os") 89 | add(rule: "(buffal|carg|mosquit|torped|zer|vet|her|ech)o$", with:"$1oes") 90 | add(rule: "fe$", with:"$1ves") 91 | add(rule: "(thie)f$", with:"$1ves") 92 | add(rule: "oaf$", with:"$1oaves") 93 | add(rule: "um$", with:"$1a") 94 | add(rule: "ium$", with:"$1ia") 95 | add(rule: "oof$", with:"$1ooves") 96 | add(rule: "(nebul)a", with:"$1ae") 97 | add(rule: "(criteri|phenomen)on$", with:"$1a") 98 | add(rule: "(potat|tomat|volcan)o$", with:"$1oes") 99 | add(rule: "^(|wo|work|fire)man$", with: "$1men") 100 | add(rule: "(f)oot$", with: "$1eet") 101 | add(rule: "lf$", with: "$1lves") 102 | add(rule: "(t)ooth$", with: "$1eeth") 103 | add(rule: "(g)oose$", with: "$1eese") 104 | add(rule: "^(c)hild$", with: "$1hildren") 105 | add(rule: "^(o)x$", with: "$1xen") 106 | add(rule: "^(p)erson$", with: "$1eople") 107 | add(rule: "(m|l)ouse$", with: "$1ice") 108 | add(rule: "^(d)ie$", with: "$1ice") 109 | add(rule: "^(alg|vertebr|vit)a$", with: "$1ae") 110 | add(rule: "^(a)lumna$", with: "$1lumnae") 111 | add(rule: "^(a)pparatus$", with: "$1pparatuses") 112 | add(rule: "^(ind)ex$", with: "$1ices") 113 | add(rule: "^(append|matr)ix$", with: "$1ices") 114 | add(rule: "^(b|tabl)eau$", with: "$1eaux") 115 | add(rule: "arf$", with: "$1arves") 116 | add(rule: "(embarg)o$", with: "$1oes") 117 | add(rule: "(gen)us$", with: "$1era") 118 | add(rule: "(r)oof$", with: "$1oofs") 119 | add(rule: "(l)eaf$", with: "$1eaves") 120 | add(rule: "(millen)ium$", with: "$1ia") 121 | add(rule: "(th)at$", with: "$1ose") 122 | add(rule: "(th)is$", with: "$1ese") 123 | 124 | unchanging(word: "sheep") 125 | unchanging(word: "deer") 126 | unchanging(word: "moose") 127 | unchanging(word: "swine") 128 | unchanging(word: "bison") 129 | unchanging(word: "corps") 130 | unchanging(word: "means") 131 | unchanging(word: "series") 132 | unchanging(word: "scissors") 133 | unchanging(word: "species") 134 | } 135 | 136 | public class func apply(word: String) -> String { 137 | guard !(sharedInstance.uncountables.contains(word.lowercased()) || word.count == 0) else { 138 | return word 139 | } 140 | 141 | for pair in sharedInstance.rules { 142 | let newValue = regexReplace(input: word, pattern: pair.rule, template: pair.template) 143 | if newValue != word { 144 | return newValue 145 | } 146 | } 147 | 148 | return word 149 | } 150 | 151 | public class func add(rule: String, with template: String) { 152 | sharedInstance.add(rule: rule, with: template) 153 | } 154 | 155 | public class func uncountable(word: String) { 156 | sharedInstance.uncountable(word: word) 157 | } 158 | 159 | public class func unchanging(word: String) { 160 | sharedInstance.unchanging(word: word) 161 | } 162 | 163 | class var sharedInstance : Pluralize { 164 | return Pluralize() 165 | } 166 | 167 | private class func regexReplace(input: String, pattern: String, template: String) -> String { 168 | let regex = try! NSRegularExpression(pattern: pattern, options: .caseInsensitive) 169 | let range = NSRange(location: 0, length: input.count) 170 | let output = regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: template) 171 | return output 172 | } 173 | 174 | private func add(rule: String, with template: String) { 175 | rules.insert((rule: rule, template: template), at: 0) 176 | } 177 | 178 | private func uncountable(word: String) { 179 | uncountables.insert(word.lowercased(), at: 0) 180 | } 181 | 182 | private func unchanging(word: String) { 183 | uncountables.insert(word.lowercased(), at: 0) 184 | } 185 | } 186 | 187 | extension String { 188 | public func pluralize(count: Int = 2, with: String = "") -> String { 189 | guard !(count == 1) else { return self } 190 | guard with.length != 0 else { return Pluralize.apply(word: self) } 191 | return with 192 | } 193 | 194 | // Workaround to allow us to use `count` as an argument name in pluralize() above. 195 | private var length: Int { 196 | return self.count 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Tests/ApiFormTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiFormTests.swift 3 | // APIModel 4 | // 5 | // Created by Craig Heneveld on 1/14/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | import Alamofire 12 | import OHHTTPStubs 13 | import RealmSwift 14 | 15 | class ApiFormTests: XCTestCase { 16 | var timeout: TimeInterval = 10 17 | var testRealm: Realm! 18 | var host = "http://you-dont-party.com" 19 | 20 | override func setUp() { 21 | 22 | super.setUp() 23 | 24 | Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name 25 | 26 | testRealm = try! Realm() 27 | 28 | ApiSingleton.setInstance(ApiManager(config: ApiConfig(host: self.host))) 29 | } 30 | 31 | override func tearDown() { 32 | 33 | super.tearDown() 34 | 35 | try! testRealm.write { 36 | self.testRealm.deleteAll() 37 | } 38 | 39 | OHHTTPStubs.removeAllStubs() 40 | } 41 | 42 | func testSimpleFindArray() { 43 | 44 | var theResponse: [Post]? 45 | let readyExpectation = self.expectation(description: "ready") 46 | 47 | stub(condition: {_ in true}) { request in 48 | let stubPath = OHPathForFile("posts.json", type(of: self)) 49 | return fixture(filePath: stubPath!, headers: ["Content-Type":"application/json"]) 50 | } 51 | 52 | Api.findArray { response, _ in 53 | theResponse = response 54 | 55 | XCTAssertEqual(response.count, 2) 56 | XCTAssertEqual(response.first!.id, "1") 57 | XCTAssertEqual(response.last!.id, "2") 58 | 59 | readyExpectation.fulfill() 60 | OHHTTPStubs.removeAllStubs() 61 | } 62 | 63 | 64 | self.waitForExpectations(timeout: self.timeout) { err in 65 | // By the time we reach this code, the while loop has exited 66 | // so the response has arrived or the test has timed out 67 | XCTAssertNotNil(theResponse, "Received data should not be nil") 68 | } 69 | } 70 | 71 | func testGetWithServerFailure() { 72 | 73 | var theResponse: ApiModelResponse? 74 | let readyExpectation = self.expectation(description: "ready") 75 | 76 | 77 | stub(condition: {_ in true}) { request in 78 | return OHHTTPStubsResponse(data:"Something went wrong!".data(using: String.Encoding.utf8)!, statusCode: 500, headers: nil) 79 | } 80 | 81 | Api.get("/v1/posts.json") { response in 82 | theResponse = response 83 | 84 | XCTAssertEqual(response.errors! as NSObject, ["base" : ["An unexpected server error occurred"]] as NSObject) 85 | 86 | readyExpectation.fulfill() 87 | OHHTTPStubs.removeAllStubs() 88 | } 89 | 90 | self.waitForExpectations(timeout: self.timeout) { err in 91 | // By the time we reach this code, the while loop has exited 92 | // so the response has arrived or the test has timed out 93 | XCTAssertNotNil(theResponse, "Received data should not be nil") 94 | } 95 | } 96 | 97 | func testFindArrayWithServerFailure() { 98 | 99 | var theResponse: [Post]? 100 | let readyExpectation = self.expectation(description: "ready") 101 | 102 | 103 | stub(condition: {_ in true}) { request in 104 | return OHHTTPStubsResponse(data:"Something went wrong!".data(using: String.Encoding.utf8)!, statusCode: 500, headers: nil) 105 | } 106 | 107 | Api.findArray { response, _ in 108 | theResponse = response 109 | 110 | XCTAssertNotNil(response) 111 | 112 | XCTAssertEqual(response.count, 0) 113 | 114 | readyExpectation.fulfill() 115 | OHHTTPStubs.removeAllStubs() 116 | } 117 | 118 | self.waitForExpectations(timeout: self.timeout) { err in 119 | // By the time we reach this code, the while loop has exited 120 | // so the response has arrived or the test has timed out 121 | XCTAssertNotNil(theResponse, "Received data should not be nil") 122 | } 123 | } 124 | 125 | func testFindWithServerFailure() { 126 | 127 | let readyExpectation = self.expectation(description: "ready") 128 | 129 | stub(condition: {_ in true}) { request in 130 | return OHHTTPStubsResponse(data:"Something went wrong!".data(using: String.Encoding.utf8)!, statusCode: 500, headers: nil) 131 | } 132 | 133 | Api.find { post, response in 134 | 135 | XCTAssertEqual("An unexpected server error occurred", response.errorMessages?.first ?? "") 136 | XCTAssertNil(post) 137 | 138 | readyExpectation.fulfill() 139 | OHHTTPStubs.removeAllStubs() 140 | } 141 | 142 | self.waitForExpectations(timeout: self.timeout) { err in 143 | // By the time we reach this code, the while loop has exited 144 | // so the response has arrived or the test has timed out 145 | XCTAssertNil(err, "Received data should be nil") 146 | } 147 | } 148 | 149 | 150 | func testFindWithServerFailureWithErrorMessage() { 151 | 152 | let readyExpectation = self.expectation(description: "ready") 153 | 154 | stub(condition: {_ in true}) { request in 155 | return OHHTTPStubsResponse(data:"{\"post\": {\"errors\": [\"Something went wrong!\"]}}".data(using: String.Encoding.utf8)!, statusCode: 500, headers: nil) 156 | } 157 | 158 | Api.find { post, response in 159 | 160 | XCTAssertEqual("Something went wrong!", response.errorMessages?.first ?? "") 161 | XCTAssertNotNil(post) 162 | 163 | readyExpectation.fulfill() 164 | OHHTTPStubs.removeAllStubs() 165 | } 166 | 167 | self.waitForExpectations(timeout: self.timeout) { err in 168 | // By the time we reach this code, the while loop has exited 169 | // so the response has arrived or the test has timed out 170 | XCTAssertNil(err, "Received data should be nil") 171 | } 172 | } 173 | 174 | func testSaveWithModelValidationErrors() { 175 | 176 | let readyExpectation = self.expectation(description: "ready") 177 | 178 | stub(condition: {_ in true}) { request in 179 | let stubPath = OHPathForFile("post_with_error.json", type(of: self)) 180 | return fixture(filePath: stubPath!, status: 422, headers: ["Content-Type":"application/json"]) 181 | } 182 | 183 | let post = Post() 184 | 185 | let form = Api(model: post) 186 | 187 | form.save { _ in 188 | XCTAssertTrue(form.hasErrors) 189 | 190 | // But what happened? - the server returned meaningful validations but are lost! 191 | XCTAssertEqual(form.errorMessages, ["An unexpected server error occurred"]) 192 | 193 | readyExpectation.fulfill() 194 | 195 | OHHTTPStubs.removeAllStubs() 196 | } 197 | 198 | self.waitForExpectations(timeout: self.timeout) { err in 199 | // By the time we reach this code, the while loop has exited 200 | // so the response has arrived or the test has timed out 201 | XCTAssertNil(err, "Received data should be nil") 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Tests/ApiManagerRequestTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiManagerRequestTests.swift 3 | // APIModel 4 | // 5 | // Created by Erik Rothoff Andersson on 2016-01-01. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | import Alamofire 12 | 13 | class ApiManagerRequestTests: XCTestCase { 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testEmptyRootNamespace() { 25 | // given 26 | let config = ApiConfig() 27 | let apiManager = ApiManager(config: config) 28 | 29 | let apiRequest = ApiRequest(config: config, method: .get, path: "/example.json") 30 | 31 | let response = ApiResponse(request: apiRequest) 32 | let parsedResponse = [ 33 | "post": [ 34 | "id": "1" 35 | ] 36 | ] 37 | 38 | // when 39 | let (finalResponse, _) = apiManager.handleResponse( 40 | response, 41 | parsedResponse: parsedResponse, 42 | apiConfig: config 43 | ) 44 | 45 | // then 46 | let responseObject = finalResponse!.parsedResponse as! [String:Any] 47 | XCTAssertEqual((responseObject["post"] as! [String:Any])["id"]! as? String, "1") 48 | } 49 | 50 | func testSimpleRootNamespace() { 51 | // given 52 | let config = ApiConfig() 53 | config.rootNamespace = "data.post" 54 | 55 | let apiManager = ApiManager(config: config) 56 | 57 | let apiRequest = ApiRequest(config: config, method: .get, path: "/example.json") 58 | 59 | let response = ApiResponse(request: apiRequest) 60 | let parsedResponse = [ 61 | "data": [ 62 | "post": [ 63 | "id": "1" 64 | ] 65 | ] 66 | ] 67 | 68 | // when 69 | let (finalResponse, _) = apiManager.handleResponse( 70 | response, 71 | parsedResponse: parsedResponse, 72 | apiConfig: config 73 | ) 74 | 75 | // then 76 | let responseObject = finalResponse!.parsedResponse as! [String:AnyObject] 77 | XCTAssertEqual(responseObject["id"] as? String, "1") 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /Tests/ApiManagerResponseTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ApiResponseTests.swift 3 | // ApiModel 4 | // 5 | // Created by Craig Heneveld on 1/14/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | import Alamofire 12 | import OHHTTPStubs 13 | import RealmSwift 14 | 15 | class ApiManagerResponseTests: XCTestCase { 16 | var timeout: TimeInterval = 10 17 | var testRealm: Realm! 18 | var host = "http://you-dont-party.com" 19 | 20 | override func setUp() { 21 | super.setUp() 22 | 23 | Realm.Configuration.defaultConfiguration.inMemoryIdentifier = self.name 24 | 25 | testRealm = try! Realm() 26 | 27 | ApiSingleton.setInstance(ApiManager(config: ApiConfig(host: self.host))) 28 | } 29 | 30 | override func tearDown() { 31 | super.tearDown() 32 | 33 | try! testRealm.write { 34 | self.testRealm.deleteAll() 35 | } 36 | 37 | OHHTTPStubs.removeAllStubs() 38 | } 39 | 40 | func testNotFoundResponse() { 41 | var theResponse: ApiModelResponse? 42 | let readyExpectation = self.expectation(description: "ready") 43 | 44 | stub(condition: {_ in true}) { request in 45 | OHHTTPStubsResponse( 46 | data:"File not found".data(using: String.Encoding.utf8)!, 47 | statusCode: 404, 48 | headers: nil 49 | ) 50 | } 51 | 52 | Api.get("/v1/posts.json") { response in 53 | 54 | XCTAssertEqual(response.rawResponse!.status!, 404, "A response should have a status of 404") 55 | XCTAssertEqual(String(describing: response.rawResponse!.error!), "invalidRequest(code: 404)") 56 | XCTAssertTrue(response.rawResponse!.isInvalid, "A response status of 404 should be invalid") 57 | 58 | theResponse = response 59 | 60 | readyExpectation.fulfill() 61 | OHHTTPStubs.removeAllStubs() 62 | } 63 | 64 | 65 | waitForExpectations(timeout: self.timeout) { err in 66 | // By the time we reach this code, the while loop has exited 67 | // so the response has arrived or the test has timed out 68 | XCTAssertNotNil(theResponse, "Received data should not be nil") 69 | } 70 | } 71 | 72 | func testServerErrorResponse() { 73 | var theResponse: ApiModelResponse? 74 | let readyExpectation = self.expectation(description: "ready") 75 | 76 | stub(condition: {_ in true}) { request in 77 | OHHTTPStubsResponse(data 78 | :"Something went wrong!".data(using: String.Encoding.utf8)!, 79 | statusCode: 500, 80 | headers: nil 81 | ) 82 | } 83 | 84 | Api.get("/v1/posts.json") { response in 85 | 86 | XCTAssertEqual(response.rawResponse!.status!, 500, "A response should have a status of 500") 87 | XCTAssertEqual(String(describing: response.rawResponse!.error!), "badRequest(code: 500)") 88 | // XCTAssertTrue(response.rawResponse!.isInvalid, "A response status of 500 should be invalid") 89 | 90 | theResponse = response 91 | 92 | readyExpectation.fulfill() 93 | OHHTTPStubs.removeAllStubs() 94 | } 95 | 96 | waitForExpectations(timeout: self.timeout) { err in 97 | // By the time we reach this code, the while loop has exited 98 | // so the response has arrived or the test has timed out 99 | XCTAssertNotNil(theResponse, "Received data should not be nil") 100 | } 101 | } 102 | 103 | func testSessionConfig() { 104 | let configuration = URLSessionConfiguration.default 105 | configuration.timeoutIntervalForRequest = 1 // seconds 106 | configuration.timeoutIntervalForResource = 1 107 | 108 | ApiSingleton.setInstance(ApiManager(config: ApiConfig(host: self.host, urlSessionConfig:configuration))) 109 | 110 | let readyExpectation = expectation(description: "ready") 111 | 112 | stub(condition: { _ in true }) { request in 113 | OHHTTPStubsResponse( 114 | data: "Something went wrong!".data(using: String.Encoding.utf8)!, 115 | statusCode: 500, 116 | headers: nil 117 | ).requestTime(2.0, responseTime: 2.0) 118 | } 119 | 120 | Api.get("/v1/posts.json") { response in 121 | 122 | // -1001 indicates a timeout occured which is what's expected 123 | XCTAssertNil(response.rawResponse?.status, "We currently can't test for raw alamofire response codes so just checking if status is nil. Which failed.") 124 | 125 | readyExpectation.fulfill() 126 | OHHTTPStubs.removeAllStubs() 127 | } 128 | 129 | waitForExpectations(timeout: self.timeout) { err in 130 | // By the time we reach this code, the while loop has exited 131 | // so the response has arrived or the test has timed out 132 | XCTAssertNil(err, "Timeout occured") 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Tests/ArrayTransformTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayTransformTests.swift 3 | // ApiModel 4 | // 5 | // Created by Erik Rothoff Andersson on 2016-20-01. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | import RealmSwift 12 | 13 | class ArrayTransformTests: XCTestCase { 14 | 15 | var realm: Realm! 16 | var postsTransform: ArrayTransform! 17 | 18 | override func setUp() { 19 | try! realm = Realm() 20 | postsTransform = ArrayTransform() 21 | } 22 | 23 | override func tearDown() { 24 | if let fileURL = realm.configuration.fileURL { 25 | try! FileManager.default.removeItem(at: fileURL) 26 | } 27 | } 28 | 29 | func testACoupleOfConversions() { 30 | let response = [ 31 | [ 32 | "id": 1, 33 | "title": "Taking over the world" 34 | ], 35 | 36 | [ 37 | "id": 2, 38 | "title": "Restoring peace" 39 | ] 40 | ] 41 | 42 | let posts = postsTransform.perform(response, realm: nil) as? [Post] 43 | 44 | XCTAssertEqual(posts?[0].title, "Taking over the world") 45 | XCTAssertEqual(posts?[0].id, "1") 46 | 47 | XCTAssertEqual(posts?[1].title, "Restoring peace") 48 | XCTAssertEqual(posts?[1].id, "2") 49 | } 50 | 51 | func testWithOneBadResponse() { 52 | let response = [ 53 | [ 54 | "id": 1, 55 | "title": "Taking over the world" 56 | ], 57 | 58 | "This api sucks" 59 | ] as [Any] 60 | 61 | let posts = postsTransform.perform(response, realm: nil) as? [Post] 62 | 63 | XCTAssertEqual(posts?[0].title, "Taking over the world") 64 | XCTAssertEqual(posts?[0].id, "1") 65 | XCTAssertEqual(posts!.count, 1) 66 | } 67 | 68 | func testWithPersistedObjects() { 69 | // Create a couple of persisted objects 70 | let persistedPost0 = Post() 71 | persistedPost0.id = "1337" 72 | persistedPost0.title = "Hello world" 73 | 74 | let persistedPost1 = Post() 75 | persistedPost1.id = "1338" 76 | persistedPost1.title = "Bye world" 77 | 78 | try! realm.write { 79 | self.realm.add(persistedPost0, update: .all) 80 | self.realm.add(persistedPost1, update: .all) 81 | } 82 | 83 | let response = [ 84 | [ 85 | "id": "1337", 86 | "title": "World hello" 87 | ], 88 | 89 | [ 90 | "id": "1338", 91 | "title": "World bye" 92 | ] 93 | ] 94 | 95 | try! realm.write { 96 | let posts = postsTransform.perform(response, realm: realm) as! [Post] 97 | 98 | XCTAssertEqual(posts[0].id, "1337") 99 | XCTAssertEqual(posts[0].title, "World hello") 100 | XCTAssertEqual(persistedPost0.id, "1337") 101 | XCTAssertEqual(persistedPost0.title, "World hello") 102 | 103 | XCTAssertEqual(posts[1].id, "1338") 104 | XCTAssertEqual(posts[1].title, "World bye") 105 | XCTAssertEqual(persistedPost1.id, "1338") 106 | XCTAssertEqual(persistedPost1.title, "World bye") 107 | } 108 | } 109 | 110 | func testWithPersistedObjectsButDuplicatesInArrayResponse() { 111 | // If an API returns the same object ID twice in an array, it could lead to weird realm crashes 112 | // Create a couple of persisted objects 113 | let persistedPost0 = Post() 114 | persistedPost0.id = "1337" 115 | persistedPost0.title = "Hello world" 116 | 117 | try! realm.write { 118 | self.realm.add(persistedPost0, update: .all) 119 | } 120 | 121 | let response = [ 122 | [ 123 | "id": "1337", 124 | "title": "World hello" 125 | ], 126 | 127 | [ 128 | "id": "1337", 129 | "title": "World hello hello" 130 | ] 131 | ] 132 | 133 | try! realm.write { 134 | let posts = postsTransform.perform(response, realm: realm) as! [Post] 135 | 136 | XCTAssertEqual(posts[0].id, "1337") 137 | XCTAssertEqual(posts[0].title, "World hello hello") 138 | XCTAssertEqual(posts[1].id, "1337") 139 | XCTAssertEqual(posts[1].title, "World hello hello") 140 | XCTAssertEqual(persistedPost0.id, "1337") 141 | XCTAssertEqual(persistedPost0.title, "World hello hello") 142 | } 143 | } 144 | 145 | func testWithNestedModelsAndDuplicateIDs() { 146 | // If an API returns the same object ID twice in an array, it could lead to weird realm crashes 147 | // Create a couple of persisted objects 148 | let feed = Feed() 149 | feed.id = "1" 150 | feed.title = "Hello world" 151 | 152 | try! realm.write { 153 | self.realm.add(feed, update: .all) 154 | } 155 | 156 | let response = [ 157 | "id": "1", 158 | "title": "Hello world", 159 | "posts": [ 160 | [ 161 | "id": "1337", 162 | "title": "World hello" 163 | ], 164 | 165 | [ 166 | "id": "1337", 167 | "title": "World hello hello" 168 | ] 169 | ] 170 | ] as [String : Any] 171 | 172 | let feedTransform = ModelTransform() 173 | 174 | try! realm.write { 175 | let feedFromResponse = feedTransform.perform(response, realm: feed.realm) as? Feed 176 | 177 | XCTAssertEqual(feedFromResponse?.posts[0].id, "1337") 178 | XCTAssertEqual(feedFromResponse?.posts[0].title, "World hello hello") 179 | } 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /Tests/Feed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // ApiModel 4 | // 5 | // Created by Craig Heneveld on 1/14/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import ApiModel 12 | 13 | // A test model for nested models 14 | class Feed: Object, ApiModel { 15 | @objc dynamic var id = "" 16 | @objc dynamic var title = "" 17 | let posts = List() 18 | 19 | override class func primaryKey() -> String { 20 | return "id" 21 | } 22 | 23 | class func apiNamespace() -> String { 24 | return "feed" 25 | } 26 | 27 | class func apiRoutes() -> ApiRoutes { 28 | return ApiRoutes() 29 | } 30 | 31 | class func fromJSONMapping() -> JSONMapping { 32 | return [ 33 | "id": ApiIdTransform(), 34 | "title": StringTransform(), 35 | "posts": ArrayTransform() 36 | ] 37 | } 38 | 39 | // Define how this object is to be serialized back into a server response format 40 | func JSONDictionary() -> [String:Any] { 41 | return [ 42 | "id": id as AnyObject, 43 | "title": title as AnyObject, 44 | "posts": posts.map { $0.JSONDictionary() } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/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/ModelTransformTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ModelTransformTests.swift 3 | // APIModel 4 | // 5 | // Created by Erik Rothoff Andersson on 2016-20-01. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | import RealmSwift 12 | 13 | class ModelTransformTests: XCTestCase { 14 | 15 | var realm: Realm! 16 | var postTransform: ModelTransform! 17 | 18 | override func setUp() { 19 | try! realm = Realm() 20 | postTransform = ModelTransform() 21 | } 22 | 23 | override func tearDown() { 24 | if let fileURL = realm.configuration.fileURL { 25 | try! FileManager.default.removeItem(at: fileURL) 26 | } 27 | } 28 | 29 | func testSimpleConversion() { 30 | let response = [ 31 | "id": 1, 32 | "title": "Taking over the world" 33 | ] as [String : Any] 34 | 35 | let post = postTransform.perform(response, realm: nil) as? Post 36 | 37 | XCTAssertEqual(post?.title, "Taking over the world") 38 | XCTAssertEqual(post?.id, "1") 39 | } 40 | 41 | func testConversionIntoPersistedObject() { 42 | // Create a persisted object 43 | let persistedPost = Post() 44 | persistedPost.id = "1337" 45 | persistedPost.title = "Hello world" 46 | 47 | try! realm.write { 48 | self.realm.add(persistedPost, update: .all) 49 | } 50 | 51 | let response = [ 52 | "id": "1337", 53 | "title": "Hello world, revision II" 54 | ] 55 | 56 | try! realm.write { 57 | let otherPost = postTransform.perform(response, realm: realm) as! Post 58 | 59 | XCTAssertEqual(otherPost.id, "1337") 60 | XCTAssertEqual(otherPost.title, "Hello world, revision II") 61 | 62 | XCTAssertEqual(persistedPost.title, "Hello world, revision II") 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/NSDateTransformTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSDateTransformTests.swift 3 | // ApiModel 4 | // 5 | // Created by Erik Rothoff Andersson on 2016-05-01. 6 | // 7 | // 8 | 9 | import XCTest 10 | import ApiModel 11 | 12 | class NSDateTransformTests: XCTestCase { 13 | 14 | var calendar = Calendar(identifier: Calendar.Identifier.gregorian) 15 | let utcTimeZone = TimeZone(identifier: "Etc/UTC")! 16 | let yyyyMMDDDateFormatter = DateFormatter() 17 | 18 | override func setUp() { 19 | calendar.timeZone = utcTimeZone 20 | yyyyMMDDDateFormatter.timeZone = utcTimeZone 21 | yyyyMMDDDateFormatter.dateFormat = "yyyy-MM-dd" 22 | } 23 | 24 | func testISO8601WithoutTimezone() { 25 | let transform = DateTransform() 26 | let res = transform.perform("2015-12-30T12:12:33.000Z", realm: nil) as? Date 27 | 28 | var referenceDateCreator = DateComponents() 29 | (referenceDateCreator as NSDateComponents).timeZone = utcTimeZone 30 | referenceDateCreator.year = 2015 31 | referenceDateCreator.month = 12 32 | referenceDateCreator.day = 30 33 | referenceDateCreator.hour = 12 34 | referenceDateCreator.minute = 12 35 | referenceDateCreator.second = 33 36 | 37 | let referenceDate = calendar.date(from: referenceDateCreator) 38 | 39 | XCTAssertEqual(res!.timeIntervalSinceReferenceDate, referenceDate!.timeIntervalSinceReferenceDate, accuracy: 0.001) 40 | } 41 | 42 | func testISO8601WithTimezone() { 43 | let transform = DateTransform() 44 | let res = transform.perform("2015-12-30T12:12:33.000-05:00", realm: nil) as? Date 45 | 46 | var referenceDateCreator = DateComponents() 47 | (referenceDateCreator as NSDateComponents).timeZone = utcTimeZone 48 | referenceDateCreator.year = 2015 49 | referenceDateCreator.month = 12 50 | referenceDateCreator.day = 30 51 | referenceDateCreator.hour = 12 + 5 // UTC is + 5 hours 52 | referenceDateCreator.minute = 12 53 | referenceDateCreator.second = 33 54 | 55 | let referenceDate = calendar.date(from: referenceDateCreator) 56 | 57 | XCTAssertEqual(res!.timeIntervalSinceReferenceDate, referenceDate!.timeIntervalSinceReferenceDate, accuracy: 0.001) 58 | } 59 | 60 | func testUserDefinedDateFormat() { 61 | let transform = DateTransform(dateFormat: "yyyy-MM-dd") 62 | let res = transform.perform("2015-12-30", realm: nil) as? Date 63 | 64 | var referenceDateCreator = DateComponents() 65 | (referenceDateCreator as NSDateComponents).timeZone = utcTimeZone 66 | referenceDateCreator.year = 2015 67 | referenceDateCreator.month = 12 68 | referenceDateCreator.day = 30 69 | 70 | let referenceDate = calendar.date(from: referenceDateCreator) 71 | 72 | XCTAssertEqual(yyyyMMDDDateFormatter.string(from: res!), yyyyMMDDDateFormatter.string(from: referenceDate!)) 73 | } 74 | 75 | 76 | func testInvalidDate() { 77 | let transform = DateTransform() 78 | let res = transform.perform("i am not a date", realm: nil) as? Date 79 | XCTAssertNil(res) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Tests/Post.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Post.swift 3 | // ApiModel 4 | // 5 | // Created by Craig Heneveld on 1/14/16. 6 | // 7 | // 8 | 9 | import Foundation 10 | import RealmSwift 11 | import ApiModel 12 | 13 | class Post: Object, ApiModel { 14 | // Standard Realm boilerplate 15 | @objc dynamic var id = "" 16 | @objc dynamic var title = "" 17 | @objc dynamic var contents = "" 18 | @objc dynamic var createdAt: Date? = Date() 19 | 20 | override class func primaryKey() -> String { 21 | return "id" 22 | } 23 | 24 | // Define the standard namespace this class usually resides in JSON responses 25 | // MUST BE singular ie `post` not `posts` 26 | class func apiNamespace() -> String { 27 | return "post" 28 | } 29 | 30 | // Define where and how to get these. Routes are assumed to use Rails style REST (index, show, update, destroy) 31 | class func apiRoutes() -> ApiRoutes { 32 | return ApiRoutes( 33 | index: "/posts.json", 34 | show: "/post/:id:.json" 35 | ) 36 | } 37 | 38 | // Define how it is converted from JSON responses into Realm objects. A host of transforms are available 39 | // See section "Transforms" in README. They are super easy to create as well! 40 | class func fromJSONMapping() -> JSONMapping { 41 | return [ 42 | "id": ApiIdTransform(), 43 | "title": StringTransform(), 44 | "contents": StringTransform(), 45 | "createdAt": DateTransform() 46 | ] 47 | } 48 | 49 | // Define how this object is to be serialized back into a server response format 50 | func JSONDictionary() -> [String:Any] { 51 | return [ 52 | "id": id as AnyObject, 53 | "title": title as AnyObject, 54 | "contents": contents as AnyObject, 55 | "created_at": createdAt as AnyObject 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/RootNamespaceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RootNamespaceTests.swift 3 | // ApiModelTests 4 | // 5 | // Created by Erik Rothoff Andersson on 2015-13-08. 6 | // 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class RootNamespaceTests: XCTestCase { 13 | let nestedObject: [String:Any] = [ 14 | "foo": [ 15 | "bar": [ 16 | "baz": [ 17 | "bam": 42 18 | ] 19 | ] 20 | ] 21 | ] 22 | 23 | let notNested: [String:Any] = ["foo": 43] 24 | let singleNesting: [String:Any] = ["foo": ["bar": 44]] 25 | 26 | let nestedButNotDictionary: [String:Any] = [ 27 | "foo": [ 28 | "bar": [1, 2, 3] 29 | ] 30 | ] 31 | 32 | func testPathFormat() { 33 | XCTAssertNil(fetchPathFromDictionary("", dictionary: nestedObject)) 34 | XCTAssertNil(fetchPathFromDictionary(".....................", dictionary: nestedObject)) 35 | } 36 | 37 | func testThatItCanFetchSingleKeys() { 38 | XCTAssert((fetchPathFromDictionary("foo", dictionary: notNested) as? Int) == 43, "Should be able to fetch single keys") 39 | XCTAssertNil(fetchPathFromDictionary("not_exists", dictionary: notNested), "Should not crash if it doesn't exist") 40 | XCTAssertNil(fetchPathFromDictionary("not_exists.foo.bam", dictionary: notNested), "Should not crash if it doesn't exist nested") 41 | } 42 | 43 | func testThatItCanFetchNestingKeys() { 44 | XCTAssert((fetchPathFromDictionary("foo.bar.baz.bam", dictionary: nestedObject) as? Int) == 42, "Should be able to fetch single keys") 45 | XCTAssertNil(fetchPathFromDictionary("foo.bar.BAM", dictionary: nestedObject), "Should be able to handle non-existing keys") 46 | } 47 | 48 | func testThatItHandlesWrongTypes() { 49 | XCTAssertNil(fetchPathFromDictionary("foo.bar.bam", dictionary: nestedButNotDictionary), "Should handle bad keys") 50 | } 51 | 52 | func testThatItCanFetchSingleNesting() { 53 | if let nested = fetchPathFromDictionary("foo", dictionary: singleNesting) as? [String:Int] { 54 | XCTAssert(nested == ["bar": 44], "Can fetch nested objects") 55 | } else { 56 | XCTAssert(false, "Can fetch nested objects") 57 | } 58 | 59 | XCTAssert((fetchPathFromDictionary("foo.bar", dictionary: singleNesting) as? Int) == 44, "Can fetch singly nested objects") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Tests/post_with_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "post": { 3 | "errors": [{"title": "Must not be blank!"}] 4 | } 5 | } -------------------------------------------------------------------------------- /Tests/posts.json: -------------------------------------------------------------------------------- 1 | { 2 | "post": [{ 3 | "id": "1", 4 | "title": "This is a party post", 5 | "contents": "Hello!", 6 | "created_at": "2015-03-08T14:19:31-01:00" 7 | }, { 8 | "id": "2", 9 | "title": "Hello world - A prologue", 10 | "contents": "Hello!", 11 | "created_at": "2015-03-08T14:19:31-01:00" 12 | }] 13 | } 14 | --------------------------------------------------------------------------------