-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
1 changed file
with
194 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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<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: | ||
|
||
```swift | ||
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: | ||
|
||
```swift | ||
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](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<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: | ||
|
||
```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 - [email protected] | ||
|