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
HalleyCodableprotocol. - A class/struct must define
_linksvariable which is used while traversing the model's tree. CodingKeysshould conformIncludeKeyprotocol 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=USThe 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
}We believe that the community can help us improve and build better a product. Please refer to our contributing guide to learn about the types of contributions we accept and the process for submitting them.
To ensure that our community remains respectful and professional, we defined a code of conduct that we expect all contributors to follow.
We appreciate your interest and look forward to your contributions.
Copyright 2024 Infinum
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
- Filip Gulan - [email protected]
- Zoran Turk - [email protected]
Maintained and sponsored by Infinum.
