├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── JSONFeed.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── xcshareddata │ └── xcschemes │ └── JSONFeed.xcscheme ├── JSONFeed ├── Attachment.swift ├── Author.swift ├── Extensions │ └── URLExtensions.swift ├── Hub.swift ├── Info.plist ├── Item.swift ├── JSONFeed.h ├── JSONFeed.swift ├── JSONFeedError.swift └── JSONFeedReader.swift ├── JSONFeedTests ├── AttachmentTests.swift ├── AuthorTests.swift ├── Extensions │ └── URLExtensionsTests.swift ├── Feeds │ ├── SimpleFeedTests.swift │ ├── TimetableFeedTests.swift │ ├── simple.json │ └── timetable.json ├── HubTests.swift ├── Info.plist ├── ItemTests.swift ├── JSONFeedReaderTests.swift ├── JSONFeedTests.swift └── Mocks │ ├── MockURLSession.swift │ └── MockURLSessionDataTask.swift ├── LICENSE ├── Package.swift ├── README.md └── codecov.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## OS 2 | .DS_Store 3 | 4 | ## Build generated 5 | /build/ 6 | DerivedData/ 7 | 8 | ## Various settings 9 | *.pbxuser 10 | !default.pbxuser 11 | *.mode1v3 12 | !default.mode1v3 13 | *.mode2v3 14 | !default.mode2v3 15 | *.perspectivev3 16 | !default.perspectivev3 17 | xcuserdata/ 18 | 19 | ## Other 20 | *.moved-aside 21 | *.xcuserstate 22 | 23 | ## Obj-C/Swift specific 24 | *.hmap 25 | *.ipa 26 | *.dSYM.zip 27 | *.dSYM 28 | 29 | ## Playgrounds 30 | timeline.xctimeline 31 | playground.xcworkspace 32 | 33 | # Swift Package Manager 34 | # 35 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 36 | # Packages/ 37 | .build/ 38 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: # rule identifiers to exclude from running 2 | - statement_position 3 | - cyclomatic_complexity 4 | - function_parameter_count 5 | - line_length 6 | - type_name 7 | 8 | included: # paths to include during linting. 9 | - JSONFeed 10 | - JSONFeedTest 11 | 12 | file_length: 13 | - 400 # warning 14 | - 450 # error 15 | 16 | function_body_length: 17 | - 300 # warning 18 | - 400 # error 19 | 20 | type_body_length: 21 | - 500 # warning 22 | - 600 # error 23 | 24 | variable_name: 25 | min_length: 26 | warning: 1 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | osx_image: xcode8.3 3 | script: 4 | - xcodebuild -scheme JSONFeed -destination 'platform=iOS Simulator,name=iPhone 7,OS=10.3' build test 5 | after_success: 6 | - bash <(curl -s https://codecov.io/bash) 7 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | CB3BE7C61ECF58E9008D884F /* JSONFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CB3BE7BC1ECF58E9008D884F /* JSONFeed.framework */; }; 11 | CB3BE7CB1ECF58E9008D884F /* JSONFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7CA1ECF58E9008D884F /* JSONFeedTests.swift */; }; 12 | CB3BE7CD1ECF58E9008D884F /* JSONFeed.h in Headers */ = {isa = PBXBuildFile; fileRef = CB3BE7BF1ECF58E9008D884F /* JSONFeed.h */; settings = {ATTRIBUTES = (Public, ); }; }; 13 | CB3BE7DC1ECF593B008D884F /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7D61ECF593B008D884F /* Author.swift */; }; 14 | CB3BE7DD1ECF593B008D884F /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7D81ECF593B008D884F /* URLExtensions.swift */; }; 15 | CB3BE7DE1ECF593B008D884F /* Hub.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7D91ECF593B008D884F /* Hub.swift */; }; 16 | CB3BE7DF1ECF593B008D884F /* JSONFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7DA1ECF593B008D884F /* JSONFeed.swift */; }; 17 | CB3BE7E01ECF593B008D884F /* JSONFeedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7DB1ECF593B008D884F /* JSONFeedError.swift */; }; 18 | CB3BE7E31ECFA409008D884F /* HubTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7E21ECFA409008D884F /* HubTests.swift */; }; 19 | CB3BE7E51ECFA4C8008D884F /* AuthorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7E41ECFA4C8008D884F /* AuthorTests.swift */; }; 20 | CB3BE7E81ECFAA53008D884F /* URLExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7E71ECFAA53008D884F /* URLExtensionsTests.swift */; }; 21 | CB3BE7EA1ECFB7DC008D884F /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7E91ECFB7DC008D884F /* Item.swift */; }; 22 | CB3BE7EC1ECFB8EB008D884F /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7EB1ECFB8EB008D884F /* Attachment.swift */; }; 23 | CB3BE7EE1ECFBF9E008D884F /* AttachmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7ED1ECFBF9E008D884F /* AttachmentTests.swift */; }; 24 | CB3BE7F01ECFC124008D884F /* ItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7EF1ECFC124008D884F /* ItemTests.swift */; }; 25 | CB3BE7F31ED0ADB5008D884F /* TimetableFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7F21ED0ADB5008D884F /* TimetableFeedTests.swift */; }; 26 | CB3BE7F51ED0AF73008D884F /* timetable.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F41ED0AF73008D884F /* timetable.json */; }; 27 | CB3BE7F71ED0B569008D884F /* simple.json in Resources */ = {isa = PBXBuildFile; fileRef = CB3BE7F61ED0B569008D884F /* simple.json */; }; 28 | CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */; }; 29 | CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */; }; 30 | CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */; }; 31 | CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */; }; 32 | CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */; }; 33 | /* End PBXBuildFile section */ 34 | 35 | /* Begin PBXContainerItemProxy section */ 36 | CB3BE7C71ECF58E9008D884F /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = CB3BE7B31ECF58E9008D884F /* Project object */; 39 | proxyType = 1; 40 | remoteGlobalIDString = CB3BE7BB1ECF58E9008D884F; 41 | remoteInfo = JSONFeed; 42 | }; 43 | /* End PBXContainerItemProxy section */ 44 | 45 | /* Begin PBXFileReference section */ 46 | CB3BE7BC1ECF58E9008D884F /* JSONFeed.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = JSONFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | CB3BE7BF1ECF58E9008D884F /* JSONFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JSONFeed.h; sourceTree = ""; }; 48 | CB3BE7C01ECF58E9008D884F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 49 | CB3BE7C51ECF58E9008D884F /* JSONFeedTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JSONFeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | CB3BE7CA1ECF58E9008D884F /* JSONFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONFeedTests.swift; sourceTree = ""; }; 51 | CB3BE7CC1ECF58E9008D884F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 52 | CB3BE7D61ECF593B008D884F /* Author.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; 53 | CB3BE7D81ECF593B008D884F /* URLExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensions.swift; sourceTree = ""; }; 54 | CB3BE7D91ECF593B008D884F /* Hub.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Hub.swift; sourceTree = ""; }; 55 | CB3BE7DA1ECF593B008D884F /* JSONFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeed.swift; sourceTree = ""; }; 56 | CB3BE7DB1ECF593B008D884F /* JSONFeedError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedError.swift; sourceTree = ""; }; 57 | CB3BE7E21ECFA409008D884F /* HubTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HubTests.swift; sourceTree = ""; }; 58 | CB3BE7E41ECFA4C8008D884F /* AuthorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthorTests.swift; sourceTree = ""; }; 59 | CB3BE7E71ECFAA53008D884F /* URLExtensionsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLExtensionsTests.swift; sourceTree = ""; }; 60 | CB3BE7E91ECFB7DC008D884F /* Item.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 61 | CB3BE7EB1ECFB8EB008D884F /* Attachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; 62 | CB3BE7ED1ECFBF9E008D884F /* AttachmentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentTests.swift; sourceTree = ""; }; 63 | CB3BE7EF1ECFC124008D884F /* ItemTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ItemTests.swift; sourceTree = ""; }; 64 | CB3BE7F21ED0ADB5008D884F /* TimetableFeedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimetableFeedTests.swift; sourceTree = ""; }; 65 | CB3BE7F41ED0AF73008D884F /* timetable.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = timetable.json; sourceTree = ""; }; 66 | CB3BE7F61ED0B569008D884F /* simple.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = simple.json; sourceTree = ""; }; 67 | CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleFeedTests.swift; sourceTree = ""; }; 68 | CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReader.swift; sourceTree = ""; }; 69 | CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONFeedReaderTests.swift; sourceTree = ""; }; 70 | CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; 71 | CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = ""; }; 72 | /* End PBXFileReference section */ 73 | 74 | /* Begin PBXFrameworksBuildPhase section */ 75 | CB3BE7B81ECF58E9008D884F /* Frameworks */ = { 76 | isa = PBXFrameworksBuildPhase; 77 | buildActionMask = 2147483647; 78 | files = ( 79 | ); 80 | runOnlyForDeploymentPostprocessing = 0; 81 | }; 82 | CB3BE7C21ECF58E9008D884F /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | CB3BE7C61ECF58E9008D884F /* JSONFeed.framework in Frameworks */, 87 | ); 88 | runOnlyForDeploymentPostprocessing = 0; 89 | }; 90 | /* End PBXFrameworksBuildPhase section */ 91 | 92 | /* Begin PBXGroup section */ 93 | CB3BE7B21ECF58E9008D884F = { 94 | isa = PBXGroup; 95 | children = ( 96 | CB3BE7BE1ECF58E9008D884F /* JSONFeed */, 97 | CB3BE7C91ECF58E9008D884F /* JSONFeedTests */, 98 | CB3BE7BD1ECF58E9008D884F /* Products */, 99 | ); 100 | sourceTree = ""; 101 | }; 102 | CB3BE7BD1ECF58E9008D884F /* Products */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | CB3BE7BC1ECF58E9008D884F /* JSONFeed.framework */, 106 | CB3BE7C51ECF58E9008D884F /* JSONFeedTests.xctest */, 107 | ); 108 | name = Products; 109 | sourceTree = ""; 110 | }; 111 | CB3BE7BE1ECF58E9008D884F /* JSONFeed */ = { 112 | isa = PBXGroup; 113 | children = ( 114 | CB3BE7BF1ECF58E9008D884F /* JSONFeed.h */, 115 | CB3BE7C01ECF58E9008D884F /* Info.plist */, 116 | CB3BE7EB1ECFB8EB008D884F /* Attachment.swift */, 117 | CB3BE7D61ECF593B008D884F /* Author.swift */, 118 | CB3BE7D91ECF593B008D884F /* Hub.swift */, 119 | CB3BE7E91ECFB7DC008D884F /* Item.swift */, 120 | CB3BE7DA1ECF593B008D884F /* JSONFeed.swift */, 121 | CB3BE7DB1ECF593B008D884F /* JSONFeedError.swift */, 122 | CB3BE7FA1ED0BB3E008D884F /* JSONFeedReader.swift */, 123 | CB3BE7D71ECF593B008D884F /* Extensions */, 124 | ); 125 | path = JSONFeed; 126 | sourceTree = ""; 127 | }; 128 | CB3BE7C91ECF58E9008D884F /* JSONFeedTests */ = { 129 | isa = PBXGroup; 130 | children = ( 131 | CB3BE7CC1ECF58E9008D884F /* Info.plist */, 132 | CB3BE7ED1ECFBF9E008D884F /* AttachmentTests.swift */, 133 | CB3BE7E41ECFA4C8008D884F /* AuthorTests.swift */, 134 | CB3BE7E21ECFA409008D884F /* HubTests.swift */, 135 | CB3BE7EF1ECFC124008D884F /* ItemTests.swift */, 136 | CB3BE7FE1ED0BF53008D884F /* JSONFeedReaderTests.swift */, 137 | CB3BE7CA1ECF58E9008D884F /* JSONFeedTests.swift */, 138 | CB3BE7E61ECFAA43008D884F /* Extensions */, 139 | CB3BE7F11ED0AB1C008D884F /* Feeds */, 140 | CB3BE8001ED0BFF3008D884F /* Mocks */, 141 | ); 142 | path = JSONFeedTests; 143 | sourceTree = ""; 144 | }; 145 | CB3BE7D71ECF593B008D884F /* Extensions */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | CB3BE7D81ECF593B008D884F /* URLExtensions.swift */, 149 | ); 150 | path = Extensions; 151 | sourceTree = ""; 152 | }; 153 | CB3BE7E61ECFAA43008D884F /* Extensions */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | CB3BE7E71ECFAA53008D884F /* URLExtensionsTests.swift */, 157 | ); 158 | path = Extensions; 159 | sourceTree = ""; 160 | }; 161 | CB3BE7F11ED0AB1C008D884F /* Feeds */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | CB3BE7F61ED0B569008D884F /* simple.json */, 165 | CB3BE7F41ED0AF73008D884F /* timetable.json */, 166 | CB3BE7F81ED0B579008D884F /* SimpleFeedTests.swift */, 167 | CB3BE7F21ED0ADB5008D884F /* TimetableFeedTests.swift */, 168 | ); 169 | path = Feeds; 170 | sourceTree = ""; 171 | }; 172 | CB3BE8001ED0BFF3008D884F /* Mocks */ = { 173 | isa = PBXGroup; 174 | children = ( 175 | CB3BE8011ED0BFFF008D884F /* MockURLSession.swift */, 176 | CB3BE8031ED0C26B008D884F /* MockURLSessionDataTask.swift */, 177 | ); 178 | path = Mocks; 179 | sourceTree = ""; 180 | }; 181 | /* End PBXGroup section */ 182 | 183 | /* Begin PBXHeadersBuildPhase section */ 184 | CB3BE7B91ECF58E9008D884F /* Headers */ = { 185 | isa = PBXHeadersBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | CB3BE7CD1ECF58E9008D884F /* JSONFeed.h in Headers */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | /* End PBXHeadersBuildPhase section */ 193 | 194 | /* Begin PBXNativeTarget section */ 195 | CB3BE7BB1ECF58E9008D884F /* JSONFeed */ = { 196 | isa = PBXNativeTarget; 197 | buildConfigurationList = CB3BE7D01ECF58E9008D884F /* Build configuration list for PBXNativeTarget "JSONFeed" */; 198 | buildPhases = ( 199 | CB3BE7B71ECF58E9008D884F /* Sources */, 200 | CB3BE7B81ECF58E9008D884F /* Frameworks */, 201 | CB3BE7B91ECF58E9008D884F /* Headers */, 202 | CB3BE7BA1ECF58E9008D884F /* Resources */, 203 | CB3BE7E11ECF59D8008D884F /* Swiftlint */, 204 | ); 205 | buildRules = ( 206 | ); 207 | dependencies = ( 208 | ); 209 | name = JSONFeed; 210 | productName = JSONFeed; 211 | productReference = CB3BE7BC1ECF58E9008D884F /* JSONFeed.framework */; 212 | productType = "com.apple.product-type.framework"; 213 | }; 214 | CB3BE7C41ECF58E9008D884F /* JSONFeedTests */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = CB3BE7D31ECF58E9008D884F /* Build configuration list for PBXNativeTarget "JSONFeedTests" */; 217 | buildPhases = ( 218 | CB3BE7C11ECF58E9008D884F /* Sources */, 219 | CB3BE7C21ECF58E9008D884F /* Frameworks */, 220 | CB3BE7C31ECF58E9008D884F /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | CB3BE7C81ECF58E9008D884F /* PBXTargetDependency */, 226 | ); 227 | name = JSONFeedTests; 228 | productName = JSONFeedTests; 229 | productReference = CB3BE7C51ECF58E9008D884F /* JSONFeedTests.xctest */; 230 | productType = "com.apple.product-type.bundle.unit-test"; 231 | }; 232 | /* End PBXNativeTarget section */ 233 | 234 | /* Begin PBXProject section */ 235 | CB3BE7B31ECF58E9008D884F /* Project object */ = { 236 | isa = PBXProject; 237 | attributes = { 238 | LastSwiftUpdateCheck = 0830; 239 | LastUpgradeCheck = 0830; 240 | ORGANIZATIONNAME = wesbillman; 241 | TargetAttributes = { 242 | CB3BE7BB1ECF58E9008D884F = { 243 | CreatedOnToolsVersion = 8.3.2; 244 | LastSwiftMigration = 0830; 245 | ProvisioningStyle = Automatic; 246 | }; 247 | CB3BE7C41ECF58E9008D884F = { 248 | CreatedOnToolsVersion = 8.3.2; 249 | ProvisioningStyle = Automatic; 250 | }; 251 | }; 252 | }; 253 | buildConfigurationList = CB3BE7B61ECF58E9008D884F /* Build configuration list for PBXProject "JSONFeed" */; 254 | compatibilityVersion = "Xcode 3.2"; 255 | developmentRegion = English; 256 | hasScannedForEncodings = 0; 257 | knownRegions = ( 258 | en, 259 | ); 260 | mainGroup = CB3BE7B21ECF58E9008D884F; 261 | productRefGroup = CB3BE7BD1ECF58E9008D884F /* Products */; 262 | projectDirPath = ""; 263 | projectRoot = ""; 264 | targets = ( 265 | CB3BE7BB1ECF58E9008D884F /* JSONFeed */, 266 | CB3BE7C41ECF58E9008D884F /* JSONFeedTests */, 267 | ); 268 | }; 269 | /* End PBXProject section */ 270 | 271 | /* Begin PBXResourcesBuildPhase section */ 272 | CB3BE7BA1ECF58E9008D884F /* Resources */ = { 273 | isa = PBXResourcesBuildPhase; 274 | buildActionMask = 2147483647; 275 | files = ( 276 | ); 277 | runOnlyForDeploymentPostprocessing = 0; 278 | }; 279 | CB3BE7C31ECF58E9008D884F /* Resources */ = { 280 | isa = PBXResourcesBuildPhase; 281 | buildActionMask = 2147483647; 282 | files = ( 283 | CB3BE7F51ED0AF73008D884F /* timetable.json in Resources */, 284 | CB3BE7F71ED0B569008D884F /* simple.json in Resources */, 285 | ); 286 | runOnlyForDeploymentPostprocessing = 0; 287 | }; 288 | /* End PBXResourcesBuildPhase section */ 289 | 290 | /* Begin PBXShellScriptBuildPhase section */ 291 | CB3BE7E11ECF59D8008D884F /* Swiftlint */ = { 292 | isa = PBXShellScriptBuildPhase; 293 | buildActionMask = 2147483647; 294 | files = ( 295 | ); 296 | inputPaths = ( 297 | ); 298 | name = Swiftlint; 299 | outputPaths = ( 300 | ); 301 | runOnlyForDeploymentPostprocessing = 0; 302 | shellPath = /bin/sh; 303 | shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi"; 304 | }; 305 | /* End PBXShellScriptBuildPhase section */ 306 | 307 | /* Begin PBXSourcesBuildPhase section */ 308 | CB3BE7B71ECF58E9008D884F /* Sources */ = { 309 | isa = PBXSourcesBuildPhase; 310 | buildActionMask = 2147483647; 311 | files = ( 312 | CB3BE7FB1ED0BB3E008D884F /* JSONFeedReader.swift in Sources */, 313 | CB3BE7DC1ECF593B008D884F /* Author.swift in Sources */, 314 | CB3BE7EC1ECFB8EB008D884F /* Attachment.swift in Sources */, 315 | CB3BE7DD1ECF593B008D884F /* URLExtensions.swift in Sources */, 316 | CB3BE7DE1ECF593B008D884F /* Hub.swift in Sources */, 317 | CB3BE7E01ECF593B008D884F /* JSONFeedError.swift in Sources */, 318 | CB3BE7EA1ECFB7DC008D884F /* Item.swift in Sources */, 319 | CB3BE7DF1ECF593B008D884F /* JSONFeed.swift in Sources */, 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | CB3BE7C11ECF58E9008D884F /* Sources */ = { 324 | isa = PBXSourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | CB3BE7CB1ECF58E9008D884F /* JSONFeedTests.swift in Sources */, 328 | CB3BE7FF1ED0BF53008D884F /* JSONFeedReaderTests.swift in Sources */, 329 | CB3BE7F91ED0B579008D884F /* SimpleFeedTests.swift in Sources */, 330 | CB3BE7E81ECFAA53008D884F /* URLExtensionsTests.swift in Sources */, 331 | CB3BE7F01ECFC124008D884F /* ItemTests.swift in Sources */, 332 | CB3BE8041ED0C26B008D884F /* MockURLSessionDataTask.swift in Sources */, 333 | CB3BE7F31ED0ADB5008D884F /* TimetableFeedTests.swift in Sources */, 334 | CB3BE7E31ECFA409008D884F /* HubTests.swift in Sources */, 335 | CB3BE7E51ECFA4C8008D884F /* AuthorTests.swift in Sources */, 336 | CB3BE8021ED0BFFF008D884F /* MockURLSession.swift in Sources */, 337 | CB3BE7EE1ECFBF9E008D884F /* AttachmentTests.swift in Sources */, 338 | ); 339 | runOnlyForDeploymentPostprocessing = 0; 340 | }; 341 | /* End PBXSourcesBuildPhase section */ 342 | 343 | /* Begin PBXTargetDependency section */ 344 | CB3BE7C81ECF58E9008D884F /* PBXTargetDependency */ = { 345 | isa = PBXTargetDependency; 346 | target = CB3BE7BB1ECF58E9008D884F /* JSONFeed */; 347 | targetProxy = CB3BE7C71ECF58E9008D884F /* PBXContainerItemProxy */; 348 | }; 349 | /* End PBXTargetDependency section */ 350 | 351 | /* Begin XCBuildConfiguration section */ 352 | CB3BE7CE1ECF58E9008D884F /* Debug */ = { 353 | isa = XCBuildConfiguration; 354 | buildSettings = { 355 | ALWAYS_SEARCH_USER_PATHS = NO; 356 | CLANG_ANALYZER_NONNULL = YES; 357 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 358 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 359 | CLANG_CXX_LIBRARY = "libc++"; 360 | CLANG_ENABLE_MODULES = YES; 361 | CLANG_ENABLE_OBJC_ARC = YES; 362 | CLANG_WARN_BOOL_CONVERSION = YES; 363 | CLANG_WARN_CONSTANT_CONVERSION = YES; 364 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 365 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 366 | CLANG_WARN_EMPTY_BODY = YES; 367 | CLANG_WARN_ENUM_CONVERSION = YES; 368 | CLANG_WARN_INFINITE_RECURSION = YES; 369 | CLANG_WARN_INT_CONVERSION = YES; 370 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 371 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 372 | CLANG_WARN_UNREACHABLE_CODE = YES; 373 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 374 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 375 | COPY_PHASE_STRIP = NO; 376 | CURRENT_PROJECT_VERSION = 1; 377 | DEBUG_INFORMATION_FORMAT = dwarf; 378 | ENABLE_STRICT_OBJC_MSGSEND = YES; 379 | ENABLE_TESTABILITY = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu99; 381 | GCC_DYNAMIC_NO_PIC = NO; 382 | GCC_NO_COMMON_BLOCKS = YES; 383 | GCC_OPTIMIZATION_LEVEL = 0; 384 | GCC_PREPROCESSOR_DEFINITIONS = ( 385 | "DEBUG=1", 386 | "$(inherited)", 387 | ); 388 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 389 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 390 | GCC_WARN_UNDECLARED_SELECTOR = YES; 391 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 392 | GCC_WARN_UNUSED_FUNCTION = YES; 393 | GCC_WARN_UNUSED_VARIABLE = YES; 394 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 395 | MTL_ENABLE_DEBUG_INFO = YES; 396 | ONLY_ACTIVE_ARCH = YES; 397 | SDKROOT = iphoneos; 398 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 399 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 400 | TARGETED_DEVICE_FAMILY = "1,2"; 401 | VERSIONING_SYSTEM = "apple-generic"; 402 | VERSION_INFO_PREFIX = ""; 403 | }; 404 | name = Debug; 405 | }; 406 | CB3BE7CF1ECF58E9008D884F /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | ALWAYS_SEARCH_USER_PATHS = NO; 410 | CLANG_ANALYZER_NONNULL = YES; 411 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 412 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 413 | CLANG_CXX_LIBRARY = "libc++"; 414 | CLANG_ENABLE_MODULES = YES; 415 | CLANG_ENABLE_OBJC_ARC = YES; 416 | CLANG_WARN_BOOL_CONVERSION = YES; 417 | CLANG_WARN_CONSTANT_CONVERSION = YES; 418 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 419 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 420 | CLANG_WARN_EMPTY_BODY = YES; 421 | CLANG_WARN_ENUM_CONVERSION = YES; 422 | CLANG_WARN_INFINITE_RECURSION = YES; 423 | CLANG_WARN_INT_CONVERSION = YES; 424 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 425 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 426 | CLANG_WARN_UNREACHABLE_CODE = YES; 427 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 428 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 429 | COPY_PHASE_STRIP = NO; 430 | CURRENT_PROJECT_VERSION = 1; 431 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 432 | ENABLE_NS_ASSERTIONS = NO; 433 | ENABLE_STRICT_OBJC_MSGSEND = YES; 434 | GCC_C_LANGUAGE_STANDARD = gnu99; 435 | GCC_NO_COMMON_BLOCKS = YES; 436 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 437 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 438 | GCC_WARN_UNDECLARED_SELECTOR = YES; 439 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 440 | GCC_WARN_UNUSED_FUNCTION = YES; 441 | GCC_WARN_UNUSED_VARIABLE = YES; 442 | IPHONEOS_DEPLOYMENT_TARGET = 10.3; 443 | MTL_ENABLE_DEBUG_INFO = NO; 444 | SDKROOT = iphoneos; 445 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 446 | TARGETED_DEVICE_FAMILY = "1,2"; 447 | VALIDATE_PRODUCT = YES; 448 | VERSIONING_SYSTEM = "apple-generic"; 449 | VERSION_INFO_PREFIX = ""; 450 | }; 451 | name = Release; 452 | }; 453 | CB3BE7D11ECF58E9008D884F /* Debug */ = { 454 | isa = XCBuildConfiguration; 455 | buildSettings = { 456 | CLANG_ENABLE_MODULES = YES; 457 | CODE_SIGN_IDENTITY = ""; 458 | DEFINES_MODULE = YES; 459 | DYLIB_COMPATIBILITY_VERSION = 1; 460 | DYLIB_CURRENT_VERSION = 1; 461 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 462 | INFOPLIST_FILE = JSONFeed/Info.plist; 463 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 464 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 465 | PRODUCT_BUNDLE_IDENTIFIER = com.wesbillman.JSONFeed; 466 | PRODUCT_NAME = "$(TARGET_NAME)"; 467 | SKIP_INSTALL = YES; 468 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 469 | SWIFT_VERSION = 3.0; 470 | }; 471 | name = Debug; 472 | }; 473 | CB3BE7D21ECF58E9008D884F /* Release */ = { 474 | isa = XCBuildConfiguration; 475 | buildSettings = { 476 | CLANG_ENABLE_MODULES = YES; 477 | CODE_SIGN_IDENTITY = ""; 478 | DEFINES_MODULE = YES; 479 | DYLIB_COMPATIBILITY_VERSION = 1; 480 | DYLIB_CURRENT_VERSION = 1; 481 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 482 | INFOPLIST_FILE = JSONFeed/Info.plist; 483 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 484 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 485 | PRODUCT_BUNDLE_IDENTIFIER = com.wesbillman.JSONFeed; 486 | PRODUCT_NAME = "$(TARGET_NAME)"; 487 | SKIP_INSTALL = YES; 488 | SWIFT_VERSION = 3.0; 489 | }; 490 | name = Release; 491 | }; 492 | CB3BE7D41ECF58E9008D884F /* Debug */ = { 493 | isa = XCBuildConfiguration; 494 | buildSettings = { 495 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 496 | INFOPLIST_FILE = JSONFeedTests/Info.plist; 497 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 498 | PRODUCT_BUNDLE_IDENTIFIER = com.wesbillman.JSONFeedTests; 499 | PRODUCT_NAME = "$(TARGET_NAME)"; 500 | SWIFT_VERSION = 3.0; 501 | }; 502 | name = Debug; 503 | }; 504 | CB3BE7D51ECF58E9008D884F /* Release */ = { 505 | isa = XCBuildConfiguration; 506 | buildSettings = { 507 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 508 | INFOPLIST_FILE = JSONFeedTests/Info.plist; 509 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 510 | PRODUCT_BUNDLE_IDENTIFIER = com.wesbillman.JSONFeedTests; 511 | PRODUCT_NAME = "$(TARGET_NAME)"; 512 | SWIFT_VERSION = 3.0; 513 | }; 514 | name = Release; 515 | }; 516 | /* End XCBuildConfiguration section */ 517 | 518 | /* Begin XCConfigurationList section */ 519 | CB3BE7B61ECF58E9008D884F /* Build configuration list for PBXProject "JSONFeed" */ = { 520 | isa = XCConfigurationList; 521 | buildConfigurations = ( 522 | CB3BE7CE1ECF58E9008D884F /* Debug */, 523 | CB3BE7CF1ECF58E9008D884F /* Release */, 524 | ); 525 | defaultConfigurationIsVisible = 0; 526 | defaultConfigurationName = Release; 527 | }; 528 | CB3BE7D01ECF58E9008D884F /* Build configuration list for PBXNativeTarget "JSONFeed" */ = { 529 | isa = XCConfigurationList; 530 | buildConfigurations = ( 531 | CB3BE7D11ECF58E9008D884F /* Debug */, 532 | CB3BE7D21ECF58E9008D884F /* Release */, 533 | ); 534 | defaultConfigurationIsVisible = 0; 535 | defaultConfigurationName = Release; 536 | }; 537 | CB3BE7D31ECF58E9008D884F /* Build configuration list for PBXNativeTarget "JSONFeedTests" */ = { 538 | isa = XCConfigurationList; 539 | buildConfigurations = ( 540 | CB3BE7D41ECF58E9008D884F /* Debug */, 541 | CB3BE7D51ECF58E9008D884F /* Release */, 542 | ); 543 | defaultConfigurationIsVisible = 0; 544 | defaultConfigurationName = Release; 545 | }; 546 | /* End XCConfigurationList section */ 547 | }; 548 | rootObject = CB3BE7B31ECF58E9008D884F /* Project object */; 549 | } 550 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/xcshareddata/xcschemes/JSONFeed.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 66 | 72 | 73 | 74 | 75 | 76 | 77 | 83 | 84 | 90 | 91 | 92 | 93 | 95 | 96 | 99 | 100 | 101 | -------------------------------------------------------------------------------- /JSONFeed/Attachment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Attachment { 9 | public let url: URL 10 | public let mimeType: String 11 | public let title: String? 12 | public let bytes: Int? 13 | public let seconds: Int? 14 | 15 | public init(json: [AnyHashable: Any]) throws { 16 | guard let url = URL(string: json["url"] as? String) else { 17 | throw JSONFeedError.invalidURL 18 | } 19 | 20 | guard let mimeType = json["mime_type"] as? String else { 21 | throw JSONFeedError.invalidMimeType 22 | } 23 | 24 | self.url = url 25 | self.mimeType = mimeType 26 | self.title = json["title"] as? String 27 | self.bytes = json["size_in_bytes"] as? Int 28 | self.seconds = json["duration_in_seconds"] as? Int 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /JSONFeed/Author.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Author { 9 | public let name: String? 10 | public let url: URL? 11 | public let avatar: URL? 12 | 13 | public init(json: [AnyHashable: Any]) { 14 | self.name = json["name"] as? String 15 | self.url = URL(string: json["url"] as? String) 16 | self.avatar = URL(string: json["avatar"] as? String) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JSONFeed/Extensions/URLExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | extension URL { 9 | init?(string: String?) { 10 | guard let string = string else { 11 | return nil 12 | } 13 | self.init(string: string) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /JSONFeed/Hub.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Hub { 9 | public let type: String 10 | public let url: URL 11 | 12 | public init?(json: [AnyHashable: Any]) { 13 | guard let type = json["type"] as? String else { 14 | return nil 15 | } 16 | 17 | guard let url = URL(string: json["url"] as? String) else { 18 | return nil 19 | } 20 | 21 | self.type = type 22 | self.url = url 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /JSONFeed/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /JSONFeed/Item.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public struct Item { 9 | public let id: String 10 | public let url: URL? 11 | public let external: URL? 12 | public let title: String? 13 | public let text: String? 14 | public let html: String? 15 | public let summary: String? 16 | public let image: URL? 17 | public let banner: URL? 18 | public let published: Date 19 | public let modified: Date? 20 | public let author: Author? 21 | public let tags: [String]? 22 | public let attachments: [Attachment] 23 | 24 | public init(json: [AnyHashable: Any]) throws { 25 | guard let id = json["id"] as? String else { 26 | throw JSONFeedError.invalidID 27 | } 28 | 29 | self.id = id 30 | self.url = URL(string: json["url"] as? String) 31 | self.external = URL(string: json["external_url"] as? String) 32 | self.title = json["title"] as? String 33 | self.text = json["content_text"] as? String 34 | self.html = json["content_html"] as? String 35 | self.summary = json["summary"] as? String 36 | self.image = URL(string: json["image"] as? String) 37 | self.banner = URL(string: json["banner_image"] as? String) 38 | 39 | if let dateString = json["date_published"] as? String, let date = ISO8601DateFormatter().date(from: dateString) { 40 | self.published = date 41 | } else { 42 | self.published = Date() 43 | } 44 | 45 | if let dateString = json["date_modified"] as? String, let date = ISO8601DateFormatter().date(from: dateString) { 46 | self.modified = date 47 | } else { 48 | self.modified = Date() 49 | } 50 | 51 | if let authorJSON = json["author"] as? [AnyHashable: Any] { 52 | self.author = Author(json: authorJSON) 53 | } else { 54 | self.author = nil 55 | } 56 | 57 | if let tagsJSON = json["tags"] as? [String] { 58 | self.tags = tagsJSON.flatMap(String.init) 59 | } else { 60 | self.tags = nil 61 | } 62 | 63 | if let attachmentsJSON = json["attachments"] as? [[AnyHashable: Any]] { 64 | self.attachments = attachmentsJSON.flatMap { 65 | try? Attachment(json: $0) 66 | } 67 | } else { 68 | self.attachments = [] 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /JSONFeed/JSONFeed.h: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | #import 7 | 8 | //! Project version number for JSONFeed. 9 | FOUNDATION_EXPORT double JSONFeedVersionNumber; 10 | 11 | //! Project version string for JSONFeed. 12 | FOUNDATION_EXPORT const unsigned char JSONFeedVersionString[]; 13 | -------------------------------------------------------------------------------- /JSONFeed/JSONFeed.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public class JSONFeed { 9 | public let version: URL 10 | public let title: String 11 | public let homePage: URL? 12 | public let feed: URL? 13 | public let description: String? 14 | public let userComment: String? 15 | public let next: URL? 16 | public let icon: URL? 17 | public let favicon: URL? 18 | public let author: Author? 19 | public let hasExpired: Bool 20 | public let hubs: [Hub]? 21 | public let items: [Item] 22 | 23 | public init(json: [AnyHashable: Any]) throws { 24 | guard let version = URL(string: json["version"] as? String) else { 25 | throw JSONFeedError.invalidVersion 26 | } 27 | 28 | guard let title = json["title"] as? String else { 29 | throw JSONFeedError.invalidTitle 30 | } 31 | 32 | self.version = version 33 | self.title = title 34 | self.homePage = URL(string: json["home_page_url"] as? String) 35 | self.feed = URL(string: json["feed_url"] as? String) 36 | self.description = json["description"] as? String 37 | self.userComment = json["user_comment"] as? String 38 | self.next = URL(string: json["next_url"] as? String) 39 | self.icon = URL(string: json["icon"] as? String) 40 | self.favicon = URL(string: json["favicon"] as? String) 41 | 42 | if let authorJSON = json["author"] as? [AnyHashable: Any] { 43 | self.author = Author(json: authorJSON) 44 | } else { 45 | self.author = nil 46 | } 47 | 48 | if let hubsJSON = json["hubs"] as? [[AnyHashable: Any]] { 49 | self.hubs = hubsJSON.flatMap(Hub.init) 50 | } else { 51 | self.hubs = nil 52 | } 53 | 54 | if let itemsJSON = json["items"] as? [[AnyHashable: Any]] { 55 | self.items = itemsJSON.flatMap { 56 | try? Item(json: $0) 57 | } 58 | } else { 59 | self.items = [] 60 | } 61 | 62 | self.hasExpired = json["expired"] as? Bool ?? false 63 | } 64 | 65 | public convenience init(data: Data) throws { 66 | let result = try? JSONSerialization.jsonObject(with: data, options: []) 67 | guard let json = result as? [AnyHashable: Any] else { 68 | throw JSONFeedError.invalidData 69 | } 70 | try self.init(json: json) 71 | } 72 | 73 | public convenience init(string: String) throws { 74 | guard let data = string.data(using: .utf8), !data.isEmpty else { 75 | throw JSONFeedError.invalidString 76 | } 77 | try self.init(data: data) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /JSONFeed/JSONFeedError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public enum JSONFeedError: Error { 9 | case notAJSONFeed 10 | case invalidVersion 11 | case invalidTitle 12 | case invalidID 13 | case invalidURL 14 | case invalidMimeType 15 | case invalidData 16 | case invalidString 17 | } 18 | -------------------------------------------------------------------------------- /JSONFeed/JSONFeedReader.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | public enum JSONFeedReaderError: Error { 9 | case invalidRequestString 10 | case emptyResponseData 11 | } 12 | 13 | public class JSONFeedReader { 14 | private let session: URLSession 15 | 16 | public init(session: URLSession = URLSession(configuration: URLSessionConfiguration.default)) { 17 | self.session = session 18 | } 19 | 20 | public func read(url: URL, complete: @escaping (JSONFeed?, Error?) -> Void) { 21 | let task = session.dataTask(with: URLRequest(url: url)) { (data, _, error) in 22 | if let error = error { 23 | complete(nil, error) 24 | return 25 | } 26 | guard let data = data else { 27 | complete(nil, JSONFeedReaderError.emptyResponseData) 28 | return 29 | } 30 | do { 31 | let feed = try JSONFeed(data: data) 32 | complete(feed, nil) 33 | } catch { 34 | complete(nil, error) 35 | } 36 | } 37 | task.resume() 38 | } 39 | 40 | public func read(string: String, complete: @escaping (JSONFeed?, Error?) -> Void) { 41 | guard let url = URL(string: string) else { 42 | complete(nil, JSONFeedReaderError.invalidRequestString) 43 | return 44 | } 45 | read(url: url, complete: complete) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /JSONFeedTests/AttachmentTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class AttachmentTests: XCTestCase { 10 | let url = "https://jsonfeed.org/version/1" 11 | let text = "Some Text" 12 | let size = 120 13 | let duration = 60 14 | 15 | func testInvalidURL() { 16 | XCTAssertThrowsError(try Attachment(json: [:])) { error in 17 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidURL) 18 | } 19 | } 20 | 21 | func testInvalidMimeType() { 22 | XCTAssertThrowsError(try Attachment(json: ["url": url])) { error in 23 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidMimeType) 24 | } 25 | } 26 | 27 | func testValidAttachment() { 28 | let json: [String : Any] = [ 29 | "url": url, 30 | "mime_type": text, 31 | "title": text, 32 | "size_in_bytes": size, 33 | "duration_in_seconds": duration, 34 | ] 35 | let attachment = try? Attachment(json: json) 36 | XCTAssertEqual(attachment?.url.absoluteString, url) 37 | XCTAssertEqual(attachment?.mimeType, text) 38 | XCTAssertEqual(attachment?.title, text) 39 | XCTAssertEqual(attachment?.bytes, size) 40 | XCTAssertEqual(attachment?.seconds, duration) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /JSONFeedTests/AuthorTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class AuthorTests: XCTestCase { 10 | let url = "https://jsonfeed.org/version/1" 11 | let text = "Some Text" 12 | 13 | func testEmptyJson() { 14 | let author = Author(json: [:]) 15 | XCTAssertEqual(author.name, nil) 16 | XCTAssertEqual(author.url, nil) 17 | XCTAssertEqual(author.avatar, nil) 18 | } 19 | 20 | func testValidJson() { 21 | let author = Author(json: ["name": text, "url": url, "avatar": url]) 22 | XCTAssertEqual(author.name, text) 23 | XCTAssertEqual(author.url?.absoluteString, url) 24 | XCTAssertEqual(author.avatar?.absoluteString, url) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /JSONFeedTests/Extensions/URLExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class URLExtensionsTests: XCTestCase { 10 | func testNilString() { 11 | XCTAssertEqual(URL(string: nil), nil) 12 | } 13 | 14 | func testValidURL() { 15 | let string: String? = "http://example.com" 16 | let url = URL(string: string) 17 | XCTAssertEqual(url?.absoluteString, string) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /JSONFeedTests/Feeds/SimpleFeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class SimpleFeedTests: XCTestCase { 10 | func testSimpleFeedData() { 11 | guard let url = Bundle(for: type(of: self)).url(forResource: "simple", withExtension: "json") else { 12 | XCTFail("json file does not exist") 13 | return 14 | } 15 | let feed = try? JSONFeed(data: Data(contentsOf: url)) 16 | XCTAssertEqual(feed?.version.absoluteString, "https://jsonfeed.org/version/1") 17 | XCTAssertEqual(feed?.title, "My Example Feed") 18 | XCTAssertEqual(feed?.homePage?.absoluteString, "https://example.org/") 19 | XCTAssertEqual(feed?.feed?.absoluteString, "https://example.org/feed.json") 20 | XCTAssertEqual(feed?.items.count, 2) 21 | XCTAssertEqual(feed?.items.first?.id, "2") 22 | XCTAssertEqual(feed?.items.first?.url?.absoluteString, "https://example.org/second-item") 23 | XCTAssertEqual(feed?.items.first?.text, "This is a second item.") 24 | } 25 | 26 | func testSimpleFeedString() { 27 | guard let url = Bundle(for: type(of: self)).url(forResource: "simple", withExtension: "json") else { 28 | XCTFail("json file does not exist") 29 | return 30 | } 31 | guard let string = try? String(contentsOf: url) else { 32 | XCTFail("json file is not text") 33 | return 34 | } 35 | let feed = try? JSONFeed(string: string) 36 | XCTAssertEqual(feed?.version.absoluteString, "https://jsonfeed.org/version/1") 37 | XCTAssertEqual(feed?.title, "My Example Feed") 38 | XCTAssertEqual(feed?.homePage?.absoluteString, "https://example.org/") 39 | XCTAssertEqual(feed?.feed?.absoluteString, "https://example.org/feed.json") 40 | XCTAssertEqual(feed?.items.count, 2) 41 | XCTAssertEqual(feed?.items.first?.id, "2") 42 | XCTAssertEqual(feed?.items.first?.url?.absoluteString, "https://example.org/second-item") 43 | XCTAssertEqual(feed?.items.first?.text, "This is a second item.") 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /JSONFeedTests/Feeds/TimetableFeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class TimetableFeedTests: XCTestCase { 10 | func testLoadingTimelineFeed() { 11 | guard let url = Bundle(for: type(of: self)).url(forResource: "timetable", withExtension: "json") else { 12 | XCTFail("json file does not exist") 13 | return 14 | } 15 | let feed = try? JSONFeed(data: Data(contentsOf: url)) 16 | XCTAssertEqual(feed?.version.absoluteString, "https://jsonfeed.org/version/1") 17 | XCTAssertEqual(feed?.title, "Timetable") 18 | XCTAssertEqual(feed?.homePage?.absoluteString, "http://timetable.manton.org/") 19 | XCTAssertEqual(feed?.items.count, 5) 20 | XCTAssertEqual(feed?.items.first?.id, "http://timetable.manton.org/2017/04/episode-45-launch-week/") 21 | XCTAssertEqual(feed?.items.first?.url?.absoluteString, "http://timetable.manton.org/2017/04/episode-45-launch-week/") 22 | XCTAssertEqual(feed?.items.first?.title, "Episode 45: Launch week") 23 | XCTAssertEqual(feed?.items.first?.html, "I’m rolling out early access to Micro.blog this week. I talk about how the first 2 days have gone, mistakes with TestFlight, and what to do next.") 24 | XCTAssertEqual(feed?.items.first?.published, ISO8601DateFormatter().date(from: "2017-04-26T01:09:45+00:00")) 25 | XCTAssertEqual(feed?.items.first?.attachments.count, 1) 26 | XCTAssertEqual(feed?.items.first?.attachments.first?.url.absoluteString, "http://timetable.manton.org/podcast-download/139/episode-45-launch-week.mp3") 27 | XCTAssertEqual(feed?.items.first?.attachments.first?.mimeType, "audio/mpeg") 28 | XCTAssertEqual(feed?.items.first?.attachments.first?.bytes, 5236920) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /JSONFeedTests/Feeds/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "title": "My Example Feed", 4 | "home_page_url": "https://example.org/", 5 | "feed_url": "https://example.org/feed.json", 6 | "items": [ 7 | { 8 | "id": "2", 9 | "content_text": "This is a second item.", 10 | "url": "https://example.org/second-item" 11 | }, 12 | { 13 | "id": "1", 14 | "content_html": "

