Halley provides a simple way on iOS to parse and traverse models according to JSON Hypertext Application Language specification also known just as HAL.
- iOS 13
- Swift 5.0
There are several ways to include Halley in your project, depending on your use case.
Halley is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'Halley'
If you are using SPM for your dependency manager, add this to the dependencies in your Package.swift
file:
dependencies: [
.package(url: "https://github.com/infinum/Halley.git")
]
A typical HAL model class prepared for Halley consists of several parts.
struct Website: HalleyCodable {
let _links: Links?
let id: String
let url: URL
}
struct Contact: HalleyCodable {
let _links: Links?
let id: String
let name: String
let contacts: [Contact]?
let website: Website?
enum CodingKeys: String, CodingKey, IncludeKey {
case _links
case id
case name
case contacts
case website = "webSiteLink"
}
}
Going from top to bottom, these classes/structs must obey the following list of rules:
- A class/struct must conform
HalleyCodable
protocol. - A class/struct must define
_links
variable which is used while traversing the model's tree. CodingKeys
should conformIncludeKey
protocol for type-safe traversing the tree.
extension Contact: IncludeableType {
enum IncludeType {
case full
case contacts
case website
case contactsOfContacts
case contactsAndWebsiteOfContacts
}
}
extension Contact.IncludeType: IncludeTypeInterface {
typealias IncludeCodingKey = Contact.CodingKeys
@IncludesBuilder<IncludeCodingKey>
public func prepareIncludes() -> [IncludeField] {
switch self {
case .full:
ToMany(.contacts)
ToOne(.website)
case .contacts:
ToMany(.contacts)
case .website:
ToOne(.website)
case .contactsOfContacts:
Nested(Contact.self, including: .contacts, at: .contacts, toMany: true)
case .contactsAndWebsiteOfContacts:
Nested(Contact.self, including: .full, at: .contacts, toMany: true)
ToOne(.website)
}
}
}
To support type-safe traversing and building pre-computed traversing paths (include lists) model should conform IncludeableType
protocol.
Supported include types: ToOne
, ToMany
, and Nested
. Nested
is used in case one needs to fetch nested relationships of an already nested relationship.
Halley is heavily extensible when it comes to fetching the data and traversing. The client needs to implement/conform to RequesterInterface
which will provide the implementation for fetching the specific resource from the given link. For example, with network requests and Alamofire:
class AlamofireRequester: RequesterInterface {
func requestResource(
at url: URL,
completion: @escaping (Result<Data, Error>) -> Void
) -> RequestContainerInterface {
let request = AF
.request(Router(url: url, method: .get))
.responseData() { response in
completion(response.result.mapError { $0 as Error })
}
return RequestContainer(dataRequest: request)
}
}
struct RequestContainer: RequestContainerInterface {
let dataRequest: DataRequest
func cancelRequest() {
dataRequest.cancel()
}
}
Once defined, the requester is used when starting the initial resource request:
let resourceManager = ResourceManager(requester: AlamofireRequester())
let request = HalleyRequest<Contact>(
url: "https//www.example.com/contact/1",
includeType: .contactsAndWebsiteOfContacts,
queryItems: [],
decoder: JSONDecoder()
)
_ = resourceManager
.request(request)
.sink { _ in
// Print error here
} receiveValue: { contact in
// Parsed and traversed Contact
}
Halley supports templated links. Each link will be resolved and templated before creating the request via RequesterInterface
. Templates are resolved via TemplateLinkResolver
and DefaultTemplateHandler.shared
where the client can provide their default values which will be templated before any request made via Halley, or by providing custom queryItems
in HalleyRequest
initializer.
DefaultTemplateHandler
.shared
.updateTemplate(for: "country_key") { "US" }
// Link object:
// { "website": "https://www.example.com/contact/1/website{?country_key}", "templated": true }
// will be resolved as:
// https://www.example.com/contact/1/website?country_key=US
The client can opt-out from using Codable and type-safe parsing and use simplified methods on ResourceManager
func resource(
from url: URL,
includes: [String] = [],
options: HalleyKit.Options = .default,
linkResolver: LinkResolver = URLLinkResolver()
) -> some Publisher<Result<Parameters, Error>, Never>
func resourceCollection(
from url: URL,
includes: [String] = [],
options: HalleyKit.Options = .default,
linkResolver: LinkResolver = URLLinkResolver()
) -> some Publisher<Result<[Parameters], Error>, Never>
func resourceCollectionWithMetadata(
from url: URL,
includes: [String] = [],
options: HalleyKit.Options = .default,
linkResolver: LinkResolver = URLLinkResolver()
) -> some Publisher<Result<Parameters, Error>, Never>
In the case of String
includes, a simple website
string represents a to-one relationship, while a string inside square brackets [contacts]
represents a to-many relationship.
The nested relationship can be achieved via dot-operator like [contacts].website
- this will fetch all the contacts of a top-level object, and for those contacts, Halley will fetch a website of each one of them.
The example above with contacts and website can be transpiled into:
let resourceManager = ResourceManager(requester: AlamofireRequester())
resourceManager
.resource(
from: URL(string: "https//www.example.com/contact/1")!,
includes: [
"[contacts]",
"[contacts].[contacts]",
"[contacts].website",
"website"
],
options: .default,
linkResolver: TemplateLinkResolver(parameters: [:])
)
.sink { _ in
// Print error here
} receiveValue: { dict in
// Parsed and traversed contact dict
}
- Filip Gulan - [email protected]
- Zoran Turk - [email protected]
Maintained and sponsored by Infinum.
Halley is available under the MIT license. See the LICENSE file for more info.