Swiftpack.co -  aveine/JsonAPI as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
aveine/JsonAPI
A Swift JSON:API standard implementation
.package(url: "https://github.com/aveine/JsonAPI.git", from: "1.0.4")

JsonAPI

platforms JsonAPI CI Carthage compatible pod

JsonAPI is a Swift JSON:API standard implementation.
It has been greatly inspired from another library: Vox.

This library allows several types of use, from framework style to "raw" JSON:API object manipulation.

Requirements

  • Xcode 11.x
  • Swift 5.x

Installation

JsonAPI doesn't contain any external dependencies.

These are currently the supported installation options:

Carthage

To integrate JsonAPI into your Xcode project using Carthage, specify it in your Cartfile:

github "Aveine/JsonAPI" ~> 1.0

For usage and installation instructions, visit their website.

CocoaPods

To integrate JsonAPI into your Xcode project using CocoaPods, specify it in your Podfile:

pod 'JsonAPISwift', '~> 1.0'

For usage and installation instructions, visit their website.

Swift Package Manager

To integrate JsonAPI into your Xcode project using Swift Package Manager, specify it in your Package.swift:

dependencies: [
    .package(url: "https://github.com/aveine/JsonAPI.git", .upToNextMajor(from: "1.0.2"))
]

For usage and installation instructions, visit their website.

Usage

Basic

Defining a resource

import JsonAPI

class Article: Resource {

    /*--------------- Attributes ---------------*/
    
    var title: String?
    
    var descriptionText: String?

    var keywords: [String]?
    
    var viewsCount: NSNumber?
    
    var isFeatured: NSNumber?
    
    var customObject: [String: Any]?
    
    /*------------- Relationships -------------*/
        
    var authors: [Person]?

    var editor: Person?

    /*------------- Resource type -------------*/

    // resource type should be defined, otherwise it is the class name
    override class var resourceType: String {
        return "articles"
    }

    /*------------- Custom mechanics -------------*/

    /**
     Override the keys expected in the JSON API resource object's attributes to match the model's attributes
     Format => [resourceObjectAttributeKey: modelKey]
     */
    override class var resourceAttributesKeys: [String : String] {
        return [
            "descriptionText": "description"
        ]
    }

    /**
     Attributes that won't be serialized when serializing to a JSON API resource object
     */
    override class var resourceExcludedAttributes: [String] {
        return [
            "customObject"
        ]
    }
}

Networking

Client

Create a client that will be used to communicate with your JSON:API server and which inherit from the following protocol Client:

public typealias ClientSuccessBlock = (_ response: HTTPURLResponse?, _ data: Data?) -> Void
public typealias ClientFailureBlock = (_ error: Error?, _ data: Data?) -> Void

/**
 A client using the library
 */
public protocol Client: class {
    /**
     - Parameter path: Path on which the client must execute the request
     - Parameter method: HTTP method the client must use to execute the request
     - Parameter queryItems: Potential query items the client must send along with the request
     - Parameter body: Potential body the client must send along with the request
     - Parameter success: Success block that will be called if the request successed
     - Parameter failure: Failure block that will be called if the request failed
     - Parameter userInfo: Potential meta information that the user can provide to the client
     */
    func executeRequest(path: String, method: HttpMethod, queryItems: [URLQueryItem]?, body: Document.JsonObject?, success: @escaping ClientSuccessBlock, failure: @escaping ClientFailureBlock, userInfo: [String: Any]?)
}

Within the executeRequest method use any networking library that you want.

For example with Alamofire you can create a client like this:

import JsonAPI
import Alamofire

public class AlamofireClient: Client {
    let baseUrl: URL
    
    public init() {
        self.baseUrl = URL(string: "https://api.com")!
    }
    
    func getHeaders() -> HTTPHeaders {
        return [
            "Content-Type": "application/vnd.api+json",
            "Accept": "application/vnd.api+json"
        ]
    }
    
    public func executeRequest(path: String, method: HttpMethod, queryItems: [URLQueryItem]?, body: Document.JsonObject?, success: @escaping ClientSuccessBlock, failure: @escaping ClientFailureBlock, userInfo: [String: Any]?) {
        var urlComponents = URLComponents.init(url: self.baseUrl.appendingPathComponent(path), resolvingAgainstBaseURL: false)!
        urlComponents.queryItems = queryItems
        
        let url = try! urlComponents.asURL()
        
        var request = URLRequest(url: url)
        request.httpMethod = method.rawValue
        self.getHeaders().forEach { request.setValue($0.value, forHTTPHeaderField: $0.key) }
        if let body = body {
            request.httpBody = try! JSONSerialization.data(withJSONObject: body)
        }
        
        SessionManager.default
            .request(request)
            .validate(statusCode: 200..<300)
            .validate(contentType: ["application/vnd.api+json"])
            .responseData { (dataResponse) in
                let response = dataResponse.response
                let data = dataResponse.data
                let error = dataResponse.error
                
                dataResponse
                    .result
                    .ifSuccess { success(response, data) }
                    .ifFailure { failure(error, data) }
        }
    }
}

