Spine
A Swift library for working with JSON:API APIs. It supports mapping to custom model classes, fetching, advanced querying, linking and persisting.
Install / Use
/learn @wvteijlingen/SpineREADME
I'm not maintaining this library anymore. The community is continuing development of Spine at jsonapi-ios/Spine. Feel free to use that fork, and submit pull-requests or open issues there.
The project that used this was shelved and I'm too busy with other work, so I cannot afford to spend time on this anymore. Feel free to fork this if you want, but don't expect me to maintain or help with issues for the foreseeable future. ❤️
Spine
Spine is a Swift library for working with APIs that adhere to the jsonapi.org standard. It supports mapping to custom model classes, fetching, advanced querying, linking and persisting.
Stability
This library was born out of a hobby project. Some things are still lacking, one of which is test coverage. Beware of this when using Spine in a production app!
Table of Contents
Supported features
| Feature | Supported | Note | | ------------------------------ | --------- | ----------------------------------------------- | | Fetching resources | Yes | | | Creating resources | Yes | | | Updating resources | Yes | | | Deleting resources | Yes | | | Top level metadata | Yes | | | Top level errors | Yes | | | Top level links | Yes | | | Top level JSON API Object | Yes | | | Client generated ID's | Yes | | | Resource metadata | Yes | | | Custom resource links | No | | | Relationships | Yes | | | Inclusion of related resources | Yes | | | Sparse fieldsets | Partially | Fetching only, all fields will be saved | | Sorting | Yes | | | Filtering | Yes | Supports custom filter strategies | | Pagination | Yes | Offset, cursor and custom pagination strategies | | Bulk extension | No | | | JSON Patch extension | No | |
Installation
Carthage
Add github "wvteijlingen/Spine" "master" to your Cartfile. See the Carthage documentation for instructions on how to integrate with your project using Xcode.
Cocoapods
Add pod 'Spine', :git => 'https://github.com/wvteijlingen/Spine.git' to your Podfile. The spec is not yet registered with the Cocoapods repository, because the library is still in flux.
Configuration
Defining resource types
Every resource is mapped to a class that inherits from Resource. A subclass should override the variables resourceType and fields. The resourceType should contain the type of resource in plural form. The fields array should contain an array of fields that must be persisted. Fields that are not in this array are ignored.
Each class must be registered using the Spine.registerResource method.
Defining resource fields
You need to specify the fields that must be persisted using an array of Fields. These fields are used when turning JSON into resources instances and vice versa. The name of each field corresponds to a variable on your resource class. This variable must be specified as optional.
Field name formatters
By default, the key in the JSON will be the same as your field name or serialized field name. You can specify a different name by using serializeAs(name: String). The name or custom serialized name will be mapped to a JSON key using a KeyFormatter. You can configure the key formatter using the keyFormatter variable on a Spine instance.
Spine comes with three key formatters: AsIsKeyFormatter, DasherizedKeyFormatter, UnderscoredKeyFormatter.
// Formats a field name 'myField' to key 'MYFIELD'.
public struct AllCapsKeyFormatter: KeyFormatter {
public func format(field: Field) -> String {
return field.serializedName.uppercaseString
}
}
spine.keyFormatter = AllCapsKeyFormatter()
Built in attribute types
Attribute
An attribute is a regular attribute that can be serialized by NSJSONSerialization. E.g. a String or NSNumber.
URLAttribute
An url attribute corresponds to an NSURL variable. These are represented by strings in the JSON document. You can instantiate it with a baseURL, in which case Spine will expand relative URLs from the JSON relative to the given baseURL. Absolute URLs will be left as is.
DateAttribute
A date attribute corresponds to an NSDate variable. By default, these are represented by ISO 8601 strings in the JSON document. You can instantiate it with a custom format, in which case that format will be used when serializing and deserializing that particular attribute.
ToOneRelationship
A to-one relationship corresponds to another resource. You must instantiate it with the type of the linked resource.
ToManyRelationship
A to-many relationship corresponds to a collection of other resources. You must instantiate it with the type of the linked resources. If the linked types are not homogenous, they must share a common ancestor as the linked type. To many relationships are mapped to LinkedResourceCollection objects.
Custom attribute types
Custom attribute types can be created by subclassing Attribute. A custom attribute type must have a registered transformer that handles serialization and deserialization.
Transformers are registered using the registerTransformer method. A transformer is a class or struct that implements the Transformer protocol.
public class RomanNumeralAttribute: Attribute { }
struct RomanNumeralValueFormatter: ValueFormatter {
func unformat(value: String, attribute: RomanNumeralAttribute) -> AnyObject {
let integerRepresentation: NSNumber = // Magic...
return integerRepresentation
}
func format(value: NSNumber, attribute: RomanNumeralAttribute) -> AnyObject {
let romanRepresentation: String = // Magic...
return romanRepresentation
}
}
spine.registerValueFormatter(RomanNumeralValueFormatter())
Example resource class
// Resource class
class Post: Resource {
var title: String?
var body: String?
var creationDate: NSDate?
var author: User?
var comments: LinkedResourceCollection?
override class var resourceType: ResourceType {
return "posts"
}
override class var fields: [Field] {
return fieldsFromDictionary([
"title": Attribute(),
"body": Attribute().serializeAs("content"),
"creationDate": DateAttribute(),
"author": ToOneRelationship(User),
"comments": ToManyRelationship(Comment)
])
}
}
spine.registerResource(Post)
Usage
Fetching resources
Resources can be fetched using find methods:
// Fetch posts with ID 1 and 2
spine.find(["1", "2"], ofType: Post).onSuccess { resources, meta, jsonapi in
println("Fetched resource collection: \(resources)")
}.onFailure { error in
println("Fetching failed: \(error)")
}
spine.findAll(Post) // Fetch all posts
spine.findOne("1", ofType: Post) // Fetch a single posts with ID 1
Alternatively, you can use a Query to perform a more advanced find:
var query = Query(resourceType: Post)
query.include("author", "comments", "comments.author") // Sideload relationships
query.whereProperty("upvotes", equalTo: 8) // Only with 8 upvotes
query.addAscendingOrder("creationDate") // Sort on creation date
spine.find(query).onSuccess { resources, meta, jsonapi in
println("Fetched resource collection: \(resources)")
}.onFailure { error in
println("Fetching failed: \(error)")
}
All fetch methods return a Future with onSuccess and onFailure callbacks.
Saving resources
spine.save(post).onSuccess { _ in
println("Saving success")
}.onFailure { error in
println("Saving failed: \(error)")
}
Extra care MUST be taken regarding related resources. Saving does not automatically save any related resources. You must explicitly save these yourself beforehand. If you added a new create resource to a pare
