Code Monkey home page Code Monkey logo

vox's Introduction

Vox

Vox is a Swift JSONAPI standard implementation. 🔌

🔜 More stable version (written in Swift 5) coming soon.

Build Status codecov Platform CocoaPods Compatible

The magic behind

Vox combines Swift with Objective-C dynamism and C selectors. During serialization and deserialization JSON is not mapped to resource object(s). Instead, it uses Marshalling and Unmarshalling techniques to deal with direct memory access and performance challenges. Proxy (surrogate) design pattern gives us an opportunity to manipulate JSON's value directly through class properties and vice versa.

import Vox

class Person: Resource {
    @objc dynamic var name: String?
}

let person = Person()
    person.name = "Sherlock Holmes"
    
    print(person.attributes?["name"]) // -> "Sherlock Holmes"

Let's explain what's going on under the hood!

  • Setting the person's name won't assign the value to Person object. Instead it will directly mutate the JSON behind (the one received from server).

  • Getting the property will actually resolve the value in JSON (it points to its actual memory address).

  • When values in resource's attributes or relationship dictionaries are directly changed, getting the property value will resolve to the one changed in JSON.

Every attribute or relationship (Resource subclass property) must have @objc dynamic prefix to be able to do so.

Think about your Resource classes as strong typed interfaces to a JSON object.

This opens up the possibility to easily handle the cases with:

  • I/O performance
  • polymorphic relationships
  • relationships with circular references
  • lazy loading resources from includes list

Installation

Requirements

  • Xcode 9
  • Cocoapods

Basic

pod 'Vox'

With Alamofire plugin

pod 'Vox/Alamofire'

Usage

Defining resource

import Vox

class Article: Resource {

    /*--------------- Attributes ---------------*/
    
    @objc dynamic
    var title: String?
    
    @objc dynamic
    var descriptionText: String?

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

    @objc dynamic
    var editor: Person?

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

    // resource type must be defined
    override class var resourceType: String {
        return "articles"
    }

    /*------------- Custom coding -------------*/

    override class var codingKeys: [String : String] {
        return [
            "descriptionText": "description"
        ]
    }
}

Serializing

Single resource

import Vox
            
let person = Person()
    person.name = "John Doe"
    person.age = .null
    person.gender = "male"
    person.favoriteArticle = .null()
            
let json: [String: Any] = try! person.documentDictionary()

// or if `Data` is needed
let data: Data = try! person.documentData()

Previous example will resolve to following JSON:

{
  "data": {
    "attributes": {
      "name": "John Doe",
      "age": null,
      "gender": "male"
    },
    "type": "persons",
    "id": "id-1",
    "relationships": {
      "favoriteArticle": {
        "data": null
      }
    }
  }
}

In this example favorite article is unassigned from person. To do so, use .null() on resource properties and .null on all other properties.

Resource collection

import Vox

let article = Article()
    article.id = "article-identifier"

let person1 = Person()
    person1.id = "id-1"
    person1.name = "John Doe"
    person1.age = .null
    person1.gender = "male"
    person1.favoriteArticle = article

let person2 = Person()
    person2.id = "id-2"
    person2.name = "Mr. Nobody"
    person2.age = 99
    person2.gender = .null
    person2.favoriteArticle = .null()


let json: [String: Any] = try! [person1, person2].documentDictionary()

// or if `Data` is needed
let data: Data = try! [person1, person2].documentData()

Previous example will resolve to following JSON:

{
  "data": [
    {
      "attributes": {
        "name": "John Doe",
        "age": null,
        "gender": "male"
      },
      "type": "persons",
      "id": "id-1",
      "relationships": {
        "favoriteArticle": {
          "data": {
            "id": "article-identifier",
            "type": "articles"
          }
        }
      }
    },
    {
      "attributes": {
        "name": "Mr. Nobody",
        "age": 99,
        "gender": null
      },
      "type": "persons",
      "id": "id-2",
      "relationships": {
        "favoriteArticle": {
          "data": null
        }
      }
    }
  ]
}

Nullability

Use .null() on Resource type properties or .null on any other type properties.

  • Setting property value to .null (or .null()) will result in JSON value being set to null
  • Setting property value to nil will remove value from JSON

Deserializing

Single resource

import Vox

let data: Data // -> provide data received from JSONAPI server

let deserializer = Deserializer.Single<Article>()

do {
    let document = try deserializer.deserialize(data: self.data)
    
    // `document.data` is an Article object
    
} catch JSONAPIError.API(let errors) {
    // API response is valid JSONAPI error document
    errors.forEach { error in
        print(error.title, error.detail)
    }
} catch JSONAPIError.serialization {
    print("Given data is not valid JSONAPI document")
} catch {
    print("Something went wrong. Maybe `data` does not contain valid JSON?")
}

Resource collection

import Vox

let data: Data // -> provide data received from JSONAPI server

let deserializer = Deserializer.Collection<Article>()

let document = try! deserializer.deserialize(data: self.data)

// `document.data` is an [Article] object

Description

Provided data must be Data object containing valid JSONAPI document or error. If this preconditions are not met, JSONAPIError.serialization error will be thrown.

Deserializer can also be declared without generic parameter but in that case the resource's data property may need an enforced casting on your side so using generics is recommended.

Document<DataType: Any> has following properties:

Property Type Description
data DataType Contains the single resource or resource collection
meta [String: Any] meta dictionary
jsonapi [String: Any] jsonApi dictionary
links Links Links object, e.g. can contain pagination data
included [[String: Any]] included array of dictionaries

Networking

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

Client protocol

Implement following method from Client protocol:

func executeRequest(path: String,
                  method: String,
              queryItems: [URLQueryItem],
          bodyParameters: [String : Any]?,
                 success: @escaping ClientSuccessBlock,
                 failure: @escaping ClientFailureBlock,
                userInfo: [String: Any])

where

  • ClientSuccessBlock = (HTTPURLResponse?, Data?) -> Void
  • ClientFailureBlock = (Error?, Data?) -> Void

Note:

userInfo contains custom data you can pass to the client to do some custom logic: e.g. add some extra headers, add encryption etc.

Alamofire client plugin

If custom networking is not required, there is a plugin which wraps Alamofire and provides networking client in accordance with JSON:API specification.

Alamofire is Elegant HTTP Networking in Swift

Example:

let baseURL = URL(string: "http://demo7377577.mockable.io")!
let client = JSONAPIClient.Alamofire(baseURL: baseURL)
let dataSource = DataSource<Article>(strategy: .path("vox/articles"), client: client)

dataSource
    .fetch()
    ...
Installation
pod 'Vox/Alamofire'

Fetching single resource

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)

dataSource
    .fetch(id:"1")
    .include([
        "favoriteArticle"
    ])
    .result({ (document: Document<Person>) in
        let person = document?.data // ➜ `person` is `Person?` type
    }) { (error) in
        if let error = error as? JSONAPIError {
            switch error {
            case .API(let errors):
                ()
            default:
                ()
            }
        }
    }

Fetching resource collection

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource(url: url)
    .fetch()
    .include([
        "favoriteArticle"
    ])
    .result({ (document: Document<[Person]>) in
        let persons = document.data // ➜ `persons` is `[Person]?` type
    }) { (error) in
        
    }

Creating resource

let person = Person()
    person.id = "1"
    person.name = "Name"
    person.age = 40
    person.gender = "female"
            
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource
    .create(person)
    .result({ (document: Document<Person>?) in
        let person = document?.data // ➜ `person` is `Person?` type
    }) { (error) in
        
    }

Updating resource

let person = Person()
    person.id = "1"
    person.age = 41
    person.gender = .null
            
let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)

dataSource
    .update(resource: person)
    .result({ (document: Document<Person>?) in
        let person = document?.data // ➜ `person` is `Person?` type
    }) { (error) in
        
    }

Deleting resource

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
            
dataSource
    .delete(id: "1")
    .result({
            
    }) { (error) in
        
    }

Pagination

Pagination on initial request
Custom pagination strategy
let paginationStrategy: PaginationStrategy // -> your object conforming `PaginationStrategy` protocol

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource
    .fetch()
    .paginate(paginationStrategy)
    .result({ (document) in
        
    }, { (error) in
        
    })
Page-based pagination strategy
let paginationStrategy = Pagination.PageBased(number: 1, size: 10)

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource
    .fetch()
    .paginate(paginationStrategy)
    .result({ (document) in
        
    }, { (error) in
        
    })