Data source

The most common way to interact with your resource is to define a data source:

import JsonAPI

let client = AlamofireClient()

let dataSourceRouter = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))
let dataSourcePath = DataSource<Article>(client: client, strategy: .path("/<type>/<id>"))

A data source need to have a strategy specified to know how to interact with your resource. The strategy can either be a router or a path.

For the strategy path, <id> and <type> annotations can be used. If possible, they'll get replaced with adequate values.

Router

Routers allow you to define the paths to interacts with the resources.

For example, this is the implementation of the ScrudRouter within the library:

/**
 Router for classical SCRUD paths architecture
 
 - Search: resourceType
 - Create: resourceType
 - Read: resourceType/id
 - Update: resourceType/id
 - Delete: resourceType/id
 */
public class ScrudRouter: Router {
    public init() {}
    
    public func search(type: String) -> String {
        return type
    }
    
    public func create(resource: Resource) -> String {
        return resource.type
    }
    
    public func read(type: String, id: String) -> String {
        return "\(type)/\(id)"
    }

    public func update(resource: Resource) -> String {
        return "\(resource.type)/\(resource.id ?? "")"
    }

    public func delete(type: String, id: String) -> String {
        return "\(type)/\(id)"
    }
    
    public func delete(resource: Resource) -> String {
        return "\(resource.type)/\(resource.id ?? "")"
    }
}
Requests
Search a resource
import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

dataSource
    .search()
    .result({ (articles: [Article], document: Document) in
    }, { (error: Error?, document: Document?) in
    })
Read a resource
import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

dataSource
    .read(id: "1")
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })
Create a resource
import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

let article = Article()
    article.id = "1"
    article.title = "Title"

dataSource
    .create(article)
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })
Update a resource
import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

let article = Article()
    article.id = "1"
    article.title = "Another Title"

dataSource
    .update(article)
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })
Delete a resource
import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

let article = Article()
    article.id = "1"

dataSource
    .delete(article)
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })

Or if you don't have previously fetched the resource:

import JsonAPI

let client = AlamofireClient()
let dataSource = DataSource<Article>(client: client, strategy: .router(ScrudRouter()))

dataSource
    .delete(id: "1")
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })
Extensions
Implementing pagination and filtering helpers

Since Pagination and Filtering can vary by server implementation, here is an example of how the library can be extended to support them.

extension ResourceCollectionRequest {
  /**
   Append the given filters to the request's query items

   - Parameter filters: Filters to append
   - Returns: The request with the filters query appended
   */
  public func filters( _ filters: [ String: [ String ] ] ) -> Self {
    var queryItems: [ URLQueryItem ] = []
    for filter in filters {
      queryItems.append( URLQueryItem( name: "filter[\(filter.key)]", value: filter.value.joined( separator: "," ) ) )
    }
    return self.queryItems( queryItems )
  }

  /**
   Append the given filters to the request's query items

   - Parameter key: Key of filter to append
   - Parameter value: Value(s) of filter to append
   - Returns: The request with the filters query appended
   */
  public func filter( _ key: String, _ value: String...) -> Self {
    return self.filters( [ key: value ] )
  }

  /**
   Append the given paging parameters to the request's query items

   - Parameter size: Maximum number of items to return
   - Parameter number: Page number to return
   - Returns: The request with the page query appended
   */
  public func page( _ size: Int, number: Int? = nil ) -> Self {
    var queryItems: [ URLQueryItem ] = [
      URLQueryItem( name: "page[size]", value: String( size ) )
    ]
    if let number = number {
      queryItems.append( URLQueryItem( name: "page[number]", value: String( number ) ) )
    }
    return self.queryItems( queryItems )
  }
}

Advanced

!! Section in progress !!

API Documentation

A complete API documentation can be found here.

Create a request without a data source

import JsonAPI

let client = AlamofireClient()
let resourceRequest = ResourceRequest<Article>(path: "/articles/1", method: HttpMethod.get, client: client, resource: nil)
let resourceCollectionRequest = ResourceCollectionRequest<Article>(path: "/articles", method: HttpMethod.get, client: client, resource: nil)

resourceRequest
    .result({ (article: Article?, document: Document?) in
    }, { (error: Error?, document: Document?) in
    })

resourceCollectionRequest
    .result({ (articles: [Article], document: Document) in
    }, { (error: Error?, document: Document?) in
    })

Raw JSON:API object manipulation

Coming soon

GitHub

link
Stars: 3
Last commit: 5 days ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Submit a free job ad (while I'm testing this). The analytics numbers for this website are here.

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics