Skip to content

Commit

Permalink
feat(hal): initial HAL support
Browse files Browse the repository at this point in the history
  • Loading branch information
tompahoward committed Jan 11, 2021
1 parent da5ab5e commit d5c700a
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 26 deletions.
2 changes: 2 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Lots 😂

Test: Create intial version of library, supporting HAL and Siten and pitch it on twitter.

Info on popularity a features of different hypermedia types at https://www.fabernovel.com/en/article/tech-en/which-technologies-should-you-use-to-build-hypermedia-apis

Success: At least 10 like, retweets or comments.

### Validated
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# waychaser

Client library for HATEOAS level 3 RESTful APIs that provide hypermedia controls using Link ([RFC8288](https://tools.ietf.org/html/rfc8288)) and [Link-Template](https://mnot.github.io/I-D/link-template/) headers.
Client library for HATEOAS level 3 RESTful APIs that provide hypermedia controls using:
- Link ([RFC8288](https://tools.ietf.org/html/rfc8288)) and [Link-Template](https://mnot.github.io/I-D/link-template/) headers.
- [HAL](http://stateless.co/hal_specification.html)

This isomorphic library is compatible with Node.js 10.x, 12.x and 14.x, Chrome, Firefox, Safari, Edge and even IE.
This [isomorphic](https://en.wikipedia.org/wiki/Isomorphic_JavaScript) library is compatible with Node.js 10.x, 12.x and 14.x, Chrome, Firefox, Safari, Edge and even IE.
<img alt="aw yeah!" src="./docs/images/aw_yeah.gif" width="20" height="20" />

[![GitHub license](https://img.shields.io/github/license/mountain-pass/waychaser)](https://github.com/mountain-pass/waychaser/blob/master/LICENSE) [![npm](https://img.shields.io/npm/v/@mountainpass/waychaser)](https://www.npmjs.com/package/@mountainpass/waychaser) [![npm downloads](https://img.shields.io/npm/dm/@mountainpass/waychaser)](https://www.npmjs.com/package/@mountainpass/waychaser)
Expand Down Expand Up @@ -140,6 +142,10 @@ const searchResultsResource = await apiResource.invoke('search', {
- [x] add tests for multiple parameters
- [x] add automate CHANGELOG.md
- [ ] add support for HAL
- [x] add support for simple self `_links`
- [ ] add support for more general `_links`
- [ ] add support for curies and curied `_links`
- [ ] add support for `_links` in `_embedded` resources
- [ ] add support for Siren
- [ ] add tests for authenticated requests
- [ ] upgrade webpack
Expand Down
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"hateoas-client",
"client",
"hypermedia",
"hypermedia-client"
"hypermedia-client",
"RFC8288",
"link-header",
"link-template-header",
"hal"
],
"main": "dist/waychaser.js",
"unpkg": "dist/waychaser.umd.min.js",
Expand Down
13 changes: 13 additions & 0 deletions src/test/hal.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@

Feature: Invoke Operation

So that I can perform actions on a resource
As a developer
I want to be able to invoke operations

@wip
Scenario: Invoke operation - self
Given a HAL resource with a "self" link that returns itself
When waychaser successfully loads that resource
And we successfully invoke the "self" operation
Then the same resource will be returned
2 changes: 0 additions & 2 deletions src/test/invoke-operation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,6 @@ Feature: Invoke Operation
| PATCH | application/json |
| PATCH | multipart/form-data |

@wip
Scenario Outline: Invoke operation - multiple body parameters with extra params
Given a resource with a "https://waychaser.io/rel/pong" operation with the "<METHOD>" method that returns the following "<CONTENT-TYPE>" provided parameters and the content type
| NAME | TYPE |
Expand Down Expand Up @@ -404,7 +403,6 @@ Feature: Invoke Operation
| PUT | application/json | path | query | body |
| PUT | multipart/form-data | path | query | body |

@wip
Scenario Outline: Invoke operation - multiple parameters of differnent type, including body with extra params
Given a resource with a "https://waychaser.io/rel/pong" operation with the "<METHOD>" method that returns the following "<CONTENT-TYPE>" provided parameters and the content type
| NAME | TYPE |
Expand Down
13 changes: 13 additions & 0 deletions src/test/resource.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,19 @@ Given(
}
)

Given(
'a HAL resource with a {string} link that returns itself',
async function (relationship) {
this.currentResourceRoute = randomApiPath()
const to = this.currentResourceRoute
const router = await this.router.route(this.currentResourceRoute)
await router.get(async (request, response) => {
response.header('content-type', 'application/hal+json')
response.status(200).send({ status: 200, _links: { self: { href: to } } })
})
}
)

Given(
'a resource with a {string} operation that returns an error',
async function (relationship) {
Expand Down
83 changes: 62 additions & 21 deletions src/waychaser.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,32 @@ polyfill()
*
* @throws {Error} If the server returns with a status >= 400
*/
function loadResource (url, options) {
async function loadResource (url, options) {
logger.waychaser(`loading ${url} with:`)
logger.waychaser(JSON.stringify(options, undefined, 2))
return fetch(url, options).then(response => {
if (!response.ok) {
logger.waychaser(`Bad response from server ${JSON.stringify(response)}`)
throw new Error('Bad response from server', response)
}
logger.waychaser(`Good response from server ${JSON.stringify(response)}`)
/* istanbul ignore next: IE fails without this, but IE doesn't report coverage */
if (response.url === undefined || response.url === '') {
// in ie url is not being populated 🤷‍♂️
response.url = url.toString()
}
const response = await fetch(url, options)
if (!response.ok) {
logger.waychaser(`Bad response from server ${JSON.stringify(response)}`)
throw new Error('Bad response from server', response)
}
logger.waychaser(
`Good response from server ${JSON.stringify(
response
)}, ${response.headers.get('content-type')}`
)
/* istanbul ignore next: IE fails without this, but IE doesn't report coverage */
if (response.url === undefined || response.url === '') {
// in ie url is not being populated 🤷‍♂️
response.url = url.toString()
}
const contentType = response.headers.get('content-type')?.split(';')
if (contentType?.[0] === 'application/hal+json') {
// only consume the body if the content type tells us that the response body will have operations
const body = await response.json()
return new waychaser.ApiResourceObject(response, body, contentType[0])
} else {
return new waychaser.ApiResourceObject(response)
})
}
}

/**
Expand All @@ -46,18 +56,32 @@ function loadResource (url, options) {
*/
function loadOperations (operations, linkHeader, callingContext) {
if (linkHeader) {
logger.debug(linkHeader)
const links = LinkHeader.parse(linkHeader)
addLinksToOperations(operations, links, callingContext)
}
}

operations.insert(
links.refs.map(reference => {
const operation = new Operation(callingContext)
Object.assign(operation, reference)
return operation
/**
* Creates operations from each link in a HAL `_links` and inserts into the operations collection
*
* @param {Loki.Collection} operations the target loki collection to load the operations into
* @param {object} _links HAL links within the response
* @param {fetch.Response} callingContext the reponse object that the links in link header are relative to.
*/
function loadHalOperations (operations, _links, callingContext) {
if (_links) {
const links = new LinkHeader()
Object.keys(_links).forEach(key => {
links.set({
rel: key,
uri: _links[key].href
})
)
})

addLinksToOperations(operations, links, callingContext)
}
}

class Operation {
constructor (callingContext) {
logger.waychaser(
Expand Down Expand Up @@ -172,14 +196,17 @@ const waychaser = {
logger: logger.waychaser,

ApiResourceObject: class {
constructor (response) {
constructor (response, body, contentType) {
this.response = response
const linkHeader = response.headers.get('link')
const linkTemplateHeader = response.headers.get('link-template')
const linkDatabase = new Loki()
this.operations = linkDatabase.addCollection()
loadOperations(this.operations, linkHeader, response)
loadOperations(this.operations, linkTemplateHeader, response)
if (contentType === 'application/hal+json') {
loadHalOperations(this.operations, body._links, response)
}
}

get ops () {
Expand All @@ -193,3 +220,17 @@ const waychaser = {
}

export { waychaser }
/**
* @param operations
* @param links
* @param callingContext
*/
function addLinksToOperations (operations, links, callingContext) {
operations.insert(
links.refs.map(reference => {
const operation = new Operation(callingContext)
Object.assign(operation, reference)
return operation
})
)
}

0 comments on commit d5c700a

Please sign in to comment.