Security News
PyPI Introduces Digital Attestations to Strengthen Python Package Security
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
A simple approach to generating HAL-esque hypermedia responses based on resource definitions. Built to work best with autohost but should work with most Node.JS HTTP stacks with a little extra effort.
This is alphaware!
You can skip to the Using section but understanding the concepts behind should explain
The resource definition provides the metadata necessary for hyped to generate hypermedia based on the model you provide it. This declaritive approach takes advantage of the structure and property names you provide to make implicit associations. The trade-off is that the associations are made based on consistent naming across several properties and values and typos will break the expected outcome.
// resource
{
[resourceName]: the resource name (must be unique)
parent: (optional) the name of the resource that this "belongs" to
urlPrefix: (optional) a URL prefix for all actions in this resource
actions: {
[actionName]: {
method: the http method
url: the URL for this action
render: override the resource/action that gets rendered from this action
include: property names array to include in the rendered response
exclude: property names array to exclude from rendered response
filter: predicate to determine if a property's key/value should be included
condition: returns true if the action is valid for the given model
transform: a map function that operates against the data
embed: defines which model properties are embedded resources and how to render them
links: provides alternate/compatible urls for activating this action
parameters: provide metadata to describe available query parameters for this action
}
},
versions: {
#: {
[actionName]: {
// provide any of the above properties to change how this
// version will differ from the previous version
// these changes are applied cumulatively in descending order
}
}
}
}
// render
// use case: when returning child items from a parent resource's action
{
resource: the resource that is actually being returned from this action
action: the action that should be used to render the resource
}
// embed
// use case: when returning a parent object that contains properties that should
// be rendered/treated like nested resources
{
[propertyName]: {
resource: name of the embedded resource
render: which action will be used to render the resource (usually "self")
actions: an array of the actions that should be included
}
}
// links
// use case: when an action has a limited set of parameter values that should be
// given a friendly name
{
[actionName]: url|generator
}
// parameters
// use case: when you want to define the available parameters for an action
// for use by the client
{
parameterName: {
[ range|choice|list|validate|invalidate ]: specification
}
}
This feature is to allow action aliases for known sets of query parameters.
</dd>
<dt>url</dt>
<dd>Works the same as the `url` for the action.</dd>
<dt>generator</dt>
<dd>A function that takes the data model and a context hash and optionally returns a url string. Returning an empty string or undefined will exclude the link from rendered results. See <a href="#rendering-api">Rendering API</a> for how the context is specified.
<div class="highlight highlight-javascript"> <pre>
"next-page": function( data, context ) {
// results in a url like "/thing?page=1&size=10"
return "/thing?page=" + ( context.page + 1 ) + "&size=" + context.size;
}
</pre>
</div>
</dd>
<dt><h3>parameters</h3></dt>
<dd>A hash of parameter names and metadata about each parameter. Each parameter should specify what "type" it is along with a specification that the client can use to validate possible parameter values. Any of the specifications can be a generator function to allow for dynamic specification.</dd>
<dt>choice</dt>
<dd>An array of valid choices where the consumer is expected to provide a single value.</dd>
<dt>multi</dt>
<dd>An array of valid choices where the consumer can provide multiple values.</dd>
<dt>range</dt>
<dd>A two-element array consisting of a lower and upper bound for the parameter value.</dd>
<dt>validate</dt>
<dd>A regular expression used to determine if a parameter value is valid.</dd>
<dt>invalidate</dt>
<dd>A regular expression used to detect invalid parameter values.</dd>
<dt>required</dt>
<dd>A boolean indicating that this parameter is required.</dd>
<dt><h3>render</h3></dt>
<dd>Use this when the resource and action that should get rendered differs from the
resource and action currently being rendered.
</dd>
<dt>resource</dt>
<dd>The name of the resource that is being rendered.</dd>
<dt>action</dt>
<dd>The name of the action to use in rendering the resource.</dd>
// account resource
{
name: "account",
actions: {
self: {
method: "get",
url: "/account/:id",
include: [ "id", "balance" ],
embed: {
transactions: {
resource: "transaction",
render: "self",
actions: [ "self", "detail" ]
}
}
},
withdraw: {
method: "POST",
url: "/account/:id/withdrawal",
condition: function( account ) {
return account.balance > 0;
},
include: [ "id", "accountId", "amount", "balance", "newBalance" ]
},
deposit: {
method: "POST",
url: "/account/:id/deposit",
include: [ "id", "accountId", "amount", "balance", "newBalance" ]
}
}
}
//transaction resource
{
name: "transaction",
parent: "account",
actions: {
self: {
method: "get",
url: "/transaction/:transaction.id",
include: [ "id", "amount", "date" ],
links: {
"details": "/transaction/:transaction.id?detail=true"
}
}
}
}
hyped generates an abstract hypermedia model based off a resource definition and data model. This hypermedia model is then passed to a "rendering engine" that produces a HTTP response body based on the requested media type.
Understanding the structure of the hypermedia model is important if you'd like to define your own rendering function for custom media types.
HyperModel Structure
{
// top level properties starting with a '_' are metadata
// all other properties are attached based on the resource definition
_origin: { href: "", method: "" },
_links: {
self: { href: "/thing/100", method: "get" },
child: { href: "/thing/100/child/:childId", method: "get", templated: true }
},
_embedded: {
children: [
// each embedded resource will follow the same pattern/format
]
}
}
<dt>_links</dt>
<dd>The actions available for the given resource.</dd>
<dt>_embedded</dt>
<dd>If a key in the <tt>embed</tt> section matches one on the data model provided, the resources contained will show up under the property matching the one on the data model. Each embedded resource will contain <tt>_origin</tt> and <tt>_links</tt> and may also include its own <tt>_embedded</tt> section.</dd>
Rendering engines are simply functions with the signature function( hyperModel )
. These functions take the hyperModel and produce the correct response body for their given media type. The built in engine for "application/hal+json" looks like this:
function render( model ) {
return JSON.stringify( model );
}
Note: the engine for "application/json" is more complex since it has to effectively reduce and filter a more complex data structure to produce simple JSON.
The accept header is used to select an appropriate rendering engine (if one is available). If no engine matches the provided media type, hyped will respond to the client request with a 415 explaining that the requested media type is not supported.
New versions are implemented as diffs that get applied, in order, against the baseline. Each version provides new values for properties that replace parts of the metadata for the resource. This will hopefully make it easy to see the differences between versions of a resource and reduce the amount of copy/pasted code between versions of a resource definition.
hyped will attempt to replace path variables specified in two separate styles for two separate use cases:
Express style variables
:property
:property.childProperty
Brace style variables
{property}
{property.childProperty}
Either style is valid when specifying the URL in the action, hyped will make sure that the correct form is used (Express style gets used server side for assigning routes while brace style is returned in all client responses).
The first form will be used to attempt to read a property directly on the model. The second will attempt to read a nested property or a property name that combines the two in camel-case fahsion (i.e. propertyChildProperty
). In either case, if no match is found, the variable will be left in tact. In the second case, the period is removed and the variable becomes camel-case.
Example:
{
name: "user",
actions: {
self: {
method: "get",
url: "/user/:user.name"
}
}
}
If the user model in question had a name property of "leroyJenkins"
, the response body would look like this:
{
"name": "leroyJenkins",
"_links": {
"self": { "href": "/user/leroyJenkins", "method": "GET" }
}
}
When a URL contains path variables that could not be replaced by a value in the model, the action/origin link will indicated this with a templated
property set to true
.
Note: the client will always see brace-style path variables
{
"name": "leroyJenkins",
"_links": {
"self": { "href": "/user/leroyJenkins", "method": "GET" },
"insult": { "href": "/user/leroyJenkins/{insult}", "method": "POST", "templated": true }
}
}
These examples show the bare minimum. You'll only get support for built in mediatypes - presently application/json
and application/hal+json
. This means if you don't provide your own engine for custom media types and a client sends an accept header for a media type hyped knows about, it will send back a 415 (unsupported media type) to the client rather than throwing an exception.
This example will add an express middleware to autohost
that extends the envelope with a fluent set of calls that can be used to construct and render a hypermedia response. The correct version, rendering engine, resource and action are all determined during the hyped middleware you add to autohost so that when rendering a response, the only things you must provide is the model.
Note: at this time autohost 0.3.0-3 or greater is required
index.js
var autohost = require( "autohost" );
var hyped = require( "hyped" )();
autohost
.init( {
noOptions: true, // turn off autohost's OPTIONS middleware
urlStrategy: hyped.urlStrategy // use hyped's URL strategy
} )
.then( hyped.addResources );
hyped.setupMiddleware( autohost );
resource.js
var databass = require( "myDAL" );
module.exports = {
name: "something",
actions: {
self: {
method: "get",
url: "/something/:id",
exclude: [],
handle: function( envelope ) {
var model = databass.getSomethingById( id );
envelope.hyped( model ).status( 200 ).render();
}
}
},
versions: {
2: {
exlude: [ "weirdFieldWeShouldNotExpose" ]
}
}
};
express
// assumes you have a hash containing all your resource definitions and express set up
var hyped = require( "hyped" )( resources );
hyped.setupMiddleware( app );
app.get( "something/:id", function( req, res ) {
var id = req.param( "id" );
var model = databass.getSomethingById( id );
req.hyped( model ).status( 200 ).render();
} );
If you are using this library with Autohost, the only API you really need to know about is used for the initial setup and the response generation.
You can skip passing resource list at all and provide the two booleans in the order specified.
defaultToNewest: causes hyped to default to the newest available version when one isn't specified includeChildrenInOptions: include child resource actions in the OPTIONS response
Adds the metadata for a particular resource.
Adds multiple resources at once. Intended to make autohost setup simple.
autohost.init( { noOptions: true } )
.then( hyped.addResources );
Registers a rendering function with one or more mediaTypes.
// this is a ridiculous example. don't do it.
hyped.registerEngine( "text/plain", function( model ) {
return model.toString();
} );
This call can take either a reference to the autohost lib or express's app instance. It will register both the hypermediaMiddleware
and the optionsMiddleware
with the HTTP library in use.
Refer to the usage examples above to see this call in use.
Changes hyped's default versioning detection approach. This is experimental and not recommended. The versionFn
is a function that takes the request object and returns a version number.
// this isn't great, but if you have to ...
hyped.versionWith( req ) {
return req.headers[ "x-version" ] || 1;
}
The hypermedia middleware extends autohost's envelope and the underlying request object with a set of fluent calls to help with the construction of a hypermedia response.
Keep in mind that normally, you will only use the hyped
, status
and render
calls. The middleware should correctly detect the version, mediatype, resource and action to be rendered.
// within an autohost handler
envelope.hyped( myData ).status( 200 ).render();
// within an express route
req.hyped( myData ).status( 200 ).render();
You provide the data model that the resource will render a response based on. The resources are designed to work with models that may have a great deal more information than should ever be exposed to the client.
Another way to provide context to any link generators for this action.
If omitted, this is always 200. Be good to your API's consumers and use proper status codes.
Returns the response to the client.
I think HAL is pretty awesome. We"ve added a few minor extensions and may continue. This is where we"ll describe them.
_versions
and _mediatypes
properties have been added to the payload returned from the OPTIONS call to the API root. They will list the available versions and mediatypes currently supported by the server.
OPTIONS response structure
{
"_links": {
"resource:action": { href: "", method: "", templated: true }
}
}
All actions will be returned under the top-level _links
property. The action names returned from OPTIONS will be namespaced by the resource name delimited by a colon.
Provides a data structure identical to the link that would be called to produce the response. This is especially useful within embedded resources as it allows the client to see what link action was used to produce the embedded resource included in the payload.
We added this to make it easier for clients to know which representation they have in memory for the sake of both local caching and requesting updated versions. We recommend including etag or last-modified data along with resources to further improve efficiency.
HAL does not include the HTTP method used for an link"s href
. Our version does include this information in the link using the method
property. As in our URL example from above:
{
"name": "leroyJenkins",
"_links": {
"self": { "href": "/user/leroyJenkins", "method": "GET" }
}
}
While the links
property allows you to provide actions with the parameters already attached to the URL, there will be times when defining every possible combination of parameters as a specific action won't make sense.
In those cases, we believe hypermedia should include metadata about the available parameters. This at least reduces the amount of implementation details exposed to API consumers.
{
"_links": {
"self": {
"href": "/thing/100",
"method": "GET",
"parameters": {
"arg1": {
"range": [ 0, 100 ]
},
"arg2": {
"choice": [ 4, 8, 15, 16, 23, 42 ]
},
"arg3": {
"multi": [ "a", "b", "c", "d" ]
},
"arg4": {
"validate": "/^starts with.*/",
"invalidate": "/.*ends with$"
}
}
}
}
}
The tests are a bit of a contrived mess at the moment but do exercise the features and known use cases. If submitting a PR, please do the following:
None of this is guaranteed but here are some items that would be nice/great to have.
FAQs
Hypermedia response generation engine
The npm package hyped receives a total of 6 weekly downloads. As such, hyped popularity was classified as not popular.
We found that hyped demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
PyPI now supports digital attestations, enhancing security and trust by allowing package maintainers to verify the authenticity of Python packages.
Security News
GitHub removed 27 malicious pull requests attempting to inject harmful code across multiple open source repositories, in another round of low-effort attacks.
Security News
RubyGems.org has added a new "maintainer" role that allows for publishing new versions of gems. This new permission type is aimed at improving security for gem owners and the service overall.