Skip to content

Commit

Permalink
feat(siren): inital support for siren links
Browse files Browse the repository at this point in the history
  • Loading branch information
tompahoward committed Jan 15, 2021
1 parent 2adf705 commit 70a5c79
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 52 deletions.
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const json = await apiResource.body()
- [x] switch to single session per browser test
- [x] add tests for multiple parameters
- [x] add automate CHANGELOG.md
- [ ] add support for HAL
- add support for HAL
- [x] add support for simple self `_links`
- [x] add methods for getting consumed body
- [x] add support for more general `_links`
Expand All @@ -163,6 +163,11 @@ const json = await apiResource.body()
- [x] add support for curies and curied `_links`
- [ ] add support for `_links` in `_embedded` resources
- [ ] add support for warning about deprecated `_links`
- add support for Siren
- [x] add support for `links`
- [ ] add support for `actions`
- [ ] add support for sub-entities as embedded links
- [ ] add support for sub-entities as embedded representations
- [ ] add 404 equivalent for when trying to invoke a relationship that doesn't exist
- [ ] add support for Siren
- [ ] add tests for authenticated requests
Expand Down
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@
],
"license": "Apache-2.0",
"keywords": [
"rest",
"rest-client",
"isomorphic",
"fetch",
"rest",
"rest-client",
"client",
"hateoas",
"hateoas-client",
"client",
"hypermedia",
"hypermedia-client",
"RFC8288",
"link-header",
"link-template-header",
"hal"
"hal",
"hal-client",
"siren",
"siren-client"
],
"main": "dist/waychaser.js",
"unpkg": "dist/waychaser.umd.min.js",
Expand Down
1 change: 1 addition & 0 deletions src/test/operations.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ Then('the body without the links will contain', async function (
const expectedBody = JSON.parse(documentString)
await checkBody.bind(this)(expectedBody, actualBody => {
delete actualBody._links
delete actualBody.links
return actualBody
})
})
Expand Down
125 changes: 86 additions & 39 deletions src/test/resource.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
animals
} from 'unique-names-generator'
import logger from '../util/logger'
import MediaTypes from '../util/media-types'

let pathCount = 0

Expand Down Expand Up @@ -36,6 +37,7 @@ function sendResponse (
curies
) {
let halLinks
let sirenLinks
switch (mediaType) {
case 'application/json':
if (links) {
Expand All @@ -45,7 +47,7 @@ function sendResponse (
response.header('link-template', linkTemplates.toString())
}
break
case 'application/hal+json':
case MediaTypes.HAL:
halLinks = {}
if (links) {
links.refs.forEach(link => {
Expand All @@ -67,11 +69,25 @@ function sendResponse (
})
}
break
case MediaTypes.SIREN:
/*
"links": [
{ "rel": [ "self" ], "href": "http://api.x.io/orders/42" },
{ "rel": [ "previous" ], "href": "http://api.x.io/orders/41" },
{ "rel": [ "next" ], "href": "http://api.x.io/orders/43" }
]
*/
sirenLinks = []
links.refs.forEach(link => {
sirenLinks.push({ rel: [link.rel], href: link.uri })
})
break
}
response.header('content-type', mediaType)
response.status(status).send({
status,
...(mediaType === 'application/hal+json' && { _links: halLinks })
...(mediaType === MediaTypes.HAL && { _links: halLinks }),
...(mediaType === MediaTypes.SIREN && { links: sirenLinks })
})
}

Expand Down Expand Up @@ -256,16 +272,25 @@ Given(
'a HAL resource returning the following with a {string} link that returns itself',
async function (relationship, responseBody) {
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(
Object.assign(JSON.parse(responseBody), {
_links: { self: { href: to } }
})
)
})
const links = {
_links: { self: { href: this.currentResourceRoute } }
}
await createLinkingResource.bind(this)(responseBody, links, MediaTypes.HAL)
}
)

Given(
'a Siren resource returning the following with a {string} link that returns itself',
async function (relationship, responseBody) {
this.currentResourceRoute = randomApiPath()
const links = {
links: [{ rel: ['self'], href: this.currentResourceRoute }]
}
await createLinkingResource.bind(this)(
responseBody,
links,
MediaTypes.SIREN
)
}
)