Offset-based pagination strategy
let paginationStrategy = Pagination.OffsetBased(offset: 10, limit: 10)

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource
    .fetch()
    .paginate(paginationStrategy)
    .result({ (document) in
        
    }, { (error) in
        
    })
Cursor-based pagination strategy
let paginationStrategy = Pagination.CursorBased(cursor: "cursor")

let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)

dataSource
    .fetch()
    .paginate(paginationStrategy)
    .result({ (document) in
        
    }, { (error) in
        
    })
Appending next page to current document
document.appendNext({ (data) in
    // data.old -> Resource values before pagination
    // data.new -> Resource values from pagination
    // data.all -> Resource values after pagination
    
    // document.data === data.all -> true
}, { (error) in

})
Fetching next document page
document.next?.result({ (nextDocument) in
    // `nextDocument` is same type as `document`
}, { (error) in
    
})
Fetching previous document page
document.previous?.result({ (previousDocument) in
    // `previousDocument` is same type as `document`
}, { (error) in
    
})
Fetching first document page
document.first?.result({ (firstDocument) in
    // `firstDocument` is same type as `document`
}, { (error) in
    
})
Fetching last document page
document.last?.result({ (lastDocument) in
    // `lastDocument` is same type as `document`
}, { (error) in
    
})
Reloading current document page
document.reload?.result({ (reloadedDocument) in
    // `reloadedDocument` is same type as `document`
}, { (error) in
    
})

Custom routing

Generating URL for resources can be automated.

Make a new object conforming Router. Simple example:

class ResourceRouter: Router {
    func fetch(id: String, type: Resource.Type) -> String {
        let type = type.resourceType
        
        return type + "/" + id // or "<type>/<id>"
    }
    
    func fetch(type: Resource.Type) -> String {
        return type.resourceType // or "<type>"
    }
    
    func create(resource: Resource) -> String {
        return resource.type // or "<type>"
    }
    
    func update(resource: Resource) -> String {
        let type = type.resourceType
        
        return type + "/" + id // or "<type>/<id>"
    }
    
    func delete(id: String, type: Resource.Type) -> String {
        let type = type.resourceType
        
        return type + "/" + id // or "<type>/<id>"
    }
}

Then you would use:

let router = ResourceRouter()

let dataSource = DataSource<Person>(strategy: .router(router), client: client)

dataSource
    .fetch()
    ...

