Giter Club home page Giter Club logo

spine's Introduction

Build Status Join the chat at https://gitter.im/wvteijlingen/Spine

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 parent resource, you must first save the child resource (to obtain an ID), before saving the parent resource.

Deleting resources

spine.delete(post).onSuccess {
    println("Deleting success")
}.onFailure { error in
    println("Deleting failed: \(error)")
}

Deleting does not cascade on the client.

Loading and reloading resources

You can use the Spine.load methods to make sure resources are loaded. If it is already loaded, it returns the resource as is. Otherwise it loads the resource using the passed query.

The Spine.reload method works similarly, except that it always reloads a resource. This can be used to make sure a resource contains the latest data from the server.

Pagination

You can fetch next and previous pages of collections by using: Spine.loadNextPageOfCollection and Spine.loadPreviousPageOfCollection.

JSON:API is agnostic about pagination strategies. Because of this, Spine by default only supports two pagination strategies:

  • Page based pagination using the page[number] and page[size] parameters
  • Offset based pagination using the page[offset] and page[limit] parameters

You can add a custom filter strategy by creating a new type that conforms to the Pagination protocol, and then subclassing the built in Router class and overriding the queryItemsForPagination(pagination: Pagination) method.

Example: implementing 'cursor' based pagination

In this example, cursor based pagination is added a using the page[limit], and either a page[before] or page[after] parameter.

public struct CursorBasedPagination: Pagination {
	var beforeCursor: String?
	var afterCursor: String?
	var limit: Int
}
class CustomRouter: Router {
	override func queryItemsForPagination(pagination: Pagination) -> [NSURLQueryItem] {
		if let cursorPagination = pagination as? CursorBasedPagination {
			var queryItems = [NSURLQueryItem(name: "page[limit]", value: String(cursorPagination.limit))]

			if let before = cursorPagination.beforeCursor {
				queryItems.append(NSURLQueryItem(name: "page[before]", value: before))
			} else if let after = cursorPagination.afterCursor {
				queryItems.append(NSURLQueryItem(name: "page[after]", value: after))
			}

			return queryItems
		} else {
			return super.queryItemsForPagination(pagination)
		}
	}
}

Filtering

JSON:API is agnostic about filter strategies. Because of this, Spine by default only supports 'is equal to' filtering in the form of ?filter[key]=value.

You can add a custom filter strategy by subclassing the built in Router class and overriding the queryItemForFilter(filter: NSComparisonPredicate) method. This method takes a comparison predicate and returns a matching NSURLQueryItem.

Example: implementing a 'not equal to' filter

In this example, a switch statement is used to add a 'not equal filer in the form of ?filter[key]=!value.

class CustomRouter: Router {
	override func queryItemForFilter(field: Field, value: AnyObject, operatorType: NSPredicateOperatorType) -> NSURLQueryItem {
		switch operatorType {
		case .NotEqualToPredicateOperatorType:
			let key = keyFormatter.format(field)
			return NSURLQueryItem(name: "filter[\(key)]", value: "!\(value)")
		default:
			return super.queryItemForFilter(filter)
		}
	}
}

let baseURL = NSURL(string: "http://api.example.com/v1")
let spine = Spine(baseURL: baseURL, router: CustomRouter())

Networking

Spine uses a NetworkClient to communicate with the remote API. By default it uses the HTTPClient class which performs request over the HTTP protocol.

Customising HTTP headers of HTTPClient

The HTTPClient class supports setting HTTP headers as follows:

(spine.networkClient as! HTTPClient).setHeader("User-Agent", to: "My App")
(spine.networkClient as! HTTPClient).removeHeader("User-Agent")

Using a custom network client

You can use a custom client by subclassing HTTPClient or by creating a class that implements the NetworkClient protocol. Pass an instance of this class when instantiating a Spine:

var customClient = CustomNetworkClient()
var spine = Spine(baseURL: NSURL(string:"http://example.com")!, networkClient: customClient)

Logging

Spine comes with a rudimentary logging system. Each logging domain can be configured with a certain log level:

Spine.setLogLevel(.Debug, forDomain: .Spine)
Spine.setLogLevel(.Info, forDomain: .Networking)
Spine.setLogLevel(.Warning, forDomain: .Serializing)

