diff --git a/README.md b/README.md index 22ac838..e2d1ebe 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,200 @@ dependencies: [ ] ``` +## Model + +A typical HAL model class prepared for _Halley_ consists of several parts. + +```swift +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 + +```swift +extension Contact: IncludeableType { + + enum IncludeType { + case full + case contacts + case website + case contactsOfContacts + case contactsAndWebsiteOfContacts + } +} + +extension Contact.IncludeType: IncludeTypeInterface { + typealias IncludeCodingKey = Contact.CodingKeys + + @IncludesBuilder + 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: + +```swift +class AlamofireRequester: RequesterInterface { + + func requestResource( + at url: URL, + completion: @escaping (Result) -> 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: + +```swift +let resourceManager = ResourceManager(requester: AlamofireRequester()) +let request = HalleyRequest( + 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](https://datatracker.ietf.org/doc/html/draft-kelly-json-hal#name-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. + +```swift +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` + +```swift +func resource( + from url: URL, + includes: [String] = [], + options: HalleyKit.Options = .default, + linkResolver: LinkResolver = URLLinkResolver() +) -> some Publisher, Never> + +func resourceCollection( + from url: URL, + includes: [String] = [], + options: HalleyKit.Options = .default, + linkResolver: LinkResolver = URLLinkResolver() +) -> some Publisher, Never> + +func resourceCollectionWithMetadata( + from url: URL, + includes: [String] = [], + options: HalleyKit.Options = .default, + linkResolver: LinkResolver = URLLinkResolver() +) -> some Publisher, 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: + +```swift +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 * Filip Gulan - filip.gulan@infinum.com