Tests

  • DataSource with router and client when creating resource invokes execute request on client
  • DataSource with router and client when creating resource invokes correct method on router
  • DataSource with router and client when creating resource passes correct parameters to router
  • DataSource with router and client when creating resource client receives correct data from router for execution
  • DataSource with router and client when fetching single resource invokes execute request on client
  • DataSource with router and client when fetching single resource invokes correct method on router
  • DataSource with router and client when fetching single resource passes correct parameters to router
  • DataSource with router and client when fetching single resource client receives correct data from router for execution
  • DataSource with router and client when fetching resource collection invokes execute request on client
  • DataSource with router and client when fetching resource collection invokes correct method on router
  • DataSource with router and client when fetching resource collection passes correct parameters to router
  • DataSource with router and client when fetching resource collection client receives correct data from router for execution
  • DataSource with router and client when updating resource invokes execute request on client
  • DataSource with router and client when updating resource invokes correct method on router
  • DataSource with router and client when updating resource passes correct parameters to router
  • DataSource with router and client when updating resource client receives correct data from router for execution
  • DataSource with router and client when deleting resource invokes execute request on client
  • DataSource with router and client when deleting resource invokes correct method on router
  • DataSource with router and client when deleting resource passes correct parameters to router
  • DataSource with router and client when deleting resource client receives correct data from router for execution
  • DataSource with path and client when creating resource invokes execute request on client
  • DataSource with path and client when creating resource client receives correct data for execution
  • DataSource with path and client when creating resource client receives userInfo for execution
  • DataSource with path and client when fetching single resource invokes execute request on client
  • DataSource with path and client when fetching single resource client receives correct data for execution
  • DataSource with path and client when fetching resource collection with custom pagination invokes execute request on client
  • DataSource with path and client when fetching resource collection with custom pagination client receives correct data for execution
  • DataSource with path and client when fetching resource collection with page based pagination invokes execute request on client
  • DataSource with path and client when fetching resource collection with page based pagination client receives correct data for execution
  • DataSource with path and client when fetching resource collection with offset based pagination invokes execute request on client
  • DataSource with path and client when fetching resource collection with offset based pagination client receives correct data for execution
  • DataSource with path and client when fetching resource collection with cursor based pagination invokes execute request on client
  • DataSource with path and client when fetching resource collection with cursor based pagination client receives correct data for execution
  • DataSource with path and client when updating resource invokes execute request on client
  • DataSource with path and client when updating resource client receives correct data for execution
  • DataSource with path and client when deleting resource invokes execute request on client
  • DataSource with path and client when deleting resource client receives correct data for execution
  • Deserializer when deserializing resource collection maps correctly
  • Deserializer when deserializing single resource and error data provided with source object included in errors maps to errors object
  • Deserializer when deserializing single resource and error data provided with source object included in errors maps to errors object 2
  • Deserializer when deserializing document with polymorphic objects in relationships maps correctly
  • Deserializer when deserializing single resource maps correctly
  • Paginated DataSource when fetching first page returns first page document
  • Paginated DataSource when fetching first page when fetching next page returns next page document
  • Paginated DataSource when fetching first page returns first page document 2
  • Paginated DataSource when fetching first page when fetching first page of document returns first page document
  • Paginated DataSource when fetching first page returns first page document 3
  • Paginated DataSource when fetching first page when appending next page document is appended
  • Paginated DataSource when fetching first page when appending next page included is appended
  • Paginated DataSource when fetching first page returns first page document 4
  • Paginated DataSource when fetching first page when reloading current page receives page
  • Paginated DataSource when fetching first page returns first page document 5
  • Paginated DataSource when fetching first page when fetching previous page receives page
  • Paginated DataSource when fetching first page returns first page document 6
  • Paginated DataSource when fetching first page when fetching last page returns last page document
  • Serializer when serializing resource collection maps correctly
  • Serializer when serializing resource collection returns document data
  • Serializer when serializing resource collection returns document dictionary
  • Serializer when serializing single resource maps correctly
  • Serializer when serializing single resource returns document data
  • Serializer when serializing single resource returns document dictionary

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

MIT

vox's People

Contributors

aronbalog avatar jfejza 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

vox's Issues

Write functions just on time

Hello Mr Balog. I’m using your Vox library , I have a question . I use it in whole of my project , I write repeating code every time and it’s not ok. Look at this attached swift file , this is my function that every time I use. Is there any way to write it once and call it every where I need and just change resource and method every time not to write all function , because I am doing same in on failure block every time .
I’ll be really happy if you help me dear Aron Balog .

let client = JSONAPIClient.Alamofire(baseURL: baseURL)
let dataSource = DataSource<resourse>(strategy: .path("/path"), client: client)
do{
    try dataSource
        .fetch().include(["something"])
        .result({ (document: Document<[resourse]>) in
            let results = document.data
            
            // do some thing
            
        }) { (error) in
            // and do this every time
            if let error = error as? JSONAPIError {
                switch error {
                case .API( let errors):
                    ApiResponse(response: errors)
                default:()
                }
                return
            }else{
                ApiResponse(response: nil)
            }
    }
}catch let error_object{
    print(error_object)
}

usefulness of userInfo

I am not sure the userInfo is the best way to handle user-specific data.
In the documentation it says that userInfo is used for adding headers at run-time.
I think headers should be an attribute of Request and methods such as Request::addHeader and Request::removeHeader should be used to manipulate headers to be able to do something like

personDataSource
    .fetch(1)
    .addHeader("token", "YsbV6gL35d")
    .result(/* [..] */)

At another extent, the Alamofire Client does not uses the userData for headers here

Relationship object not deserialized

If you try to deserialize an object such as

  "data": {
      "attributes": {
        "name": "John Doe",
        "age": null,
        "gender": "male"
      },
      "type": "persons",
      "id": "id-1",
      "relationships": {
        "favoriteArticle": {
          "data": {
            "id": "article-identifier",
            "type": "articles"
          }
        }
      }
    }