These levels are global, meaning they apply to all Spine instances.

Log domains

  • Spine: The main Spine component.
  • Networking: The networking component, requests, responses etc.
  • Serializing: The (de)serializing component.

Log levels

  • Debug
  • Info
  • Warning
  • Error
  • None

Custom loggers

The default ConsoleLogger logs to the console using the Swift built in print command. You can assign a custom logger that implements the Logger protocol to the static Spine.logger variable.

Memory management

Spine suffers from the same memory management issues as Core Data, namely retain cycles for recursive relationships. These cycles can be broken in two ways:

  1. Declare one end of the relationship as weak or unowned.
  2. Use a Resource's unload method to break cycles when you are done with the resource.

Using the serializer separately

You can also just use the Serializer to (de)serialize to and from JSON:

let serializer = Serializer()

// Register resources
serializer.registerResource(Post)

// Optional configuration
serializer.registerValueFormatter(RomanNumeralValueFormatter())
serializer.keyFormatter = DasherizedKeyFormatter()

// Convert NSData to a JSONAPIDocument struct
let data = fetchData()
let document = try! serializer.deserializeData(data)

// Convert resources to NSData
let data = try! serializer.serializeResources([post])

// Convert resources to link data
let data = try! serializer.serializeLinkData(post)
let data = try! serializer.serializeLinkData([firstPost, secondPost])

spine's People

Contributors

admsyn avatar cbieniak avatar davidgoli avatar dbenninger avatar guiruiz avatar invlid avatar jensgrud avatar jviney avatar krasnoukhov avatar kurko avatar rlwimi avatar wvteijlingen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

spine's Issues

Custom loggers

It would be nice if we had a Logger protocol so developers can hoop up different loggers.

Ascending sort order does not work

It also doesn't match the JSONAPI spec.

Routing.swift line 156 builds an ascending sort order with a prepended +:

                if descriptor.ascending {
                    return "+\(key)"
                } else {
                    return "-\(key)"
                }

However, this gets converted into a path that looks like /api/cameras/21/sleeps?sort=+started-at, which without extra encoding is interpreted as a space character per https://www.w3.org/Addressing/URL/4_URI_Recommentations.html:

Within the query string, the plus sign is reserved as shorthand notation for a space. Therefore, real plus signs must be encoded. This method was used to make query URIs easier to pass in systems which did not allow spaces.

This plus sign is also not required by the JSONAPI spec, which says:

The sort order for each sort field MUST be ascending unless it is prefixed with a minus (U+002D HYPHEN-MINUS, "-"), in which case it MUST be descending.

