JS>> Vox>> 返回
项目作者: aronbalog

项目描述 :
Swift JSON:API client framework
高级语言: Swift
项目地址: git://github.com/aronbalog/Vox.git
创建时间: 2018-02-24T17:36:23Z
项目社区:https://github.com/aronbalog/Vox

开源协议:MIT License

下载


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.

  1. import Vox
  2. class Person: Resource {
  3. @objc dynamic var name: String?
  4. }
  5. let person = Person()
  6. person.name = "Sherlock Holmes"
  7. 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

  1. pod 'Vox'

With Alamofire plugin

  1. pod 'Vox/Alamofire'

Usage

Defining resource

  1. import Vox
  2. class Article: Resource {
  3. /*--------------- Attributes ---------------*/
  4. @objc dynamic
  5. var title: String?
  6. @objc dynamic
  7. var descriptionText: String?
  8. @objc dynamic
  9. var keywords: [String]?
  10. @objc dynamic
  11. var viewsCount: NSNumber?
  12. @objc dynamic
  13. var isFeatured: NSNumber?
  14. @objc dynamic
  15. var customObject: [String: Any]?
  16. /*------------- Relationships -------------*/
  17. @objc dynamic
  18. var authors: [Person]?
  19. @objc dynamic
  20. var editor: Person?
  21. /*------------- Resource type -------------*/
  22. // resource type must be defined
  23. override class var resourceType: String {
  24. return "articles"
  25. }
  26. /*------------- Custom coding -------------*/
  27. override class var codingKeys: [String : String] {
  28. return [
  29. "descriptionText": "description"
  30. ]
  31. }
  32. }

Serializing

Single resource

  1. import Vox
  2. let person = Person()
  3. person.name = "John Doe"
  4. person.age = .null
  5. person.gender = "male"
  6. person.favoriteArticle = .null()
  7. let json: [String: Any] = try! person.documentDictionary()
  8. // or if `Data` is needed
  9. let data: Data = try! person.documentData()

Previous example will resolve to following JSON:

  1. {
  2. "data": {
  3. "attributes": {
  4. "name": "John Doe",
  5. "age": null,
  6. "gender": "male"
  7. },
  8. "type": "persons",
  9. "id": "id-1",
  10. "relationships": {
  11. "favoriteArticle": {
  12. "data": null
  13. }
  14. }
  15. }
  16. }

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

  1. import Vox
  2. let article = Article()
  3. article.id = "article-identifier"
  4. let person1 = Person()
  5. person1.id = "id-1"
  6. person1.name = "John Doe"
  7. person1.age = .null
  8. person1.gender = "male"
  9. person1.favoriteArticle = article
  10. let person2 = Person()
  11. person2.id = "id-2"
  12. person2.name = "Mr. Nobody"
  13. person2.age = 99
  14. person2.gender = .null
  15. person2.favoriteArticle = .null()
  16. let json: [String: Any] = try! [person1, person2].documentDictionary()
  17. // or if `Data` is needed
  18. let data: Data = try! [person1, person2].documentData()

Previous example will resolve to following JSON:

  1. {
  2. "data": [
  3. {
  4. "attributes": {
  5. "name": "John Doe",
  6. "age": null,
  7. "gender": "male"
  8. },
  9. "type": "persons",
  10. "id": "id-1",
  11. "relationships": {
  12. "favoriteArticle": {
  13. "data": {
  14. "id": "article-identifier",
  15. "type": "articles"
  16. }
  17. }
  18. }
  19. },
  20. {
  21. "attributes": {
  22. "name": "Mr. Nobody",
  23. "age": 99,
  24. "gender": null
  25. },
  26. "type": "persons",
  27. "id": "id-2",
  28. "relationships": {
  29. "favoriteArticle": {
  30. "data": null
  31. }
  32. }
  33. }
  34. ]
  35. }

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

  1. import Vox
  2. let data: Data // -> provide data received from JSONAPI server
  3. let deserializer = Deserializer.Single<Article>()
  4. do {
  5. let document = try deserializer.deserialize(data: self.data)
  6. // `document.data` is an Article object
  7. } catch JSONAPIError.API(let errors) {
  8. // API response is valid JSONAPI error document
  9. errors.forEach { error in
  10. print(error.title, error.detail)
  11. }
  12. } catch JSONAPIError.serialization {
  13. print("Given data is not valid JSONAPI document")
  14. } catch {
  15. print("Something went wrong. Maybe `data` does not contain valid JSON?")
  16. }