when trying to get person.favoritArticle.id you are getting "" because When fetching the attribute, the relationship is properly found and the Context tries to get the relationship In Context_Query but the ResourcePool has no knowledge of a favoriteArticle with the ID article-identifier, the the value is null...
I think it should be a nice feature to add the objects in the resourcePool either at the deserialization or even bette when a relationship exists and the object is not found, creating it.

Documentation is incomplete

Hello,

Thanks for the library.

let dataSource = DataSource(strategy: .path("/"), client: client)

I'm trying to create a request but i still can't find what the "client" must be?The documentation is incomplete and can't find any information about it.

Best,

Pagination struct is not visible

I've installed Vox using Cocoapods. But when I write:
let paginationStrategy = Pagination.PageBased(number: 1, size: 10)
the compiler show me the error: 'Use of unresolved identifier 'Pagination'; did you mean 'PaginationData'?'

In my opinion the reason of this issue that Pagination struct is not public. If I change this struct to public - everything is ok.

Allow basic Alamofire client to be subclassed

It'd be nice if the Alamofire client could be subclassed and functions overriden. My use-case is that I want to add pre/post-processing in request handling while using Alamofire client.
To do so, I think it would be a good idea to have a more-atomic control on the request process.

Creating Resource - Issue unwrapping response

Hi There -

I've got a JSONAPI compliant Rails server that I'm using with VOX on a pretty simple Swift app. When tying to create a resource as per the README, I'm running into issues unwrapping the server response which it appears VOX is wrapping in an optional. I've currently got the following:

func authenticate(auth: AuthResource, completion: @escaping (AuthResource) -> ()) {
    let auth = AuthResource()
    auth.email = emailField.text
    auth.password = passwordField.text

    try? dataSource.create(auth).result({ (document: Document<AuthResource>?) in
         let auth = document?.data
         completion(auth)
     }) { (error) in
         print(error!)
     }
}

When I inspect the auth variable I see the server response && document tree however I'm having trouble unwrapping and then gaining access to the variables from the server response. A simple p auth returns nil and if I wrap the completion statement it in an if let it doesn't do anything.

I'm pretty new to Swift but hoping I can get this to work. Any help is appreciated!

Context handling error response as unknown incorrectly.

My response payload contains the following error:

{
    "errors": {
        "detail": {
            "auth": [
                "invalid password"
            ]
        }
    }
}

However Vox will always respond with JSONAPIError.serialization due to not casting the error array correctly:
https://github.com/aronbalog/Vox/blob/master/Vox/Core/Class/Context.swift#L42

(lldb) po dictionary["errors"] as? NSArray
nil

(lldb) po dictionary["errors"] as? NSMutableArray
nil

(lldb) po type(of: dictionary["errors"])
Swift.Optional<Any>

(lldb) po dictionary["errors"]
▿ Optional<Any>
  ▿ some : 1 element
    ▿ 0 : 2 elements
      - key : detail
      ▿ value : 1 element
        ▿ 0 : 2 elements
          - key : auth
          ▿ value : 1 element
            - 0 : invalid email or password

Deserializer not work

Hi, I've tried to convert Data to a JSONAPIDocument struct, and it doesn't throw an error, but when I read documen.data, all the Person attributes return nil. Any suggestion for this problem? Thanks

Here is sample of my code:

let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted)
let deserializer = Deserializer.Collection<Person>()
do {
    let document = try deserializer.deserialize(data: jsonData)
    if let personData = document.data {   
        self?.person = personData
    } 
} catch JSONAPIError.API(let errors) {
    errors.forEach { error in

    }
} catch JSONAPIError.serialization {
    print("Given data is not valid JSONAPI document")
} catch {
    print("Something went wrong. Maybe `data` does not contain valid JSON?")
}

jsonObject type is [String:Any]

Exception: EXC_BAD_INSTRUCTION When accessing nested relationship

When trying to access a nested object my app crashes

let baseURL = Environment.backendURL.appendingPathComponent("api")
            let dataSource = DataSource<ProductOfferResource>(strategy: .path(ProductOfferResource.resourceType), client: self.client)

            try! dataSource.fetch()
                    .result({ (document: Document<[ProductOfferResource]>) in
                                let productOffers = document.data
                                observer.onNext(productOffers ?? [])

                                // Causes crash
                                let background = productOffers?[0].design?.background_image?.meta 

                                observer.onCompleted()
                            }, { (error) in
                                observer.onError(error ?? NetworkServiceError.unknown)
                            })