Expand All @@ -290,7 +315,7 @@ Given(
const halResourcePath = randomApiPath()
const router = await this.router.route(halResourcePath)
await router.get(async (request, response) => {
response.header('content-type', 'application/hal+json')
response.header('content-type', MediaTypes.HAL)
response.status(200).send({
_links
})
Expand All @@ -316,17 +341,38 @@ Given(
'a HAL resource with a {string} operation that returns an error',
async function (relationship) {
this.currentResourceRoute = randomApiPath()
const to = `http://${API_ACCESS_HOST}:33556/api`
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({
_links: { [relationship]: { href: to } }
})
})
const links = {
_links: {
[relationship]: { href: `http://${API_ACCESS_HOST}:33556/api` }
}
}
createLinkingResource.bind(this)(undefined, links, MediaTypes.HAL)
}
)

Given(
'a Siren resource with a {string} operation that returns an error',
async function (relationship) {
this.currentResourceRoute = randomApiPath()
const links = {
links: [
{ rel: [relationship], href: `http://${API_ACCESS_HOST}:33556/api` }
]
}
createLinkingResource.bind(this)(undefined, links, MediaTypes.SIREN)
}
)

async function createLinkingResource (responseBody, links, mediaType) {
const router = await this.router.route(this.currentResourceRoute)
await router.get(async (request, response) => {
response.header('content-type', mediaType)
response
.status(200)
.send(Object.assign(JSON.parse(responseBody || '{}'), links))
})
}

async function createResourceToPrevious (relationship, mediaType, curies) {
const links = createLinks(relationship, this.currentResourceRoute)
this.currentResourceRoute = randomApiPath()
Expand All @@ -349,10 +395,14 @@ Given(
Given(
'a HAL resource with a {string} operation that returns that resource',
async function (relationship) {
await createResourceToPrevious.bind(this)(
relationship,
'application/hal+json'
)
await createResourceToPrevious.bind(this)(relationship, MediaTypes.HAL)
}
)

Given(
'a Siren resource with a {string} operation that returns that resource',
async function (relationship) {
await createResourceToPrevious.bind(this)(relationship, MediaTypes.SIREN)
}
)

Expand Down Expand Up @@ -385,11 +435,14 @@ Given(
Given(
'a list of {int} HAL resources linked with {string} operations',
async function (count, relationship) {
createListOfResources.bind(this)(
count,
relationship,
'application/hal+json'
)
createListOfResources.bind(this)(count, relationship, MediaTypes.HAL)
}
)

Given(
'a list of {int} Siren resources linked with {string} operations',
async function (count, relationship) {
createListOfResources.bind(this)(count, relationship, MediaTypes.SIREN)
}
)

Expand All @@ -412,7 +465,7 @@ Given(
'GET',
[{ NAME: parameter, TYPE: parameterType }],
undefined,
'application/hal+json'
MediaTypes.HAL
)
}
)
Expand Down Expand Up @@ -480,13 +533,7 @@ Given(
async function (relationship, method, dataTable) {
this.currentResourceRoute = await createRandomDynamicResourceRoute.bind(
this
)(
relationship,
method,
dataTable.hashes(),
undefined,
'application/hal+json'
)
)(relationship, method, dataTable.hashes(), undefined, MediaTypes.HAL)
}
)

Expand All @@ -504,7 +551,7 @@ Given(
async function (relationship, curies) {
await createResourceToPrevious.bind(this)(
relationship,
'application/hal+json',
MediaTypes.HAL,
curies.hashes()
)
}
Expand Down
45 changes: 45 additions & 0 deletions src/test/siren.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Feature: Invoke Siren Operation

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

Scenario: Invoke operation - self
Given a Siren resource returning the following with a "self" link that returns itself
"""
{
"properties": {
"status": 200
}
}
"""
When waychaser successfully loads that resource
And we successfully invoke the "self" operation
Then the same resource will be returned
And the body without the links will contain
"""
{
"properties": {
"status": 200
}
}
"""

Scenario: Invoke operation error
Given a Siren resource with a "error" operation that returns an error
When waychaser successfully loads that resource
And we invoke the "error" operation
Then it will NOT have loaded successfully

Scenario: Invoke operation - next
Given a resource returning status code 200
And a Siren resource with a "next" operation that returns that resource
When waychaser successfully loads the latter resource
And we successfully invoke the "next" operation
Then the former resource will be returned

Scenario: Invoke operation - list
Given a list of 4 Siren resources linked with "next" operations
When waychaser successfully loads the first resource in the list
And invokes each of the "next" operations in turn 3 times
Then the last resource returned will be the last item in the list
6 changes: 6 additions & 0 deletions src/util/media-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const MediaTypes = {
HAL: 'application/hal+json',
SIREN: 'application/vnd.siren+json'
}

export default MediaTypes
Loading

0 comments on commit 70a5c79

Please sign in to comment.