Resource collection

  1. import Vox
  2. let data: Data // -> provide data received from JSONAPI server
  3. let deserializer = Deserializer.Collection<Article>()
  4. let document = try! deserializer.deserialize(data: self.data)
  5. // `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:

  1. func executeRequest(path: String,
  2. method: String,
  3. queryItems: [URLQueryItem],
  4. bodyParameters: [String : Any]?,
  5. success: @escaping ClientSuccessBlock,
  6. failure: @escaping ClientFailureBlock,
  7. 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:

  1. let baseURL = URL(string: "http://demo7377577.mockable.io")!
  2. let client = JSONAPIClient.Alamofire(baseURL: baseURL)
  3. let dataSource = DataSource<Article>(strategy: .path("vox/articles"), client: client)
  4. dataSource
  5. .fetch()
  6. ...
Installation
  1. pod 'Vox/Alamofire'

Fetching single resource

  1. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
  2. dataSource
  3. .fetch(id:"1")
  4. .include([
  5. "favoriteArticle"
  6. ])
  7. .result({ (document: Document<Person>) in
  8. let person = document?.data // ➜ `person` is `Person?` type
  9. }) { (error) in
  10. if let error = error as? JSONAPIError {
  11. switch error {
  12. case .API(let errors):
  13. ()
  14. default:
  15. ()
  16. }
  17. }
  18. }

Fetching resource collection

  1. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  2. dataSource(url: url)
  3. .fetch()
  4. .include([
  5. "favoriteArticle"
  6. ])
  7. .result({ (document: Document<[Person]>) in
  8. let persons = document.data // ➜ `persons` is `[Person]?` type
  9. }) { (error) in
  10. }

Creating resource

  1. let person = Person()
  2. person.id = "1"
  3. person.name = "Name"
  4. person.age = 40
  5. person.gender = "female"
  6. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  7. dataSource
  8. .create(person)
  9. .result({ (document: Document<Person>?) in
  10. let person = document?.data // ➜ `person` is `Person?` type
  11. }) { (error) in
  12. }

Updating resource

  1. let person = Person()
  2. person.id = "1"
  3. person.age = 41
  4. person.gender = .null
  5. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>/<id>"), client: client)
  6. dataSource
  7. .update(resource: person)
  8. .result({ (document: Document<Person>?) in
  9. let person = document?.data // ➜ `person` is `Person?` type
  10. }) { (error) in
  11. }

Deleting resource

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

Pagination

Pagination on initial request
Custom pagination strategy
  1. let paginationStrategy: PaginationStrategy // -> your object conforming `PaginationStrategy` protocol
  2. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  3. dataSource
  4. .fetch()
  5. .paginate(paginationStrategy)
  6. .result({ (document) in
  7. }, { (error) in
  8. })
Page-based pagination strategy
  1. let paginationStrategy = Pagination.PageBased(number: 1, size: 10)
  2. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  3. dataSource
  4. .fetch()
  5. .paginate(paginationStrategy)
  6. .result({ (document) in
  7. }, { (error) in
  8. })
Offset-based pagination strategy
  1. let paginationStrategy = Pagination.OffsetBased(offset: 10, limit: 10)
  2. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  3. dataSource
  4. .fetch()
  5. .paginate(paginationStrategy)
  6. .result({ (document) in
  7. }, { (error) in
  8. })
Cursor-based pagination strategy
  1. let paginationStrategy = Pagination.CursorBased(cursor: "cursor")
  2. let dataSource = DataSource<Person>(strategy: .path("custom-path/<type>"), client: client)
  3. dataSource
  4. .fetch()
  5. .paginate(paginationStrategy)
  6. .result({ (document) in
  7. }, { (error) in
  8. })
Appending next page to current document
  1. document.appendNext({ (data) in
  2. // data.old -> Resource values before pagination
  3. // data.new -> Resource values from pagination
  4. // data.all -> Resource values after pagination
  5. // document.data === data.all -> true
  6. }, { (error) in
  7. })
Fetching next document page
  1. document.next?.result({ (nextDocument) in
  2. // `nextDocument` is same type as `document`
  3. }, { (error) in
  4. })
Fetching previous document page
  1. document.previous?.result({ (previousDocument) in
  2. // `previousDocument` is same type as `document`
  3. }, { (error) in
  4. })
Fetching first document page
  1. document.first?.result({ (firstDocument) in
  2. // `firstDocument` is same type as `document`
  3. }, { (error) in
  4. })
Fetching last document page
  1. document.last?.result({ (lastDocument) in
  2. // `lastDocument` is same type as `document`
  3. }, { (error) in
  4. })
Reloading current document page
  1. document.reload?.result({ (reloadedDocument) in
  2. // `reloadedDocument` is same type as `document`
  3. }, { (error) in
  4. })

Custom routing

Generating URL for resources can be automated.

Make a new object conforming Router. Simple example:

  1. class ResourceRouter: Router {
  2. func fetch(id: String, type: Resource.Type) -> String {
  3. let type = type.resourceType
  4. return type + "/" + id // or "<type>/<id>"
  5. }
  6. func fetch(type: Resource.Type) -> String {
  7. return type.resourceType // or "<type>"
  8. }
  9. func create(resource: Resource) -> String {
  10. return resource.type // or "<type>"
  11. }
  12. func update(resource: Resource) -> String {
  13. let type = type.resourceType
  14. return type + "/" + id // or "<type>/<id>"
  15. }
  16. func delete(id: String, type: Resource.Type) -> String {
  17. let type = type.resourceType
  18. return type + "/" + id // or "<type>/<id>"
  19. }
  20. }

Then you would use:

  1. let router = ResourceRouter()
  2. let dataSource = DataSource<Person>(strategy: .router(router), client: client)
  3. dataSource
  4. .fetch()
  5. ...

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