Skip to content

infinum/Halley

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

90 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Halley

Version License Swift Package Manager Platforms

Halley

Halley provides a simple way on iOS to parse and traverse models according to JSON Hypertext Application Language specification also known just as HAL.

Getting started

Requirements

  • iOS 13
  • Swift 5.0

There are several ways to include Halley in your project, depending on your use case.

CocoaPods

Halley is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod 'Halley'

Swift Package Manager

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")
]

Model

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 conform IncludeKey protocol for type-safe traversing the tree.

Traversal paths - Include list

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.

Traversing

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
    }

Templating

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

Manual traversing

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
    }

Author

Maintained and sponsored by Infinum.

License

Halley is available under the MIT license. See the LICENSE file for more info.