Hello, world!

", 15 | "url": "https://example.org/initial-post" 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /JSONFeedTests/Feeds/timetable.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "https://jsonfeed.org/version/1", 3 | "title": "Timetable", 4 | "home_page_url": "http://timetable.manton.org/", 5 | "items": [{ 6 | "id": "http://timetable.manton.org/2017/04/episode-45-launch-week/", 7 | "url": "http://timetable.manton.org/2017/04/episode-45-launch-week/", 8 | "title": "Episode 45: Launch week", 9 | "content_html": "I’m rolling out early access to Micro.blog this week. I talk about how the first 2 days have gone, mistakes with TestFlight, and what to do next.", 10 | "date_published": "2017-04-26T01:09:45+00:00", 11 | "attachments": [{ 12 | "url": "http://timetable.manton.org/podcast-download/139/episode-45-launch-week.mp3", 13 | "mime_type": "audio/mpeg", 14 | "size_in_bytes": 5236920 15 | }] 16 | }, 17 | { 18 | "id": "http://timetable.manton.org/2017/04/episode-44-disappoint-some-people/", 19 | "url": "http://timetable.manton.org/2017/04/episode-44-disappoint-some-people/", 20 | "title": "Episode 44: Disappoint some people", 21 | "content_html": "I look back at Katy Perry’s performance at the Grammys. Why it’s okay to make a political statement and risk disappointing some fans.", 22 | "date_published": "2017-04-20T16:59:09+00:00", 23 | "attachments": [{ 24 | "url": "http://timetable.manton.org/podcast-download/136/episode-44-disappoint-some-people.mp3", 25 | "mime_type": "audio/mpeg", 26 | "size_in_bytes": 3590496 27 | }] 28 | }, 29 | { 30 | "id": "http://timetable.manton.org/2017/04/episode-43-default-themes/", 31 | "url": "http://timetable.manton.org/2017/04/episode-43-default-themes/", 32 | "title": "Episode 43: Default themes", 33 | "content_html": "Finalizing the default themes for Micro.blog, why they’re based on Jekyll, and a thousand beta testers.", 34 | "date_published": "2017-04-18T18:24:39+00:00", 35 | "attachments": [{ 36 | "url": "http://timetable.manton.org/podcast-download/134/episode-43-default-themes.mp3", 37 | "mime_type": "audio/mpeg", 38 | "size_in_bytes": 4400851 39 | }] 40 | }, 41 | { 42 | "id": "http://timetable.manton.org/2017/04/episode-42-officially-announced/", 43 | "url": "http://timetable.manton.org/2017/04/episode-42-officially-announced/", 44 | "title": "Episode 42: Officially announced", 45 | "content_html": "Today I sent a Kickstarter update about the rollout date. I also talk about the invite code system, the book, and stickers.", 46 | "date_published": "2017-04-17T22:34:14+00:00", 47 | "attachments": [{ 48 | "url": "http://timetable.manton.org/podcast-download/132/episode-42-officially-announced.mp3", 49 | "mime_type": "audio/mpeg", 50 | "size_in_bytes": 7436651 51 | }] 52 | }, 53 | { 54 | "id": "http://timetable.manton.org/2017/04/episode-41-final-rollout-date/", 55 | "url": "http://timetable.manton.org/2017/04/episode-41-final-rollout-date/", 56 | "title": "Episode 41: Final rollout date", 57 | "content_html": "Wrapping up a busy week, I realize there’s too much going on to start the Micro.blog rollout next week. It will be the following week, April 24th.", 58 | "date_published": "2017-04-15T02:11:02+00:00", 59 | "attachments": [{ 60 | "url": "http://timetable.manton.org/podcast-download/129/episode-41-final-rollout-date.mp3", 61 | "mime_type": "audio/mpeg", 62 | "size_in_bytes": 3796695 63 | }] 64 | } 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /JSONFeedTests/HubTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class HUBTests: XCTestCase { 10 | let url = "https://jsonfeed.org/version/1" 11 | let text = "Some Text" 12 | 13 | func testInvalidType() { 14 | XCTAssertNil(Hub(json: [:])) 15 | } 16 | 17 | func testInvalidURL() { 18 | XCTAssertNil(Hub(json: ["type": text])) 19 | } 20 | 21 | func testValidHub() { 22 | let hub = Hub(json: ["type": text, "url": url]) 23 | XCTAssertEqual(hub?.type, text) 24 | XCTAssertEqual(hub?.url.absoluteString, url) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /JSONFeedTests/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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /JSONFeedTests/ItemTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class ItemTests: XCTestCase { 10 | let url = "https://jsonfeed.org/version/1" 11 | let text = "Some Text" 12 | let date = "2017-04-26T01:09:45+00:00" 13 | 14 | func testInvalidURL() { 15 | XCTAssertThrowsError(try Item(json: [:])) { error in 16 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidID) 17 | } 18 | } 19 | 20 | func testValidAttachment() { 21 | let json: [String : Any] = [ 22 | "id": text, 23 | "url": url, 24 | "external_url": url, 25 | "title": text, 26 | "content_text": text, 27 | "content_html": text, 28 | "summary": text, 29 | "image": url, 30 | "banner_image": url, 31 | "date_published": date, 32 | "date_modified": date, 33 | "author": ["name": text, "url": url, "avatar": url], 34 | "tags": ["one", "two"], 35 | "attachments": [["url": url, "mime_type": text]] 36 | ] 37 | let item = try? Item(json: json) 38 | XCTAssertEqual(item?.id, text) 39 | XCTAssertEqual(item?.url?.absoluteString, url) 40 | XCTAssertEqual(item?.external?.absoluteString, url) 41 | XCTAssertEqual(item?.title, text) 42 | XCTAssertEqual(item?.text, text) 43 | XCTAssertEqual(item?.html, text) 44 | XCTAssertEqual(item?.image?.absoluteString, url) 45 | XCTAssertEqual(item?.banner?.absoluteString, url) 46 | XCTAssertEqual(item?.published, ISO8601DateFormatter().date(from: date)) 47 | XCTAssertEqual(item?.modified, ISO8601DateFormatter().date(from: date)) 48 | XCTAssertEqual(item?.author?.name, text) 49 | XCTAssertEqual(item?.author?.url?.absoluteString, url) 50 | XCTAssertEqual(item?.author?.avatar?.absoluteString, url) 51 | XCTAssertEqual(item?.tags?.count, 2) 52 | XCTAssertEqual(item?.tags?[0], "one") 53 | XCTAssertEqual(item?.tags?[1], "two") 54 | XCTAssertEqual(item?.attachments.count, 1) 55 | XCTAssertEqual(item?.attachments.first?.url.absoluteString, url) 56 | XCTAssertEqual(item?.attachments.first?.mimeType, text) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /JSONFeedTests/JSONFeedReaderTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class JSONFeedReaderTests: XCTestCase { 10 | enum TestError: Error { 11 | case sessionError 12 | } 13 | 14 | private let url = "https://jsonfeed.org/version/1" 15 | private let text = "Some Text" 16 | 17 | private var session: MockURLSession! 18 | private var subject: JSONFeedReader! 19 | 20 | var validData: Data { 21 | let json = ["version": url, "title": text] 22 | guard let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) else { 23 | return Data() 24 | } 25 | return data 26 | } 27 | 28 | override func setUp() { 29 | super.setUp() 30 | session = MockURLSession() 31 | subject = JSONFeedReader(session: session) 32 | } 33 | 34 | func testInvalidURLString() { 35 | var resultFeed: JSONFeed? = nil 36 | var resultError: Error? = nil 37 | let nilString: String? = nil 38 | 39 | subject.read(string: "bogus\\\(String(describing: nilString))") { (feed, error) in 40 | resultFeed = feed 41 | resultError = error 42 | } 43 | XCTAssert(!session.task.resumed) 44 | XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.invalidRequestString) 45 | XCTAssertNil(resultFeed) 46 | } 47 | 48 | func testSessionError() { 49 | session.error = TestError.sessionError 50 | var resultFeed: JSONFeed? = nil 51 | var resultError: Error? = nil 52 | 53 | subject.read(string: "example.com") { (feed, error) in 54 | resultFeed = feed 55 | resultError = error 56 | } 57 | XCTAssert(session.task.resumed) 58 | XCTAssertEqual(resultError as? TestError, TestError.sessionError) 59 | XCTAssertNil(resultFeed) 60 | } 61 | 62 | func testNilData() { 63 | session.data = nil 64 | var resultFeed: JSONFeed? = nil 65 | var resultError: Error? = nil 66 | 67 | subject.read(string: "example.com") { (feed, error) in 68 | resultFeed = feed 69 | resultError = error 70 | } 71 | XCTAssert(session.task.resumed) 72 | XCTAssertEqual(resultError as? JSONFeedReaderError, JSONFeedReaderError.emptyResponseData) 73 | XCTAssertNil(resultFeed) 74 | } 75 | 76 | func testInvalidData() { 77 | guard let data = "bogus".data(using: .utf8) else { 78 | XCTFail() 79 | return 80 | } 81 | session.data = data 82 | var resultFeed: JSONFeed? = nil 83 | var resultError: Error? = nil 84 | 85 | subject.read(string: "example.com") { (feed, error) in 86 | resultFeed = feed 87 | resultError = error 88 | } 89 | XCTAssert(session.task.resumed) 90 | XCTAssertEqual(resultError as? JSONFeedError, JSONFeedError.invalidData) 91 | XCTAssertNil(resultFeed) 92 | } 93 | 94 | func testReadingFeedURL() { 95 | session.data = validData 96 | var resultFeed: JSONFeed? = nil 97 | var resultError: Error? = nil 98 | 99 | guard let feedUrl = URL(string: "example.com") else { 100 | XCTFail("unable to create url") 101 | return 102 | } 103 | 104 | subject.read(url: feedUrl) { (feed, error) in 105 | resultFeed = feed 106 | resultError = error 107 | } 108 | XCTAssert(session.task.resumed) 109 | XCTAssertEqual(resultFeed?.version.absoluteString, url) 110 | XCTAssertEqual(resultFeed?.title, text) 111 | XCTAssertNil(resultError) 112 | } 113 | 114 | func testReadingFeedURLString() { 115 | session.data = validData 116 | var resultFeed: JSONFeed? = nil 117 | var resultError: Error? = nil 118 | 119 | subject.read(string: "example.com") { (feed, error) in 120 | resultFeed = feed 121 | resultError = error 122 | } 123 | XCTAssert(session.task.resumed) 124 | XCTAssertEqual(resultFeed?.version.absoluteString, url) 125 | XCTAssertEqual(resultFeed?.title, text) 126 | XCTAssertNil(resultError) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /JSONFeedTests/JSONFeedTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/19/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import XCTest 7 | @testable import JSONFeed 8 | 9 | class JSONFeedTests: XCTestCase { 10 | let url = "https://jsonfeed.org/version/1" 11 | let text = "Some Text" 12 | 13 | func testInvalidVersion() { 14 | XCTAssertThrowsError(try JSONFeed(json: [:])) { error in 15 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidVersion) 16 | } 17 | } 18 | 19 | func testInvalidTitle() { 20 | let json = ["version": url] 21 | XCTAssertThrowsError(try JSONFeed(json: json)) { error in 22 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidTitle) 23 | } 24 | } 25 | 26 | func testInvalidData() { 27 | guard let data = "bogus".data(using: .utf8) else { 28 | XCTFail() 29 | return 30 | } 31 | XCTAssertThrowsError(try JSONFeed(data: data)) { error in 32 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidData) 33 | } 34 | } 35 | 36 | func testInvalidString() { 37 | XCTAssertThrowsError(try JSONFeed(string: "")) { error in 38 | XCTAssertEqual(error as? JSONFeedError, JSONFeedError.invalidString) 39 | } 40 | } 41 | 42 | func testEmptyFeed() { 43 | let json = ["version": url, "title": text] 44 | let feed = try? JSONFeed(json: json) 45 | XCTAssertEqual(feed?.version.absoluteString, url) 46 | XCTAssertEqual(feed?.title, text) 47 | } 48 | 49 | func testFullyPopulatedFeed() { 50 | let json: [String : Any] = [ 51 | "version": url, 52 | "title": text, 53 | "home_page_url": url, 54 | "feed_url": url, 55 | "description": text, 56 | "user_comment": text, 57 | "next_url": url, 58 | "icon": url, 59 | "favicon": url, 60 | "author": ["name": text, "url": url, "avatar": url], 61 | "hubs": [["type": text, "url": url]], 62 | "items": [["id": text]] 63 | ] 64 | let feed = try? JSONFeed(json: json) 65 | XCTAssertEqual(feed?.version.absoluteString, url) 66 | XCTAssertEqual(feed?.title, text) 67 | XCTAssertEqual(feed?.homePage?.absoluteString, url) 68 | XCTAssertEqual(feed?.feed?.absoluteString, url) 69 | XCTAssertEqual(feed?.description, text) 70 | XCTAssertEqual(feed?.userComment, text) 71 | XCTAssertEqual(feed?.next?.absoluteString, url) 72 | XCTAssertEqual(feed?.icon?.absoluteString, url) 73 | XCTAssertEqual(feed?.favicon?.absoluteString, url) 74 | XCTAssertEqual(feed?.author?.name, text) 75 | XCTAssertEqual(feed?.author?.url?.absoluteString, url) 76 | XCTAssertEqual(feed?.author?.avatar?.absoluteString, url) 77 | XCTAssertEqual(feed?.hubs?.count, 1) 78 | XCTAssertEqual(feed?.hubs?.first?.type, text) 79 | XCTAssertEqual(feed?.hubs?.first?.url.absoluteString, url) 80 | XCTAssertEqual(feed?.items.count, 1) 81 | XCTAssertEqual(feed?.items.first?.id, text) 82 | } 83 | 84 | func testFeedFromData() { 85 | let json = ["version": url, "title": text] 86 | guard let data = try? JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) else { 87 | XCTFail() 88 | return 89 | } 90 | 91 | let feed = try? JSONFeed(data: data) 92 | XCTAssertEqual(feed?.version.absoluteString, url) 93 | XCTAssertEqual(feed?.title, text) 94 | } 95 | 96 | func testFeedFromString() { 97 | let string = "{\"version\": \"\(url)\", \"title\": \"\(text)\"}" 98 | 99 | let feed = try? JSONFeed(string: string) 100 | XCTAssertEqual(feed?.version.absoluteString, url) 101 | XCTAssertEqual(feed?.title, text) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /JSONFeedTests/Mocks/MockURLSession.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | class MockURLSession: URLSession { 9 | let task = MockURLSessionDataTask() 10 | var data: Data? 11 | var response: URLResponse? 12 | var error: Error? 13 | 14 | override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { 15 | completionHandler(data, response, error) 16 | return task 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JSONFeedTests/Mocks/MockURLSessionDataTask.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Created by Wes Billman on 5/20/17. 3 | // Copyright © 2017 wesbillman. All rights reserved. 4 | // 5 | 6 | import Foundation 7 | 8 | class MockURLSessionDataTask: URLSessionDataTask { 9 | private(set) var resumed = false 10 | override func resume() { 11 | resumed = true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Toto Tvalavadze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "JSONFeed", 5 | exclude: ["JSONFeed.xcodeproj", "JSONFeedTests", ".travis.yml", "codecov.yml", ".swiftlint.yml"] 6 | ) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/wesbillman/JSONFeed.svg?branch=master)](https://travis-ci.org/wesbillman/JSONFeed) 2 | [![Codecov](https://img.shields.io/codecov/c/github/wesbillman/JSONFeed.svg)](https://codecov.io/gh/wesbillman/JSONFeed) 3 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 4 | ![SwiftPM Compatible](https://img.shields.io/badge/SwiftPM-Compatible-brightgreen.svg) 5 | 6 | # JSONFeed 7 | 8 | Swift parsing for [JSON Feed](https://jsonfeed.org/) [Spec](https://jsonfeed.org/version/1) 9 | 10 | ## Installation 11 | 12 | ### Carthage 13 | 14 | You can install [Carthage](https://github.com/Carthage/Carthage) with [Homebrew](http://brew.sh/) using the following command: 15 | 16 | ```bash 17 | brew update 18 | brew install carthage 19 | ``` 20 | To integrate JSONFeed into your Xcode project using Carthage, specify it in your `Cartfile` where `"x.x.x"` is the current release: 21 | 22 | ```ogdl 23 | github "wesbillman/JSONFeed" "x.x.x" 24 | ``` 25 | 26 | ### Swift Package Manager 27 | 28 | To install using [Swift Package Manager](https://swift.org/package-manager/) have your Swift package set up, and add JSONFeed as a dependency to your `Package.swift`. 29 | 30 | ```swift 31 | dependencies: [ 32 | .Package(url: "https://github.com/wesbillman/JSONFeed.git", majorVersion: 0) 33 | ] 34 | ``` 35 | 36 | ### Manually 37 | Add all the files from `JSONFeed/JSONFeed` to your project 38 | 39 | ## Usage 40 | 41 | > See [JSONFeedTests](https://github.com/wesbillman/JSONFeed/blob/master/JSONFeedTests/JSONFeedTests.swift) for detailed usage examples 42 | 43 | #### Load a feed from a dictionary 44 | 45 | ```swift 46 | let dictionary = 47 | let feed = try? JSONFeed(json: dictionary) 48 | ``` 49 | 50 | #### Load a feed from data 51 | 52 | ```swift 53 | let data = 54 | let feed = try? JSONFeed(data: data) 55 | ``` 56 | 57 | #### Load a feed from a json ut8f string 58 | 59 | ```swift 60 | let string = 61 | let feed = try? JSONFeed(string: string) 62 | ``` 63 | ### Reading from a feed via URLSession 64 | 65 | Using default configuration and URLSession 66 | ```swift 67 | let reader = JSONFeedReader() 68 | reader.read(string: "https://jsonfeed.org/feed.json") { (feed, error) in 69 | if let error = error { 70 | //bad things happened 71 | } 72 | 73 | if let feed = feed { 74 | //good things happened 75 | } 76 | } 77 | ``` 78 | 79 | Using custom implemenation of URLSession (example: for unit testing) 80 | ```swift 81 | let reader = JSONFeedReader(session: SomeCustomURLSession) 82 | reader.read(string: "https://jsonfeed.org/feed.json") { (feed, error) in 83 | if let error = error { 84 | //bad things happened 85 | } 86 | 87 | if let feed = feed { 88 | //good things happened 89 | } 90 | } 91 | ``` 92 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "JSONFeedTests" 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | against: parent 8 | target: auto 9 | threshold: 1% 10 | --------------------------------------------------------------------------------