(http://jsonapi.org/format/#fetching-sorting)

As-is, this implementation of ascending sort is not compatible with popular server-side frameworks like Rails' jsonapi-resources, which returns a 400: Bad Request since it interprets the + sign as a leading space, not as a plus.

Swift 2.0?

I had a stab at getting this library going in Swift 2.0, but I don't understand enough of how it uses BrightFutures (the API of which has changed dramatically in the last few months it seems) to get it all going again.

Any idea when you will get it going in Swift 2.0?

ToManyRelationship and included resources

Hey,

Here is my json :

{
    "data": {
        "type": "ads",
        "id": "671",
        "attributes": {
            "id": 671,
            "title": "SONY TV (de 32 pouces)",
        },
        "relationships": {
            "seller": {
                "data": {
                    "type": "persons",
                    "id": "95vl7"
                }
            },
        }
    },
    "included": [
        {
            "type": "persons",
            "id": "95vl7",
            "attributes": {
                "hash": "95vl7",
                "surname": "Prawidlo",
                "given_name": "Marine",
                "verified": true,
                "gamification_level": 3,
                "avatar_url_pattern": "/users/{id}/avatars/{width}x{height}/{cropped}"
            },
            "relationships": {
                "friends": {
                    "data": [
                        {"type": "persons", "id": "87aY7"},
                        {"type": "persons", "id": "xN21b"}
                    ]
                }
            }
        },
        {
            "type": "persons",
            "id": "87aY7",
            "attributes": {
                "hash": "87aY7",
                "surname": "Abergel",
                "given_name": "Arthur",
                "verified": true,
                "gamification_level": 3,
                "avatar_url_pattern": "/users/{id}/avatars/{width}x{height}/{cropped}"
            }
        },
        {
            "type": "persons",
            "id": "xN21b",
            "attributes": {
                "hash": "xN21b",
                "surname": "Dubreuil",
                "given_name": "Jeremy",
                "verified": true,
                "gamification_level": 3,
                "avatar_url_pattern": "/users/{id}/avatars/{width}x{height}/{cropped}"
            }
        }
    ]
}

My query code :

let spine = Spine(baseURL: url)

// Registering entities
spine.registerResource(Ad)
spine.registerResource(User)
var query = Query(resourceType: Ad.self, resourceIDs: ["691"])
query.include("evaluations", "seller.friends")
spine.find(query).onSuccess(callback: { (resources, meta, jsonapi) -> Void in
      print("success")
}).onFailure { (error) -> Void in
      print(error)
}

my errors :

❕ Info    - Cannot resolve to-many link persons:87aY7 - friends because the linked collection is not fetched.
❕ Info    - Cannot resolve to-many link persons:xN21b - friends because the linked collection is not fetched.

I don't really understand why. Any idea?

Thanks.

Parse local json file

Hi there,

Is there any way to parse a local json file instead of calling a API endpoint?

Thanks, Wouter

EXC_BAD_ACCESS (code=1, address=0x18) in operationQueue

Since I upgraded to the latest master (the big rewrite), I'm getting the following error in Spine.swift:

EXC_BAD_ACCESS (code=1, address=0x18) in operationQueue

When

  • It doesn't happen when I run the tests individually. The integration tests in my project pass.
  • It happens when I run the entire test suite.

I don't reset Spine between tests, but it seems that something inside Spine one operation is leaking into another. I really don't know how to debug this...

The error

spine-error

Can you suggest some things I could do to debug this?

Retrieving results but result collection is empty

First, I'd like to thank you for this library!

However, I'm trying to retrieve some data from my server but I got nothing and I can't figure out why because there should be data, and the log is even displaying it (I set all logs to Debug).

Here is my resource :

class Device : Resource {
    /////////////////////////////////////////////////////////////////////////
    // Entity definition
    dynamic var authId: String?
    dynamic var macAddress: String?
    dynamic var imei: String?
    dynamic var imsi: String?
    dynamic var creationDate: NSDate?
    dynamic var updateDate: NSDate?

    override class var resourceType: String {
        return "devices"
    }

    override class var fields: [Field] {
        return fieldsFromDictionary([
            "authId": Attribute().serializeAs("auth_id"),
            "macAddress": Attribute().serializeAs("mac_address"),
            "imei": Attribute().serializeAs("imei"),
            "imsi": Attribute().serializeAs("imsi"),
            "creationDate": DateAttribute().serializeAs("creation_ts"),
            "updateDate": DateAttribute().serializeAs("modif_ts")
            ])
    }
    /////////////////////////////////////////////////////////////////////////
}

And here is my code that initializes Spine and executes the first request that should retrieve all Device

// baseURL is defined above and is valid
let spine = Spine(baseURL: baseURL!)

        Spine.setLogLevel(.Debug, forDomain: .Spine)
        Spine.setLogLevel(.Debug, forDomain: .Networking)
        Spine.setLogLevel(.Debug, forDomain: .Serializing)

        // Registering entities
        spine.registerResource(Device.resourceType) { Device() }

        // tests
        spine.find(Device.self).onSuccess { res in
            NSLog("Count : " + String(res.count))         // Displays 0
            for r in res {
                if let device = r as? Device {
                    NSLog("Device : " + device.authId!)     // Code not reached
                }
            }
        }

Now, here are my logs :

❕ Info    - Fetching resources using URL: http://myurl/devices/
❕ Info    - GET: http://myurl/devices/
❕ Info    - 200: Optional(http://myurl/devices/)
❔ Debug   - {"device":[{"auth_id":"d9e87da5-d2bc-4d5c-a979-2527ee971b07","creation_ts":"2014-12-03 03:21:44.222516","id":"b60cb2cb-219b-41ae-8f68-f7e338407a0a","imei":"imei_test","imsi":"imsi_test","mac_address":"mac_test","modif_ts":"2014-12-03 03:21:44.222516"},{"auth_id":"0ef3f0b4-487e-40bf-bd03-cd12abc4c748","creation_ts":"2014-12-03 04:23:44.73796","id":"c1a2ac9b-a7bb-4fff-b1b8-d35e888ea89f","imei":"imei_test","imsi":"imsi_test","mac_address":"mac_test","modif_ts":"2014-12-03 04:23:44.73796"},{"auth_id":"b3e55323-464c-423c-bdd3-5f38515e22b4","creation_ts":"2014-12-03 04:29:53.680171","id":"18ce7709-0d3c-4746-a74b-80df1b8e1177","imei":"mac_test","imsi":"imei_test","mac_address":null,"modif_ts":"2014-12-03 04:29:53.680171"},{"auth_id":"8e7b2362-2a6c-4a38-81d5-d803ee0e26fa","creation_ts":"2014-12-03 04:41:35.734255","id":"84973337-3bf3-427d-acfa-849ef1136ecd","imei":"imei_test","imsi":"imsi_test","mac_address":"mac_test","modif_ts":"2014-12-03 04:41:35.734255"},{"auth_id":"f201c61f-e666-415a-8d0e-62798977f089","creation_ts":"2015-05-15 11:01:17.254469","id":"dfa0ff32-792f-4772-ad12-9fa9715b5c60","imei":"imei","imsi":"imsi","mac_address":"mac address","modif_ts":"2015-05-15 11:01:17.254469"},{"auth_id":"2074c964-00f1-4a77-a34d-2836f5f8b05a","creation_ts":"2015-05-15 11:24:33.866635","id":"9b83039c-2dba-4428-9d73-3af4ca95def5","imei":"imei","imsi":"imsi","mac_address":"macaddress","modif_ts":"2015-05-15 11:24:33.866635"},{"auth_id":"b66bbf06-cd3c-4d86-abcd-8abb95bce30f","creation_ts":"2015-05-15 12:01:25.74062","id":"7a58dc86-e3e0-4ae4-8576-f7b5f30f5110","imei":"imei","imsi":"imsi","mac_address":"macaddress","modif_ts":"2015-05-15 12:01:25.74062"}]}
2015-05-21 10:29:52.307 PROJECT[2335:889799] Count : 0

Did I do something wrong?

Thank you!

Tag some (semantically-versioned) releases

Hi! Could you start tagging releases with SemVer for library clients who use things like Cocoapods to have stable version tags to point to? I noticed we were on the swift-2.0 branch, and now that that's on master, I'd love to point to something more stable than the SHA du jour.

Complex attributes with subobject

Hey,

I'm wondering if there is a way to parse subobject in attributes like :

{
    "data": {
        "type": "posts",
        "id": "671",
        "attributes": {
            "id": 671,
            "title": "funy place",
            "location": {
                "name": "La Sorbonne, Paris",
                "short_name": "La Sorbonne",
                "latitude": 52.377965,
                "longitude": 4.912809,
                "distance_from_me": 9125000
            }
        }
    }
}

Thanks

Support client side ids

  • ID generator protocol to generate ID's
  • Generate ID on save
  • Keep track of isNew status via other means

Attributes not being serialized on save()

I'm experiencing a problem. Once I instantiate a resource and call spine.save(user), the attributes object is not sent to my server (POST /users). The full details are below, under code.

I'm beginner in iOS dev, so I'm trying to look at Spine's source code to figure out what could be causing it. I realized there are zero tests covering the save() method (I could be totally wrong, too). Once you help me out with this, I'd love to open a PR to add some documentation. There's none on how to set attributes (e.g user.setValue("Alex2", forField: "name")).

The code

import Spine

class UserResource: Resource {
    dynamic var name: String?
    dynamic var email: String?

    override class var resourceType: String {
        return "users"
    }

    var fields: [Field] {
        return fieldsFromDictionary([
            "name": Attribute(),
            "email": Attribute()
        ])
    }
}

// in another place, when I hit a button
let baseURL = NSURL(string: "http://localhost:3000/")
let spine = Spine(baseURL: baseURL!)
spine.registerResource(UserResource.resourceType)  { UserResource() }

var user = UserResource()
user.name = "Alex"
user.email = "[email protected]"

println(user.name) // outputs "Alex"
spine.save(user)

My server gets

{"data"=>{"type"=>"users"}}

The problem

It's not sending the attributes it should.

Bool instead of NSNumber

Hey,
I'm just started using Spine and I wonder if it is possible to use native Bool instead of NSNumber?

ATM it looks like this:

  dynamic var demo: NSNumber?

Cannot invoke 'onSuccess' with an argument list of type '(() -> _)'

I'm getting the following error

screen shot 2015-07-16 at 12 10 50 am

Here's the code:

let baseURL = NSURL(string: "http://localhost:3000/")
let spine = Spine(baseURL: baseURL!)
spine.registerResource(UserResource.resourceType)  { UserResource() }

var user = UserResource()
user.name = "Alex"

spine.save(user).onSuccess {
  println("saved")
}

screen shot 2015-07-16 at 12 19 58 am

Any idea how to fix that?

Dirty checking

Only save dirty variables to avoid overriding in case of sparse fieldsets.

Consider making `addResourceAsExisting` public func

In my project, I have a resource that has toMany relation with finite list of other resources (ids are known as well).

It would be nice to be able to have this func public as well as addResourcesAsExisting since it helps me to manually assign resources that I know already persisted in order to properly track further changes and persist them later.

Let me know what you think and if you want me to make a change.

Break up into separate components

I'm thinking about breaking up Spine into different public components. This allows you to for example just use the Serializer and handle all the networking yourself. The current monolithic structure might put people off if they're just looking for one part of the functionality.

Error/failure handling needs to be redesigned

So, I had a hard time figuring out how to return error messages so I can show them in the UI. After hours of debugging, the answer is we can't do it currently until we redesign the entire error handling logic.

This is the centralization for any work towards this goal.

  • 4xx-5xx aren't handled at all

    Currently, only network errors are being handled; everything else triggers .onSuccess (even 4xx responses) #27

    I have a fix for this in a PR, but it's impossible to unit test without a big rewrite in the CallbackHTTPClient object, which completely replaces the Networking object in tests. Instead of stubbing the server, it's stubbing the client.

    To get make this rewrite not needed, we have to handle errors inside Operations. I believe that'd be correct because a 4xx is not an error, it's just a decline from the server in the API negotiation. An error is an exception, such as a network error.

  • Expose error response to .onFailure block

    Given the expectations inside the code, there's no way to return response body all the way up and expose it in the .onFailure block. I need to show error messages in the UI, but I can't.

    We need to stop returning NSError from Networking and return our own error object, one that supports not just error code (e.g 4xx), but the entire JSON response (not string, actual dictionary) and the JSONAPI standard.

    For example, in my code, inside Networking's performRequest(), I tried something like the following,

    var serializationErr: NSError?
    var jsonResult = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.MutableContainers, error: &serializationErr) as! NSDictionary
    
    // Trying to cramp jsonResult inside userInfo...
    let userInfo = [NSLocalizedFailureReasonErrorKey: jsonResult]
    operationError = NSError(domain: SpineServerErrorDomain, code: response!.statusCode, userInfo: userInfo)

    Inside my .onFailure block, I was able to output the dictionary above,

    println(error.userInfo)
    Optional([NSLocalizedFailureReason: {
        errors =     (
                    {
                code = 11001;
                source =             {
                    pointer = "/data/attributes/email";
                };
                title = "Value not specified.";
            }
        );
    }])
    

    However, when calling println(error.localizedFailureReason), I get -[__NSDictionaryI length]: unrecognized selector sent to instance 0x7ffd148788c0 because clearly a String was expected, not a dictionary.

Sample application

Hi,

Is there any sample project or tutorial showing how to setup a Spine backed application?

That would be really helpful to get a clearer picture of its features.

Thanks,
BQ.

SwiftyJSON version

Spine currently (0.4) depends on SwiftyJSON 2.3.3, which already exists on GitHub but has not yet been released to CocoaPods.

DeserializeOperation taking 5s to compile

When I compiled using the following line, I discovered that a Spine function is at the top of the list of things taking the longest.

xcodebuild -workspace myApp.xcworkspace -scheme myApp clean build OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" | grep [1-9].[0-9]ms | sort -nr > culprits.txt

Not sure why it's duplicated, though, but the times are different...

3025.6ms    /Users/MyUser/myApp/Pods/Spine/Spine/DeserializeOperation.swift:338:15  private func resolveRelations()
2933.1ms    /Users/MyUser/myApp/Pods/Spine/Spine/DeserializeOperation.swift:338:15  private func resolveRelations()

Not sure how to fix it, just thought I'd share the finding.

Can't build dependencies for local development

I'm trying to build the dependencies with carthage update to send a PR later, but can't build SwiftyJSON. It builds BrightFutures just fine, though. No error messages, no nothing.

→ carthage update
*** Fetching SwiftyJSON
*** Fetching BrightFutures
*** Checking out BrightFutures at "v1.0.1"
*** Checking out SwiftyJSON at "2.2.0"
*** xcodebuild output can be found in /var/folders/69/b9jl90x976v8w47171_qjpv00000gn/T/carthage-xcodebuild.76R0AN.log
*** Building scheme "BrightFutures-iOS" in BrightFutures.xcodeproj
*** Building scheme "BrightFutures-Mac" in BrightFutures.xcodeproj

→ tree Carthage/Build/
Carthage/Build/
├── Mac
│   └── BrightFutures.framework
│       ├── BrightFutures -> Versions/Current/BrightFutures
│       ├── Headers -> Versions/Current/Headers
│       ├── Modules -> Versions/Current/Modules
│       ├── Resources -> Versions/Current/Resources
│       └── Versions
│           ├── A
│           │   ├── BrightFutures
│           │   ├── Headers
│           │   │   └── BrightFutures-Swift.h
│           │   ├── Modules
│           │   │   ├── BrightFutures.swiftmodule
│           │   │   │   ├── x86_64.swiftdoc
│           │   │   │   └── x86_64.swiftmodule
│           │   │   └── module.modulemap
│           │   └── Resources
│           │       └── Info.plist
│           └── Current -> A
└── iOS
    └── BrightFutures.framework
        ├── BrightFutures
        ├── Headers
        │   └── BrightFutures-Swift.h
        ├── Info.plist
        ├── Modules
        │   ├── BrightFutures.swiftmodule
        │   │   ├── arm.swiftdoc
        │   │   ├── arm.swiftmodule
        │   │   ├── arm64.swiftdoc
        │   │   ├── arm64.swiftmodule
        │   │   ├── i386.swiftdoc
        │   │   ├── i386.swiftmodule
        │   │   ├── x86_64.swiftdoc
        │   │   └── x86_64.swiftmodule
        │   └── module.modulemap
        └── _CodeSignature
            └── CodeResources

18 directories, 20 files

The log file mentioned above has absolutely nothing, no string about SwiftyJSON being touched at all. In the end, it's simply not generating any .framework file (except for BrightFutures).

Could there be a problem in the Cartfile or something? Any idea at all why this isn't building?

😁

Show network activity indicator while loading

This is a nice-to-have. Currently we can explicitly call UIApplication.sharedApplication().networkActivityIndicatorVisible = true before/after each request, but that becomes cumbersome and clutters the codebase with boilerplate. The preferred option is to just provide a boolean to toggle this behavior on/off globally for the Spine instance or the HTTPClient instance. Another option is to provide a hook in HTTPClient for request began/ended so that we could implement this ourselves and/or possibly other behaviors around network activity.

Include top level metadata in the promise of find methods

Right now top level metadata is deserialized, but it is not included in the promise that is returned from find operations.

We need to decide what to return from a FetchOperation. Right now it's a ResourceCollection, but for this it might need to become a (ResourceCollection, NSDictionary) or maybe even a whole JSONAPIDocument.

Relationship not included in payload on PUT

Problem

Relationships are not being included in payloads when the request is an update (PUT). If the request is a creation (POST), the relationship hash is included normally.

Expected behavior: relationships should be included on both POST and PUT.

Example

Given a Post resource that has a Category (dynamic public var category: Category?).

spine.registerResource(Post.resourceType) { Post() }
spine.registerResource(Category.resourceType) { Category() }

var category = Category()
category.id   = "some-uuid"
category.name = "Swift"

var post = Post()
post.id     = "uuid-id"
post.title   = "title"
post.body  = "body"
post.category = category

spine.save(post)

In the case above, a PUT operation takes place because id was set. data.relationships is NOT included in the call.

If we remove post.id = "uuid-id", then a POST operation will take place and with that, data.relationships is included in the payload.


I could very well tackle this issue but I have 5 PRs opened right now and I'd need to know if you'd accept this before starting, specially given the Swift 2 effort.

NSURLSession not setting 'error'

In the lib tests, the mock library is setting the Error just fine, therefore onFailure is triggered correctly.

However, in my own app, the error value from a request is never set, even if the status code is 4xx. Check this out:

screen shot 2015-07-18 at 2 54 22 pm

Here's the console output.

screen shot 2015-07-18 at 2 55 09 pm

I think the tests are broken (the mock code is mistakenly setting the error). Could you check in your own app with real requests whether this is true or not?

I think the solution is to actually rely on the status code, not on urlSession.error.

Replace NSError for api errors

Currently we use NSError objects to represent errors returned in the API response. This makes it hard to access all the error fields that JSON:API specifies. It's probably better to use a custom struct for this,

Nested resource - design question

I'm wading into a a Java implementation and I'm curious what your findOne or find interface looks like for nested URLs. For example when fetch /articles/1/author, as found in this section, how do you call findOne such that you fetch the author from article 1?

Paginatable collections

Add support to load new pages of ResourceCollection and LinkedResourceCollection objects.

Get rid of `fieldsFromDictionary`

The public API would be nicer if fields directly returned a Dictionary instead of an Array. This removes the need for a public fieldsFromDictionary function.

Error using Carthage

Adding github "wvteijlingen/Spine" to the Cartfile and executing the carthage update command, the result is: No tagged versions found for github "wvteijlingen/Spine"

As workaround I have used github "wvteijlingen/Spine" "master"

Signing the entire request for authentication

My API uses an amazon-like authentication system:
Each client has a session ID and a session key created upon authentication. The key is stored in the client and never transmitted again.

Each request then has the following headers:
x-request-time (the time the request is sent in ISO-8601 format)
x-session-id (the session ID from the authentication above)
x-signature (HMAC digest with the session key of the string x-request-time + x-session-id + post-data"(optional - only if present)

In ember data i do this by overriding ajaxOptions (yes, this is private. I haven't found a better place to do it where I can see the stringified data request)

I don't see an obvious way of doing this with Spine? Am I missing something?

Force unwrapping crash in Networking

The error I'm getting exactly is fatal error: unexpectedly found nil while unwrapping an Optional value.

This happens on current master, on the following line:

let response = (response as! NSHTTPURLResponse)

It's easy to reproduce, you just have to make a request when connection is not available (e.g. airplane mode). Basically this is something from a TODO list, so I just wanted to bring attention 😄

Weird EXC_BAD_ACCESS in `enumerateFields` when Fastest level of optimization is used

This is really weird, but I ran into strange behavior which can be reproduced only when app is built in a release mode. Can't reproduce this in a debug mode at all.

I've tried as much as possible to make release mode to be close to debug, including turning off Swift compiler optimizations, with no luck unfortunately.

My app has a few models with String attributes as well as both types of relations. Here is a dump:

Exception Type:  EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x0000000000000018
Triggered by Thread:  9

Thread 9 name:  Dispatch queue: NSOperationQueue 0x170428680 :: NSOperation 0x1740551b0 (QOS: LEGACY)
Thread 9 Crashed:
0   libobjc.A.dylib                 0x000000019758fbd0 objc_msgSend + 16
1   Spine                           0x0000000100324058 Spine.enumerateFields <A>(Spine.ResourceProtocol, A.Type, (A) -> ()) -> () (Spine.swift:403)
2   Spine                           0x00000001002d1ae4 function signature specialization <Arg[0] = Owned To Guaranteed> of Spine.DeserializeOperation.main (Spine.DeserializeOperation)() -> () (DeserializeOperation.swift:340)
3   Spine                           0x00000001002cf9e4 @objc Spine.DeserializeOperation.main (Spine.DeserializeOperation)() -> () (DeserializeOperation.swift:0)
4   Foundation                      0x00000001866d81c8 -[__NSOperationInternal _start:] + 632
5   Spine                           0x000000010031ea24 function signature specialization <Arg[0] = Owned To Guaranteed, Arg[1] = Owned To Guaranteed, Arg[2] = Owned To Guaranteed> of Spine.JSONSerializer.deserializeData (Spine.JSONSerializer)(ObjectiveC.NSData, mappingTargets : [Spine.ResourceProtocol]?) -> Spine.Failable<Spine.JSONAPIDocument> (Serializing.swift:107)
6   Spine                           0x000000010031dbd0 Spine.JSONSerializer.deserializeData (Spine.JSONSerializer)(ObjectiveC.NSData, mappingTargets : [Spine.ResourceProtocol]?) -> Spine.Failable<Spine.JSONAPIDocument> (Serializing.swift:99)
7   Spine                           0x00000001002ea88c Spine.FetchOperation.(execute <A : Spine.ResourceProtocol>(Spine.FetchOperation<A>) -> () -> ()).(closure #1) (Operation.swift:126)
8   Spine                           0x00000001002de000 Spine.URLSessionClient.((performRequest in _A65F1CFD10DBD88413D97E892B017C13) (Spine.URLSessionClient) -> (ObjectiveC.NSURLRequest, callback : (statusCode : Swift.Int?, responseData : ObjectiveC.NSData?, networkError : ObjectiveC.NSError?) -> ()) -> ()).(closure #1) (Networking.swift:108)
9   Spine                           0x00000001002dda10 partial apply forwarder for reabstraction thunk helper from @callee_owned (@in (ObjectiveC.NSData!, ObjectiveC.NSURLResponse!, ObjectiveC.NSError!)) -> (@out ()) to @callee_owned (@owned ObjectiveC.NSData!, @owned ObjectiveC.NSURLResponse!, @owned ObjectiveC.NSError!) -> (@unowned ()) (Networking.swift:0)
10  CFNetwork                       0x000000018521b24c __49-[__NSCFLocalSessionTask _task_onqueue_didFinish]_block_invoke + 296
11  Foundation                      0x00000001867971c0 __NSBLOCKOPERATION_IS_CALLING_OUT_TO_A_BLOCK__ + 12
12  Foundation                      0x00000001866e8600 -[NSBlockOperation main] + 92
13  Foundation                      0x00000001866d81c8 -[__NSOperationInternal _start:] + 632
14  Foundation                      0x0000000186799f24 __NSOQSchedule_f + 224
15  libdispatch.dylib               0x0000000197bf9950 _dispatch_client_callout + 12
16  libdispatch.dylib               0x0000000197c040a0 _dispatch_queue_drain + 1444
17  libdispatch.dylib               0x0000000197bfca58 _dispatch_queue_invoke + 128
18  libdispatch.dylib               0x0000000197c06314 _dispatch_root_queue_drain + 716
19  libdispatch.dylib               0x0000000197c07c48 _dispatch_worker_thread3 + 104
20  libsystem_pthread.dylib         0x0000000197dd9228 _pthread_wqthread + 812
21  libsystem_pthread.dylib         0x0000000197dd8eec start_wqthread + 0

Any ideas?

Pagination initializers

Hi,

I get an error when I try to add pagination to my query.

The following code:

var query = Query(resourceType: Post.self)
query.paginate(OffsetBasedPagination(offset: 0, limit: 5))

results in the following error:

'OffsetBasedPagination' cannot be constructed because it has no accessible initializers

I have tried adding the initializer to struct myself but it doesn't seem to make a difference.

Cheers!

Override default serializedName for Fields

Hi,

Thanks for the great work on Spine - it's fantastic 👍 .

The API I'm working with always uses underscored strings for serialization. Rather than specifying the serialized name every time with Attribute().serializedAs("underscored_name_here") I'd instead like to override or subclass Attribute to make this the default.

This seems tricky currently because none of the properties of the Field class are public, and hence can't be overridden or accessed from a subclass inside a my project.

Could the name and serializedName properties be made public? I'm happy to provide a patch. I also tried to provide a different implementation of fieldsFromDictionary, but a similar problem exists because the name field is not accessible. Or perhaps there is a better way to do this?

I'm using the swift-2.0 branch.

Cheers.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.