All objects can be found https://gist.github.com/sadiq81/bf0e77a8561049ecd0a105479fc3ff5a

Filtering Objects

Hi dear Mr Vox 😁 .
I can't filter my objects while fetching from the server . You know I don't know where should I use it and how ?🧐 Could you please help me ???

Rest relationship route

For a resource with relationships, it should be possible to get a relationship route

       "author": {
        "links": {
          "self": "http://example.com/articles/1/relationships/author",
          "related": "http://example.com/articles/1/author"
        },
        "data": { "type": "people", "id": "9" }
      }

At another extent, the JSONAPI does not provides any specifications on routing. I think it'd be nice if we could build REST routes for nested/related resources see this post to a better example of what I mean.

relationship subset

Hi , I have a relationship and I want to set subset for it . How can I do that ?
You know I want POST my relationships and attributes .
We have a URL and we use it to create a request to a server . Cause of this , we should create a class and we have 2 attributes and 2 other attributes which they should be in our relationship as subsets .
I don't know how can I set it as code , How can I define attribute for relationship ????☹️
here is the screenshot which I mean .
screen shot 1397-02-27 at 3 06 13 pm

Relationships missing on deserialization

I'm not sure if this is a bug or if it's an issue with how I'm using the lib, but I can't seem to get related objects to populate correctly.

Given the following JSON:

{
    "data": [
        {
            "attributes": {
                "arrival_time": "2019-01-02T09:27:37-05:00",
                "departure_time": "2019-01-02T09:27:37-05:00",
                "direction_id": 1,
                "schedule_relationship": null,
                "status": null,
                "stop_sequence": 3
            },
            "id": "prediction-39288223-2844-3",
            "relationships": {
                "route": {
                    "data": {
                        "id": "93",
                        "type": "route"
                    }
                },
                "stop": {
                    "data": {
                        "id": "2844",
                        "type": "stop"
                    }
                },
                "trip": {
                    "data": {
                        "id": "39288223",
                        "type": "trip"
                    }
                }
            },
            "type": "prediction"
        },
        {
            "attributes": {
                "arrival_time": "2019-01-02T09:41:32-05:00",
                "departure_time": "2019-01-02T09:41:32-05:00",
                "direction_id": 1,
                "schedule_relationship": null,
                "status": null,
                "stop_sequence": 3
            },
            "id": "prediction-39288313-2844-3",
            "relationships": {
                "route": {
                    "data": {
                        "id": "93",
                        "type": "route"
                    }
                },
                "stop": {
                    "data": {
                        "id": "2844",
                        "type": "stop"
                    }
                },
                "trip": {
                    "data": {
                        "id": "39288313",
                        "type": "trip"
                    }
                }
            },
            "type": "prediction"
        },
        {
            "attributes": {
                "arrival_time": "2019-01-02T09:56:32-05:00",
                "departure_time": "2019-01-02T09:56:32-05:00",
                "direction_id": 1,
                "schedule_relationship": null,
                "status": null,
                "stop_sequence": 3
            },
            "id": "prediction-39288212-2844-3",
            "relationships": {
                "route": {
                    "data": {
                        "id": "93",
                        "type": "route"
                    }
                },
                "stop": {
                    "data": {
                        "id": "2844",
                        "type": "stop"
                    }
                },
                "trip": {
                    "data": {
                        "id": "39288212",
                        "type": "trip"
                    }
                }
            },
            "type": "prediction"
        },
        {
            "attributes": {
                "arrival_time": "2019-01-02T10:14:11-05:00",
                "departure_time": "2019-01-02T10:14:11-05:00",
                "direction_id": 1,
                "schedule_relationship": null,
                "status": null,
                "stop_sequence": 3
            },
            "id": "prediction-39288292-2844-3",
            "relationships": {
                "route": {
                    "data": {
                        "id": "93",
                        "type": "route"
                    }
                },
                "stop": {
                    "data": {
                        "id": "2844",
                        "type": "stop"
                    }
                },
                "trip": {
                    "data": {
                        "id": "39288292",
                        "type": "trip"
                    }
                }
            },
            "type": "prediction"
        }
    ],
    "included": [
        {
            "attributes": {
                "color": "FFC72C",
                "description": "Local Bus",
                "direction_destinations": [
                    "Sullivan",
                    "Downtown Boston"
                ],
                "direction_names": [
                    "Outbound",
                    "Inbound"
                ],
                "long_name": "Sullivan - Downtown Boston",
                "short_name": "93",
                "sort_order": 9300,
                "text_color": "000000",
                "type": 3
            },
            "id": "93",
            "links": {
                "self": "/routes/93"
            },
            "type": "route"
        }
    ],
    "jsonapi": {
        "version": "1.0"
    }
}

