├── .github └── workflows │ ├── ci.yml │ └── documentation.yml ├── .gitignore ├── Example.playground ├── Contents.swift └── contents.xcplayground ├── JSONFeed.xcodeproj ├── JSONFeedTests_Info.plist ├── JSONFeed_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── xcshareddata │ └── xcschemes │ ├── JSONFeed.xcscheme │ └── xcschememanagement.plist ├── JSONFeed.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── JSONFeed │ ├── Attachment.swift │ ├── Author.swift │ ├── Feed.swift │ ├── Hub.swift │ └── Item.swift └── Tests └── JSONFeedTests └── JSONFeedTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | macos: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v1 16 | - name: Build and Test 17 | run: swift test 18 | 19 | linux: 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | matrix: 24 | swift: ["5.1", "5.2", "latest"] 25 | 26 | container: 27 | image: swift:${{ matrix.swift }} 28 | 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v1 32 | - name: Build and Test 33 | run: swift test --enable-test-discovery 34 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths: 8 | - .github/workflows/documentation.yml 9 | - Sources/**.swift 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v1 18 | - name: Generate Documentation 19 | uses: SwiftDocOrg/swift-doc@master 20 | with: 21 | inputs: "Sources/SwiftDoc" 22 | output: "Documentation" 23 | - name: Upload Documentation to Wiki 24 | uses: SwiftDocOrg/github-wiki-publish-action@v1 25 | with: 26 | path: "Documentation" 27 | env: 28 | GH_PERSONAL_ACCESS_TOKEN: ${{ secrets.GH_PERSONAL_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | -------------------------------------------------------------------------------- /Example.playground/Contents.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import JSONFeed 3 | import PlaygroundSupport 4 | 5 | let url = URL(string: "https://flight.school/feed.json")! 6 | URLSession.shared.dataTask(with: url) { (data, _, error) in 7 | guard let data = data, error == nil else { 8 | fatalError("\(error?.localizedDescription ?? "request failed")") 9 | } 10 | 11 | let decoder = JSONDecoder() 12 | decoder.dateDecodingStrategy = .iso8601 13 | 14 | let feed = try! decoder.decode(Feed.self, from: data) 15 | print(feed.title) 16 | 17 | for item in feed.items { 18 | print("* \(item.title!) - \(item.datePublished!)") 19 | } 20 | }.resume() 21 | 22 | PlaygroundPage.current.needsIndefiniteExecution = true 23 | 24 | 25 | -------------------------------------------------------------------------------- /Example.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/JSONFeedTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/JSONFeed_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "JSONFeed::JSONFeedPackageTests::ProductTarget" /* JSONFeedPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_40 /* Build configuration list for PBXAggregateTarget "JSONFeedPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_43 /* PBXTargetDependency */, 17 | ); 18 | name = JSONFeedPackageTests; 19 | productName = JSONFeedPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | F803FE8D21DD285D0044D780 /* feed.json in Resources */ = {isa = PBXBuildFile; fileRef = F803FE8C21DD285D0044D780 /* feed.json */; }; 25 | OBJ_27 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Attachment.swift */; }; 26 | OBJ_28 /* Author.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Author.swift */; }; 27 | OBJ_29 /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* Feed.swift */; }; 28 | OBJ_30 /* Hub.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* Hub.swift */; }; 29 | OBJ_31 /* Item.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Item.swift */; }; 30 | OBJ_38 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 31 | OBJ_49 /* JSONFeedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* JSONFeedTests.swift */; }; 32 | OBJ_50 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* XCTestManifests.swift */; }; 33 | OBJ_52 /* JSONFeed.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "JSONFeed::JSONFeed::Product" /* JSONFeed.framework */; }; 34 | /* End PBXBuildFile section */ 35 | 36 | /* Begin PBXContainerItemProxy section */ 37 | F803FE8721DD27E30044D780 /* PBXContainerItemProxy */ = { 38 | isa = PBXContainerItemProxy; 39 | containerPortal = OBJ_1 /* Project object */; 40 | proxyType = 1; 41 | remoteGlobalIDString = "JSONFeed::JSONFeed"; 42 | remoteInfo = JSONFeed; 43 | }; 44 | F803FE8821DD27E40044D780 /* PBXContainerItemProxy */ = { 45 | isa = PBXContainerItemProxy; 46 | containerPortal = OBJ_1 /* Project object */; 47 | proxyType = 1; 48 | remoteGlobalIDString = "JSONFeed::JSONFeedTests"; 49 | remoteInfo = JSONFeedTests; 50 | }; 51 | /* End PBXContainerItemProxy section */ 52 | 53 | /* Begin PBXFileReference section */ 54 | F803FE8C21DD285D0044D780 /* feed.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = feed.json; path = Tests/Resources/feed.json; sourceTree = SOURCE_ROOT; }; 55 | "JSONFeed::JSONFeed::Product" /* JSONFeed.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = JSONFeed.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 56 | "JSONFeed::JSONFeedTests::Product" /* JSONFeedTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = JSONFeedTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 57 | OBJ_10 /* Author.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Author.swift; sourceTree = ""; }; 58 | OBJ_11 /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = ""; }; 59 | OBJ_12 /* Hub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Hub.swift; sourceTree = ""; }; 60 | OBJ_13 /* Item.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Item.swift; sourceTree = ""; }; 61 | OBJ_16 /* JSONFeedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONFeedTests.swift; sourceTree = ""; }; 62 | OBJ_17 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 63 | OBJ_18 /* JSONFeed.xcworkspace */ = {isa = PBXFileReference; lastKnownFileType = wrapper.workspace; path = JSONFeed.xcworkspace; sourceTree = SOURCE_ROOT; }; 64 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 65 | OBJ_9 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = ""; }; 66 | /* End PBXFileReference section */ 67 | 68 | /* Begin PBXFrameworksBuildPhase section */ 69 | OBJ_32 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 0; 72 | files = ( 73 | ); 74 | runOnlyForDeploymentPostprocessing = 0; 75 | }; 76 | OBJ_51 /* Frameworks */ = { 77 | isa = PBXFrameworksBuildPhase; 78 | buildActionMask = 0; 79 | files = ( 80 | OBJ_52 /* JSONFeed.framework in Frameworks */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | /* End PBXFrameworksBuildPhase section */ 85 | 86 | /* Begin PBXGroup section */ 87 | OBJ_14 /* Tests */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | OBJ_15 /* JSONFeedTests */, 91 | ); 92 | name = Tests; 93 | sourceTree = SOURCE_ROOT; 94 | }; 95 | OBJ_15 /* JSONFeedTests */ = { 96 | isa = PBXGroup; 97 | children = ( 98 | F803FE8C21DD285D0044D780 /* feed.json */, 99 | OBJ_16 /* JSONFeedTests.swift */, 100 | OBJ_17 /* XCTestManifests.swift */, 101 | ); 102 | name = JSONFeedTests; 103 | path = Tests/JSONFeedTests; 104 | sourceTree = SOURCE_ROOT; 105 | }; 106 | OBJ_19 /* Products */ = { 107 | isa = PBXGroup; 108 | children = ( 109 | "JSONFeed::JSONFeed::Product" /* JSONFeed.framework */, 110 | "JSONFeed::JSONFeedTests::Product" /* JSONFeedTests.xctest */, 111 | ); 112 | name = Products; 113 | sourceTree = BUILT_PRODUCTS_DIR; 114 | }; 115 | OBJ_5 /* */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | OBJ_6 /* Package.swift */, 119 | OBJ_7 /* Sources */, 120 | OBJ_14 /* Tests */, 121 | OBJ_18 /* JSONFeed.xcworkspace */, 122 | OBJ_19 /* Products */, 123 | ); 124 | name = ""; 125 | sourceTree = ""; 126 | }; 127 | OBJ_7 /* Sources */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | OBJ_8 /* JSONFeed */, 131 | ); 132 | name = Sources; 133 | sourceTree = SOURCE_ROOT; 134 | }; 135 | OBJ_8 /* JSONFeed */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | OBJ_9 /* Attachment.swift */, 139 | OBJ_10 /* Author.swift */, 140 | OBJ_11 /* Feed.swift */, 141 | OBJ_12 /* Hub.swift */, 142 | OBJ_13 /* Item.swift */, 143 | ); 144 | name = JSONFeed; 145 | path = Sources/JSONFeed; 146 | sourceTree = SOURCE_ROOT; 147 | }; 148 | /* End PBXGroup section */ 149 | 150 | /* Begin PBXNativeTarget section */ 151 | "JSONFeed::JSONFeed" /* JSONFeed */ = { 152 | isa = PBXNativeTarget; 153 | buildConfigurationList = OBJ_23 /* Build configuration list for PBXNativeTarget "JSONFeed" */; 154 | buildPhases = ( 155 | OBJ_26 /* Sources */, 156 | OBJ_32 /* Frameworks */, 157 | ); 158 | buildRules = ( 159 | ); 160 | dependencies = ( 161 | ); 162 | name = JSONFeed; 163 | productName = JSONFeed; 164 | productReference = "JSONFeed::JSONFeed::Product" /* JSONFeed.framework */; 165 | productType = "com.apple.product-type.framework"; 166 | }; 167 | "JSONFeed::JSONFeedTests" /* JSONFeedTests */ = { 168 | isa = PBXNativeTarget; 169 | buildConfigurationList = OBJ_45 /* Build configuration list for PBXNativeTarget "JSONFeedTests" */; 170 | buildPhases = ( 171 | OBJ_48 /* Sources */, 172 | OBJ_51 /* Frameworks */, 173 | F803FE8A21DD28430044D780 /* Resources */, 174 | ); 175 | buildRules = ( 176 | ); 177 | dependencies = ( 178 | OBJ_53 /* PBXTargetDependency */, 179 | ); 180 | name = JSONFeedTests; 181 | productName = JSONFeedTests; 182 | productReference = "JSONFeed::JSONFeedTests::Product" /* JSONFeedTests.xctest */; 183 | productType = "com.apple.product-type.bundle.unit-test"; 184 | }; 185 | "JSONFeed::SwiftPMPackageDescription" /* JSONFeedPackageDescription */ = { 186 | isa = PBXNativeTarget; 187 | buildConfigurationList = OBJ_34 /* Build configuration list for PBXNativeTarget "JSONFeedPackageDescription" */; 188 | buildPhases = ( 189 | OBJ_37 /* Sources */, 190 | ); 191 | buildRules = ( 192 | ); 193 | dependencies = ( 194 | ); 195 | name = JSONFeedPackageDescription; 196 | productName = JSONFeedPackageDescription; 197 | productType = "com.apple.product-type.framework"; 198 | }; 199 | /* End PBXNativeTarget section */ 200 | 201 | /* Begin PBXProject section */ 202 | OBJ_1 /* Project object */ = { 203 | isa = PBXProject; 204 | attributes = { 205 | LastUpgradeCheck = 9999; 206 | }; 207 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "JSONFeed" */; 208 | compatibilityVersion = "Xcode 3.2"; 209 | developmentRegion = English; 210 | hasScannedForEncodings = 0; 211 | knownRegions = ( 212 | en, 213 | ); 214 | mainGroup = OBJ_5 /* */; 215 | productRefGroup = OBJ_19 /* Products */; 216 | projectDirPath = ""; 217 | projectRoot = ""; 218 | targets = ( 219 | "JSONFeed::JSONFeed" /* JSONFeed */, 220 | "JSONFeed::SwiftPMPackageDescription" /* JSONFeedPackageDescription */, 221 | "JSONFeed::JSONFeedPackageTests::ProductTarget" /* JSONFeedPackageTests */, 222 | "JSONFeed::JSONFeedTests" /* JSONFeedTests */, 223 | ); 224 | }; 225 | /* End PBXProject section */ 226 | 227 | /* Begin PBXResourcesBuildPhase section */ 228 | F803FE8A21DD28430044D780 /* Resources */ = { 229 | isa = PBXResourcesBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | F803FE8D21DD285D0044D780 /* feed.json in Resources */, 233 | ); 234 | runOnlyForDeploymentPostprocessing = 0; 235 | }; 236 | /* End PBXResourcesBuildPhase section */ 237 | 238 | /* Begin PBXSourcesBuildPhase section */ 239 | OBJ_26 /* Sources */ = { 240 | isa = PBXSourcesBuildPhase; 241 | buildActionMask = 0; 242 | files = ( 243 | OBJ_27 /* Attachment.swift in Sources */, 244 | OBJ_28 /* Author.swift in Sources */, 245 | OBJ_29 /* Feed.swift in Sources */, 246 | OBJ_30 /* Hub.swift in Sources */, 247 | OBJ_31 /* Item.swift in Sources */, 248 | ); 249 | runOnlyForDeploymentPostprocessing = 0; 250 | }; 251 | OBJ_37 /* Sources */ = { 252 | isa = PBXSourcesBuildPhase; 253 | buildActionMask = 0; 254 | files = ( 255 | OBJ_38 /* Package.swift in Sources */, 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | }; 259 | OBJ_48 /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 0; 262 | files = ( 263 | OBJ_49 /* JSONFeedTests.swift in Sources */, 264 | OBJ_50 /* XCTestManifests.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | /* End PBXSourcesBuildPhase section */ 269 | 270 | /* Begin PBXTargetDependency section */ 271 | OBJ_43 /* PBXTargetDependency */ = { 272 | isa = PBXTargetDependency; 273 | target = "JSONFeed::JSONFeedTests" /* JSONFeedTests */; 274 | targetProxy = F803FE8821DD27E40044D780 /* PBXContainerItemProxy */; 275 | }; 276 | OBJ_53 /* PBXTargetDependency */ = { 277 | isa = PBXTargetDependency; 278 | target = "JSONFeed::JSONFeed" /* JSONFeed */; 279 | targetProxy = F803FE8721DD27E30044D780 /* PBXContainerItemProxy */; 280 | }; 281 | /* End PBXTargetDependency section */ 282 | 283 | /* Begin XCBuildConfiguration section */ 284 | OBJ_24 /* Debug */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ENABLE_TESTABILITY = YES; 288 | FRAMEWORK_SEARCH_PATHS = ( 289 | "$(inherited)", 290 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 291 | ); 292 | HEADER_SEARCH_PATHS = "$(inherited)"; 293 | INFOPLIST_FILE = JSONFeed.xcodeproj/JSONFeed_Info.plist; 294 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 295 | OTHER_CFLAGS = "$(inherited)"; 296 | OTHER_LDFLAGS = "$(inherited)"; 297 | OTHER_SWIFT_FLAGS = "$(inherited)"; 298 | PRODUCT_BUNDLE_IDENTIFIER = JSONFeed; 299 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 300 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 301 | SKIP_INSTALL = YES; 302 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 303 | SWIFT_VERSION = 4.0; 304 | TARGET_NAME = JSONFeed; 305 | }; 306 | name = Debug; 307 | }; 308 | OBJ_25 /* Release */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ENABLE_TESTABILITY = YES; 312 | FRAMEWORK_SEARCH_PATHS = ( 313 | "$(inherited)", 314 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 315 | ); 316 | HEADER_SEARCH_PATHS = "$(inherited)"; 317 | INFOPLIST_FILE = JSONFeed.xcodeproj/JSONFeed_Info.plist; 318 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 319 | OTHER_CFLAGS = "$(inherited)"; 320 | OTHER_LDFLAGS = "$(inherited)"; 321 | OTHER_SWIFT_FLAGS = "$(inherited)"; 322 | PRODUCT_BUNDLE_IDENTIFIER = JSONFeed; 323 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 324 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 325 | SKIP_INSTALL = YES; 326 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 327 | SWIFT_VERSION = 4.0; 328 | TARGET_NAME = JSONFeed; 329 | }; 330 | name = Release; 331 | }; 332 | OBJ_3 /* Debug */ = { 333 | isa = XCBuildConfiguration; 334 | buildSettings = { 335 | CLANG_ENABLE_OBJC_ARC = YES; 336 | COMBINE_HIDPI_IMAGES = YES; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = dwarf; 339 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 340 | ENABLE_NS_ASSERTIONS = YES; 341 | GCC_OPTIMIZATION_LEVEL = 0; 342 | GCC_PREPROCESSOR_DEFINITIONS = ( 343 | "DEBUG=1", 344 | "$(inherited)", 345 | ); 346 | MACOSX_DEPLOYMENT_TARGET = 10.10; 347 | ONLY_ACTIVE_ARCH = YES; 348 | OTHER_SWIFT_FLAGS = "-DXcode"; 349 | PRODUCT_NAME = "$(TARGET_NAME)"; 350 | SDKROOT = macosx; 351 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 352 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "SWIFT_PACKAGE DEBUG"; 353 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 354 | USE_HEADERMAP = NO; 355 | }; 356 | name = Debug; 357 | }; 358 | OBJ_35 /* Debug */ = { 359 | isa = XCBuildConfiguration; 360 | buildSettings = { 361 | LD = /usr/bin/true; 362 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 363 | SWIFT_VERSION = 4.0; 364 | }; 365 | name = Debug; 366 | }; 367 | OBJ_36 /* Release */ = { 368 | isa = XCBuildConfiguration; 369 | buildSettings = { 370 | LD = /usr/bin/true; 371 | OTHER_SWIFT_FLAGS = "-swift-version 4 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode-beta.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk"; 372 | SWIFT_VERSION = 4.0; 373 | }; 374 | name = Release; 375 | }; 376 | OBJ_4 /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | CLANG_ENABLE_OBJC_ARC = YES; 380 | COMBINE_HIDPI_IMAGES = YES; 381 | COPY_PHASE_STRIP = YES; 382 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 383 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 384 | GCC_OPTIMIZATION_LEVEL = s; 385 | MACOSX_DEPLOYMENT_TARGET = 10.10; 386 | OTHER_SWIFT_FLAGS = "-DXcode"; 387 | PRODUCT_NAME = "$(TARGET_NAME)"; 388 | SDKROOT = macosx; 389 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 390 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 391 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 392 | USE_HEADERMAP = NO; 393 | }; 394 | name = Release; 395 | }; 396 | OBJ_41 /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | }; 400 | name = Debug; 401 | }; 402 | OBJ_42 /* Release */ = { 403 | isa = XCBuildConfiguration; 404 | buildSettings = { 405 | }; 406 | name = Release; 407 | }; 408 | OBJ_46 /* Debug */ = { 409 | isa = XCBuildConfiguration; 410 | buildSettings = { 411 | CLANG_ENABLE_MODULES = YES; 412 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 413 | FRAMEWORK_SEARCH_PATHS = ( 414 | "$(inherited)", 415 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 416 | ); 417 | HEADER_SEARCH_PATHS = "$(inherited)"; 418 | INFOPLIST_FILE = JSONFeed.xcodeproj/JSONFeedTests_Info.plist; 419 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 420 | OTHER_CFLAGS = "$(inherited)"; 421 | OTHER_LDFLAGS = "$(inherited)"; 422 | OTHER_SWIFT_FLAGS = "$(inherited)"; 423 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 424 | SWIFT_VERSION = 4.0; 425 | TARGET_NAME = JSONFeedTests; 426 | }; 427 | name = Debug; 428 | }; 429 | OBJ_47 /* Release */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | CLANG_ENABLE_MODULES = YES; 433 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 434 | FRAMEWORK_SEARCH_PATHS = ( 435 | "$(inherited)", 436 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 437 | ); 438 | HEADER_SEARCH_PATHS = "$(inherited)"; 439 | INFOPLIST_FILE = JSONFeed.xcodeproj/JSONFeedTests_Info.plist; 440 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; 441 | OTHER_CFLAGS = "$(inherited)"; 442 | OTHER_LDFLAGS = "$(inherited)"; 443 | OTHER_SWIFT_FLAGS = "$(inherited)"; 444 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 445 | SWIFT_VERSION = 4.0; 446 | TARGET_NAME = JSONFeedTests; 447 | }; 448 | name = Release; 449 | }; 450 | /* End XCBuildConfiguration section */ 451 | 452 | /* Begin XCConfigurationList section */ 453 | OBJ_2 /* Build configuration list for PBXProject "JSONFeed" */ = { 454 | isa = XCConfigurationList; 455 | buildConfigurations = ( 456 | OBJ_3 /* Debug */, 457 | OBJ_4 /* Release */, 458 | ); 459 | defaultConfigurationIsVisible = 0; 460 | defaultConfigurationName = Release; 461 | }; 462 | OBJ_23 /* Build configuration list for PBXNativeTarget "JSONFeed" */ = { 463 | isa = XCConfigurationList; 464 | buildConfigurations = ( 465 | OBJ_24 /* Debug */, 466 | OBJ_25 /* Release */, 467 | ); 468 | defaultConfigurationIsVisible = 0; 469 | defaultConfigurationName = Release; 470 | }; 471 | OBJ_34 /* Build configuration list for PBXNativeTarget "JSONFeedPackageDescription" */ = { 472 | isa = XCConfigurationList; 473 | buildConfigurations = ( 474 | OBJ_35 /* Debug */, 475 | OBJ_36 /* Release */, 476 | ); 477 | defaultConfigurationIsVisible = 0; 478 | defaultConfigurationName = Release; 479 | }; 480 | OBJ_40 /* Build configuration list for PBXAggregateTarget "JSONFeedPackageTests" */ = { 481 | isa = XCConfigurationList; 482 | buildConfigurations = ( 483 | OBJ_41 /* Debug */, 484 | OBJ_42 /* Release */, 485 | ); 486 | defaultConfigurationIsVisible = 0; 487 | defaultConfigurationName = Release; 488 | }; 489 | OBJ_45 /* Build configuration list for PBXNativeTarget "JSONFeedTests" */ = { 490 | isa = XCConfigurationList; 491 | buildConfigurations = ( 492 | OBJ_46 /* Debug */, 493 | OBJ_47 /* Release */, 494 | ); 495 | defaultConfigurationIsVisible = 0; 496 | defaultConfigurationName = Release; 497 | }; 498 | /* End XCConfigurationList section */ 499 | }; 500 | rootObject = OBJ_1 /* Project object */; 501 | } 502 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/xcshareddata/xcschemes/JSONFeed.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 74 | 76 | 77 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /JSONFeed.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | JSONFeed-Package.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /JSONFeed.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /JSONFeed.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 Read Evaluate Press, LLC 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 14 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "JSONFeed", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "JSONFeed", 12 | targets: ["JSONFeed"]), 13 | ], 14 | dependencies: [ 15 | // Dependencies declare other packages that this package depends on. 16 | // .package(url: /* package url */, from: "1.0.0"), 17 | ], 18 | targets: [ 19 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 20 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 21 | .target( 22 | name: "JSONFeed", 23 | dependencies: []), 24 | .testTarget( 25 | name: "JSONFeedTests", 26 | dependencies: ["JSONFeed"]), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSONFeed 2 | 3 | [![Build Status][build status badge]][build status] 4 | 5 | A Swift encoder and decoder for the [JSON Feed](https://jsonfeed.org) format. 6 | 7 | ## Usage 8 | 9 | ```swift 10 | // For an example, see https://jsonfeed.org/feed.json 11 | let json = "{ ... }" 12 | 13 | let decoder = JSONDecoder() 14 | decoder.dateDecodingStrategy = .iso8601 15 | 16 | let feed = try! decoder.decode(Feed.self, from: data) 17 | 18 | print(feed.title) 19 | 20 | for item in feed.items { 21 | print("* \(item.title!) - \(item.datePublished!)") 22 | } 23 | ``` 24 | 25 | ## License 26 | 27 | MIT 28 | 29 | ## Contact 30 | 31 | Mattt ([@mattt](https://twitter.com/mattt)) 32 | 33 | [build status]: https://github.com/Flight-School/JSONFeed/actions?query=workflow%3ACI 34 | [build status badge]: https://github.com/Flight-School/JSONFeed/workflows/CI/badge.svg 35 | -------------------------------------------------------------------------------- /Sources/JSONFeed/Attachment.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Attachment: Codable, Equatable { 4 | /** 5 | The location of the attachment. 6 | */ 7 | public var url: URL 8 | 9 | /** 10 | The type of the attachment, such as "audio/mpeg." 11 | */ 12 | public var mimeType: String 13 | 14 | /** 15 | A name for the attachment. 16 | 17 | - Important: If there are multiple attachments, 18 | and two or more have the exact same title 19 | (when title is present), then they are considered as 20 | alternate representations of the same thing. 21 | In this way a podcaster, for instance, 22 | might provide an audio recording in different formats. 23 | */ 24 | public var title: String? 25 | 26 | /** 27 | The size of the file, in bytes. 28 | */ 29 | public var size: Int? 30 | 31 | /** 32 | The duration of the audio or video, when played at normal speed. 33 | */ 34 | public var duration: TimeInterval? 35 | 36 | // MARK: - 37 | 38 | public init(url: URL, 39 | mimeType: String, 40 | title: String? = nil, 41 | size: Int? = nil, 42 | duration: TimeInterval? = nil) 43 | { 44 | self.url = url 45 | self.mimeType = mimeType 46 | self.title = title 47 | self.size = size 48 | self.duration = duration 49 | } 50 | 51 | // MARK: Codable 52 | 53 | private enum CodingKeys: String, CodingKey { 54 | case url 55 | case mimeType = "mime_type" 56 | case title 57 | case size = "size_in_bytes" 58 | case duration = "duration_in_seconds" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/JSONFeed/Author.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Author: Codable, Equatable { 4 | /** 5 | The author's name. 6 | */ 7 | public var name: String? 8 | 9 | /** 10 | The URL of a site owned by the author. 11 | 12 | The URL could be a blog, micro-blog, Twitter account, and so on. 13 | Ideally the linked-to page provides a way to contact the author, 14 | but that's not required. 15 | The URL could be a `mailto:` link, 16 | though we suspect that will be rare. 17 | */ 18 | public var url: URL? 19 | 20 | /** 21 | The URL for an image for the author. 22 | 23 | As with `icon`, it should be square and relatively large — 24 | such as 512 x 512 — and should use transparency where appropriate, 25 | since it may be rendered on a non-white background. 26 | */ 27 | public var avatar: URL? 28 | 29 | // MARK: - 30 | 31 | public init?(name: String? = nil, url: URL? = nil, avatar: URL?) { 32 | if name == nil && url == nil && avatar == nil { 33 | return nil 34 | } 35 | 36 | self.name = name 37 | self.url = url 38 | self.avatar = url 39 | } 40 | 41 | // MARK: Codable 42 | 43 | private enum CodingKeys: String, CodingKey { 44 | case name 45 | case url 46 | case avatar 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/JSONFeed/Feed.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Feed: Codable, Equatable { 4 | /// JSON Feed Version 1 5 | public static let v1 = URL(string: "https://jsonfeed.org/version/1")! 6 | 7 | /** 8 | The URL of the version of the format the feed uses. 9 | 10 | This should appear at the very top, 11 | though we recognize that not all JSON generators allow for ordering. 12 | */ 13 | public var version: URL 14 | 15 | /** 16 | The name of the feed, which will often correspond 17 | to the name of the website (blog, for instance), 18 | though not necessarily. 19 | */ 20 | public var title: String 21 | 22 | /** 23 | The URL of the resource that the feed describes. 24 | 25 | This resource may or may not actually be a "home" page, 26 | but it should be an HTML page. If a feed is published on the public web, 27 | this should be considered as required. 28 | But it may not make sense in the case 29 | of a file created on a desktop computer, 30 | when that file is not shared or is shared only privately. 31 | */ 32 | public var homePageURL: URL? 33 | 34 | /** 35 | The URL of the feed, serving as the unique identifier for the feed. 36 | 37 | As with `homePageURL`, 38 | this should be considered required for feeds on the public web. 39 | */ 40 | public var feedURL: URL? 41 | 42 | /** 43 | Provides more detail, beyond the title, 44 | on what the feed is about. 45 | 46 | A feed reader may display this text. 47 | */ 48 | public var description: String? 49 | 50 | /** 51 | A description of the purpose of the feed. 52 | 53 | This is for the use of people looking at the raw JSON, 54 | and should be ignored by feed readers. 55 | */ 56 | public var userComment: String? 57 | 58 | /** 59 | The URL of a feed that provides the next `n` items, 60 | where `n` is determined by the publisher. 61 | This allows for pagination, but with the expectation that 62 | reader software is not required to use it 63 | and probably won't use it very often. 64 | 65 | - Warning: `nextURL` must not be the same as `feedURL`, 66 | and it must not be the same as a previous `nextURL` 67 | (to avoid infinite loops). 68 | */ 69 | public var nextURL: URL? 70 | 71 | /** 72 | The URL of an image for the feed suitable to be used in a timeline, 73 | much the way an avatar might be used. 74 | 75 | The avatar should be square and relatively large — such as 512 x 512 — 76 | so that it can be scaled-down and so that 77 | it can look good on retina displays. 78 | It should use transparency where appropriate, 79 | since it may be rendered on a non-white background. 80 | */ 81 | public var icon: URL? 82 | 83 | /** 84 | The URL of an image for the feed suitable to be used in a source list. 85 | 86 | The favicon should be square and relatively small, 87 | but not smaller than 64 x 64 (so that it can look good on retina displays). 88 | As with `icon`, this image should use transparency where appropriate, 89 | since it may be rendered on a non-white background. 90 | */ 91 | public var favicon: URL? 92 | 93 | /** 94 | The feed author. 95 | */ 96 | public var author: Author? 97 | 98 | 99 | /** 100 | Whether or not the feed is finished — that is, 101 | whether or not it will ever update again. 102 | 103 | A feed for a temporary event, 104 | such as an instance of the Olympics, could expire. 105 | */ 106 | public var expired: Bool? 107 | 108 | /** 109 | Endpoints that can be used to subscribe to real-time notifications 110 | from the publisher of this feed. 111 | */ 112 | public var hubs: [Hub]? 113 | 114 | /** 115 | The items in the feed. 116 | */ 117 | public var items: [Item] 118 | 119 | // MARK: - 120 | 121 | public init(version: URL = Feed.v1, 122 | title: String, 123 | homePageURL: URL? = nil, 124 | feedURL: URL? = nil, 125 | description: String? = nil, 126 | userComment: String? = nil, 127 | nextURL: URL? = nil, 128 | icon: URL? = nil, 129 | favicon: URL? = nil, 130 | author: Author? = nil, 131 | expired: Bool? = nil, 132 | hubs: [Hub]? = nil, 133 | items: [Item]) 134 | { 135 | self.version = version 136 | self.title = title 137 | self.homePageURL = homePageURL 138 | self.feedURL = feedURL 139 | self.description = description 140 | self.userComment = userComment 141 | self.nextURL = nextURL 142 | self.icon = icon 143 | self.favicon = favicon 144 | self.author = author 145 | self.expired = expired 146 | self.hubs = hubs 147 | self.items = items 148 | } 149 | 150 | // MARK: Codable 151 | 152 | private enum CodingKeys: String, CodingKey { 153 | case version 154 | case title 155 | case homePageURL = "home_page_url" 156 | case feedURL = "feed_url" 157 | case description 158 | case userComment = "user_comment" 159 | case nextURL = "next_url" 160 | case icon 161 | case favicon 162 | case author 163 | case expired 164 | case hubs 165 | case items 166 | } 167 | } 168 | 169 | 170 | -------------------------------------------------------------------------------- /Sources/JSONFeed/Hub.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Hub: Codable, Equatable { 4 | /// The type of hub. 5 | public var type: String 6 | 7 | /// The URL for the hub. 8 | public var url: URL 9 | 10 | // MARK: - 11 | 12 | public init(type: String, url: URL) { 13 | self.type = type 14 | self.url = url 15 | } 16 | 17 | // MARK: Codable 18 | 19 | private enum CodingKeys: String, CodingKey { 20 | case type 21 | case url 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/JSONFeed/Item.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct Item: Codable, Equatable { 4 | /** 5 | A unique identifier for an item in a feed. 6 | 7 | If an item is ever updated, the `id` should be unchanged. 8 | New items should never use a previously-used `id`. 9 | If an `id` is presented as a number or other type, 10 | a JSON Feed reader must coerce it to a string. 11 | Ideally, the `id` is the full URL of the resource described by the item, 12 | since URLs make great unique identifiers. 13 | */ 14 | public var id: String 15 | 16 | /** 17 | The URL of the resource described by the item. 18 | 19 | It's the permalink. 20 | This may be the same as the id — but should be present regardless. 21 | */ 22 | public var url: URL? 23 | 24 | /** 25 | The URL of a page elsewhere. 26 | 27 | This is especially useful for linkblogs. 28 | If url links to where you're talking about a thing, 29 | then `externalURL` links to the thing you're talking about. 30 | */ 31 | public var externalURL: URL? 32 | 33 | /** 34 | The title in plain text. 35 | 36 | Microblog items in particular may omit titles. 37 | */ 38 | public var title: String? 39 | 40 | /** 41 | The HTML of the item. 42 | 43 | - Important: This is the only place HTML is allowed in this format. 44 | A Twitter-like service might use `contentText`, 45 | while a blog might use `contentHTML`. 46 | Use whichever makes sense for your resource. 47 | (It doesn't even have to be the same for each item in a feed.) 48 | - SeeAlso: `contentText` 49 | */ 50 | public var contentHTML: String? 51 | 52 | /** 53 | The plain text of the item. 54 | 55 | - SeeAlso: `contentHTML` 56 | */ 57 | public var contentText: String? 58 | 59 | /** 60 | A plain text sentence or two describing the item. 61 | 62 | This might be presented in a timeline, 63 | for instance, where a detail view would display 64 | all of `contentHTML` or `contentText`. 65 | */ 66 | public var summary: String? 67 | 68 | /** 69 | The URL of the main image for the item. 70 | 71 | This image may also appear in the `contentHTML` — if so, 72 | it's a hint to the feed reader that this is the main, featured image. 73 | Feed readers may use the image as a preview 74 | (probably resized as a thumbnail and placed in a timeline). 75 | */ 76 | public var image: URL? 77 | 78 | /** 79 | The URL of an image to use as a banner. 80 | 81 | Some blogging systems (such as Medium) 82 | display a different banner image chosen to go with each post, 83 | but that image wouldn't otherwise appear in the `contentHTML`. 84 | 85 | A feed reader with a detail view may choose to show this banner image 86 | at the top of the detail view, 87 | possibly with the title overlaid. 88 | */ 89 | public var bannerImage: URL? 90 | 91 | /** 92 | The publication date. 93 | */ 94 | public var datePublished: Date? 95 | 96 | /** 97 | The modification date 98 | */ 99 | public var dateModified: Date? 100 | 101 | /** 102 | The author of the item. 103 | 104 | If not specified in an item, then the top-level author, if present, 105 | is the author of the item. 106 | */ 107 | public var author: Author? 108 | 109 | /** 110 | Tags related to the item. 111 | 112 | Tags tend to be just one word, but they may be anything. 113 | 114 | - Note: Tags are not the equivalent of Twitter hashtags. 115 | Some blogging systems and other feed formats call these categories. 116 | */ 117 | public var tags: [String]? 118 | 119 | /** 120 | Resources related to the item. 121 | 122 | Podcasts, for instance, 123 | would include an attachment that's an audio or video file. 124 | */ 125 | public var attachments: [Attachment]? 126 | 127 | // MARK: - 128 | 129 | public init?(id: String, 130 | url: URL? = nil, 131 | externalURL: URL? = nil, 132 | title: String? = nil, 133 | contentHTML: String? = nil, 134 | contentText: String? = nil, 135 | summary: String? = nil, 136 | image: URL? = nil, 137 | bannerImage: URL? = nil, 138 | datePublished: Date? = nil, 139 | dateModified: Date? = nil, 140 | author: Author? = nil, 141 | tags: [String]? = nil, 142 | attachments: [Attachment]? = nil) 143 | { 144 | if contentHTML == nil && contentText == nil { 145 | return nil 146 | } 147 | 148 | self.id = id 149 | self.url = url 150 | self.externalURL = externalURL 151 | self.title = title 152 | self.contentHTML = contentHTML 153 | self.contentText = contentText 154 | self.summary = summary 155 | self.image = image 156 | self.bannerImage = bannerImage 157 | self.datePublished = datePublished 158 | self.dateModified = dateModified 159 | self.author = author 160 | self.tags = tags 161 | self.attachments = attachments 162 | } 163 | 164 | // MARK: Codable 165 | 166 | private enum CodingKeys: String, CodingKey { 167 | case id 168 | case url 169 | case externalURL = "external_url" 170 | case title 171 | case contentHTML = "content_html" 172 | case contentText = "content_text" 173 | case summary 174 | case image 175 | case bannerImage = "banner_image" 176 | case datePublished = "date_published" 177 | case dateModified = "date_modified" 178 | case author 179 | case tags 180 | case attachments 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /Tests/JSONFeedTests/JSONFeedTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import JSONFeed 3 | 4 | @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) 5 | final class JSONFeedTests: XCTestCase { 6 | var decoder: JSONDecoder! 7 | 8 | override func setUp() { 9 | self.decoder = JSONDecoder() 10 | self.decoder.dateDecodingStrategy = .iso8601 11 | } 12 | 13 | func testJSONFeedDecoding() { 14 | let feed = try! self.decoder.decode(Feed.self, from: json) 15 | 16 | XCTAssertEqual(feed.version.absoluteString, "https://jsonfeed.org/version/1") 17 | XCTAssertEqual(feed.title, "JSON Feed") 18 | XCTAssertEqual(feed.description, "JSON Feed is a pragmatic syndication format for blogs, microblogs, and other time-based content.") 19 | XCTAssertEqual(feed.homePageURL?.absoluteString, "https://jsonfeed.org/") 20 | XCTAssertEqual(feed.feedURL?.absoluteString, "https://jsonfeed.org/feed.json") 21 | 22 | XCTAssertEqual(feed.author?.name, "Brent Simmons and Manton Reece") 23 | XCTAssertEqual(feed.author?.url?.absoluteString, "https://jsonfeed.org/") 24 | 25 | XCTAssertEqual(feed.items.count, 1) 26 | XCTAssertEqual(feed.items.first?.title, "Announcing JSON Feed") 27 | XCTAssertEqual(feed.items.first?.datePublished, Date(timeIntervalSince1970: 1495033332)) 28 | XCTAssertEqual(feed.items.first?.id, "https://jsonfeed.org/2017/05/17/announcing_json_feed") 29 | XCTAssertEqual(feed.items.first?.url?.absoluteString, "https://jsonfeed.org/2017/05/17/announcing_json_feed") 30 | XCTAssert((feed.items.first?.contentHTML?.starts(with: "

We — Manton Reece and Brent Simmons"))!) 31 | } 32 | } 33 | 34 | fileprivate let json = #""" 35 | { 36 | "version": "https://jsonfeed.org/version/1", 37 | "user_comment": "This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL — https://jsonfeed.org/feed.json — and add it your reader.", 38 | "title": "JSON Feed", 39 | "description": "JSON Feed is a pragmatic syndication format for blogs, microblogs, and other time-based content.", 40 | "home_page_url": "https://jsonfeed.org/", 41 | "feed_url": "https://jsonfeed.org/feed.json", 42 | "author": { 43 | "name": "Brent Simmons and Manton Reece", 44 | "url": "https://jsonfeed.org/" 45 | }, 46 | "items": [ 47 | { 48 | "title": "Announcing JSON Feed", 49 | "date_published": "2017-05-17T08:02:12-07:00", 50 | "id": "https://jsonfeed.org/2017/05/17/announcing_json_feed", 51 | "url": "https://jsonfeed.org/2017/05/17/announcing_json_feed", 52 | "content_html": "

We — Manton Reece and Brent Simmons — have noticed that JSON has become the developers’ choice for APIs, and that developers will often go out of their way to avoid XML. JSON is simpler to read and write, and it’s less prone to bugs.

\n\n

So we developed JSON Feed, a format similar to RSS and Atom but in JSON. It reflects the lessons learned from our years of work reading and publishing feeds.

\n\n

See the spec. It’s at version 1, which may be the only version ever needed. If future versions are needed, version 1 feeds will still be valid feeds.

\n\n

Notes

\n\n

We have a WordPress plugin and, coming soon, a JSON Feed Parser for Swift. As more code is written, by us and others, we’ll update the code page.

\n\n

See Mapping RSS and Atom to JSON Feed for more on the similarities between the formats.

\n\n

This website — the Markdown files and supporting resources — is up on GitHub, and you’re welcome to comment there.

\n\n

This website is also a blog, and you can subscribe to the RSS feed or the JSON feed (if your reader supports it).

\n\n

We worked with a number of people on this over the course of several months. We list them, and thank them, at the bottom of the spec. But — most importantly — Craig Hockenberry spent a little time making it look pretty. :)

\n" 53 | } 54 | ] 55 | } 56 | """#.data(using: .utf8)! 57 | --------------------------------------------------------------------------------