And these resource classes:

class MBTAPrediction: Resource {

    @objc dynamic var arrivalTime: String?
    @objc dynamic var route: MBTARoute?

    override static var resourceType: String {
        return "prediction"
    }

    override class var codingKeys: [String : String] {
        return [
            "arrivalTime": "arrival_time"
        ]
    }

}

class MBTARoute: Resource {

    @objc dynamic var long_name: String?
    @objc dynamic var short_name: String?

    override static var resourceType: String {
        return "route"
    }

}

The following code fails because the related route object is nil.

        guard let url = Bundle.main.url(forResource: "mbta_predictions", withExtension: "json"),
            let data = try? Data(contentsOf: url) else {
                fatalError()
        }

        let deserializer = Deserializer.Collection<MBTAPrediction>()
        do {
            let document = try deserializer.deserialize(data: data)
            let predictions = document.data
            let routeDesc = predictions?.first?.route?.long_name
            // routeDesc should not be nil
        } catch {
            NSLog("Error deserializing MBTA response: \(error)")
        }

@aronbalog I found that if I put a break point inside of the ResourcePool where each resource is added and step through slowly, it makes the code work. So I think this maybe some sort of async race condition.

If I'm just doing something wrong, please let me know. Thanks!

ssl pinning in vox

I want to implement ssl pinning in Vox , Could you please help me how to do it ?

JSONAPI error handling

It doesn't seems like Vox handles error with a JSONAPI format. From the specification an error object may look like:

{
    "errors": [
        {
            "id": "BAD",
            "links": {
                "about": "https://my.api/kb/BAD"
            },
            "status": "400",
            "code": "BAD-ERROR-CODE",
            "title": "bad attribute",
            "detail": "name can't be blank",
            "source": {
                "pointer": "/data/attributes/name"
            },
            "meta": {}
        }
    ]
}

For now, on error the data is deserialized as a DataSource's resource type, see here.

Allow ErrorObject to be stringified

I would like to log any error in a text log system. As an error is not a resource, it is not possible to serialize it. We could imagine something like keeping the dictionary from the init() and transform it as json string afterward.

Relationships not deserialize

I am still waiting ... there is no library for json api and I am in force to use your library , please post an update and solve this bug

New resource as relation fails to create

The ResourcePool store resources, with a key that use id and type

    private func keyForBasicObject(_ basicObject: [String: String]) -> String {
        return basicObject["id"]! + "_" + basicObject["type"]!
    }  
    private func keyForResource(_ resource: Resource) -> String {
        return resource.id! + "_" + resource.type
    }

So, any Resource without an id can not be stores. Problem is, when you want to create object and relation on client-side, it fails.
From the JSONAPI :

Resource Objects

“Resource objects” appear in a JSON API document to represent resources.

A resource object MUST contain at least the following top-level members:

id
type
Exception: The id member is not required when the resource object originates at the client and represents a new resource to be created on the server.


Such a Thing would not work :

import Vox

class Tire: Resource {
    override class var resourceType: String {
        return "tires"
    }
}

class Car: Resource {
    
    @objc dynamic
    var tires: [Tire]?
    
    override class var resourceType: String {
        return "cars"
    }
}

func createCar() {
    let c = Car()
    let t = Tire()
    c.tires = [t]
}

I would suggest to add a default id to a resource in ResourcePool::addResource if none is given by the user :

    func addResource(_ resource: Resource) {
        queue.async(flags: .barrier) {
            if resource.id == nil {
                resource.id = resource.internalIdentifier
            }
            self.mapTable.setObject(resource, forKey: self.keyForResource(resource) as NSString)
        }
    }

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.