Skip to content

Commit

Permalink
Making hypermedia() smartr by knowing when a key is an item or a re…
Browse files Browse the repository at this point in the history
…lated URI, & to not remove `link` keys for obvious data modeling issues!
  • Loading branch information
avoidwork committed Sep 6, 2014
1 parent 14c8ab2 commit 7959bd5
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 57 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Change Log

## 0.9.0
- Made `hypermedia()` smartr by knowing when a key is an item or a related URI, & to not remove `link` keys for obvious data modeling issues!

## 0.8.3
- Fixed `hypermedia()` when dealing with collections: `Array` of `Objects` e.g. a record set, added a test

Expand Down
2 changes: 1 addition & 1 deletion doc/global.html
Original file line number Diff line number Diff line change
Expand Up @@ -1826,7 +1826,7 @@ <h5>Returns:</h5>

<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.2.2</a>
on 2014-09-06T11:44:11-04:00 using the <a
on 2014-09-06T16:52:41-04:00 using the <a
href="https://github.com/terryweiss/docstrap">DocStrap template</a>.
</span>
</footer>
Expand Down
2 changes: 1 addition & 1 deletion doc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ <h2>License</h2>

<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.2.2</a>
on 2014-09-06T11:44:11-04:00 using the <a
on 2014-09-06T16:52:41-04:00 using the <a
href="https://github.com/terryweiss/docstrap">DocStrap template</a>.
</span>
</footer>
Expand Down
4 changes: 2 additions & 2 deletions doc/module-tenso.html
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ <h2>
<dt class="tag-version">Version:</dt>
<dd class="tag-version">
<ul class="dummy">
<li>0.8.3</li>
<li>0.9.0</li>
</ul>
</dd>

Expand Down Expand Up @@ -218,7 +218,7 @@ <h2>

<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.2.2</a>
on 2014-09-06T11:44:12-04:00 using the <a
on 2014-09-06T16:52:41-04:00 using the <a
href="https://github.com/terryweiss/docstrap">DocStrap template</a>.
</span>
</footer>
Expand Down
2 changes: 1 addition & 1 deletion doc/modules.list.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ <h2>

<span class="jsdoc-message">
Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 3.2.2</a>
on 2014-09-06T11:44:11-04:00 using the <a
on 2014-09-06T16:52:41-04:00 using the <a
href="https://github.com/terryweiss/docstrap">DocStrap template</a>.
</span>
</footer>
Expand Down
52 changes: 32 additions & 20 deletions lib/tenso.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
* @license BSD-3 <https://raw.github.com/avoidwork/tenso/master/LICENSE>
* @link http://avoidwork.github.io/tenso
* @module tenso
* @version 0.8.3
* @version 0.9.0
*/
( function () {
"use strict";

var turtleio = require( "turtle.io" ),
SERVER = "tenso/0.8.3",
SERVER = "tenso/0.9.0",
CONFIG = require( __dirname + "/../config.json" ),
keigai = require( "keigai" ),
util = keigai.util,
Expand All @@ -38,7 +38,7 @@ var turtleio = require( "turtle.io" ),
SAMLStrategy = require( "passport-saml" ).Strategy,
TwitterStrategy = require( "passport-twitter" ).Strategy,
RedisStore = require( "connect-redis" )( session ),
REGEX_HYPERMEDIA = /_(guid|uuid|id|url|uri)$/,
REGEX_HYPERMEDIA = /[a-zA-Z]+_(guid|uuid|id|url|uri)$/,
REGEX_TRAILING = /_.*$/,
REGEX_TRAILING_S = /s$/,
REGEX_SCHEME = /^(\w+\:\/\/)|\//,
Expand All @@ -48,7 +48,9 @@ var turtleio = require( "turtle.io" ),
REGEX_BODY = /POST|PUT|PATCH/i,
REGEX_FORMENC = /application\/x-www-form-urlencoded/,
REGEX_JSONENC = /application\/json/,
REGEX_BODY_SPLIT = /&|=/;
REGEX_BODY_SPLIT = /&|=/,
REGEX_LEADING = /.*\//,
REGEX_ID = /^(_id|id)$/i;

/**
* Tenso
Expand All @@ -61,7 +63,7 @@ function Tenso () {
this.rates = {};
this.server = turtleio();
this.server.tenso = this;
this.version = "0.8.3";
this.version = "0.9.0";
}

/**
Expand Down Expand Up @@ -762,12 +764,12 @@ function factory ( arg ) {
* @return {Undefined} undefined
*/
function hypermedia ( server, req, rep, headers ) {
var query, page, page_size, nth, root, remove, rewrite;
var seen = {},
query, page, page_size, nth, root, remove, rewrite, parent;

// Parsing the object for hypermedia properties
function parse ( obj, rel ) {
rel = rel || "related";

function parse ( obj, rel, item_collection ) {
rel = rel || "related";
var keys = array.keys( obj );

if ( keys.length === 0 ) {
Expand All @@ -778,20 +780,24 @@ function hypermedia ( server, req, rep, headers ) {
var collection, uri;

// If ID like keys are found, and are not URIs, they are assumed to be root collections
if ( REGEX_HYPERMEDIA.test( i ) ) {
collection = i.replace( REGEX_TRAILING, "" ).replace( REGEX_TRAILING_S, "" ) + "s";
if ( REGEX_ID.test( i ) || REGEX_HYPERMEDIA.test( i ) ) {
if ( !REGEX_ID.test( i ) ) {
collection = i.replace( REGEX_TRAILING, "" ).replace( REGEX_TRAILING_S, "" ) + "s";
rel = "related";
}
else {
collection = item_collection;
rel = "item";
}

uri = REGEX_SCHEME.test( obj[i] ) ? ( obj[i].indexOf( "//" ) > -1 ? obj[i] : req.parsed.protocol + "//" + req.parsed.host + obj[i] ) : ( req.parsed.protocol + "//" + req.parsed.host + "/" + collection + "/" + obj[i] );

if ( uri !== root ) {
if ( uri !== root && !seen[uri] ) {
rep.data.link.push( {uri: uri, rel: rel} );
delete obj[i];
seen[uri] = 1;
}
}
} );

if ( array.keys( obj ).length === 0 ) {
obj = null;
}
}

return obj;
Expand Down Expand Up @@ -862,7 +868,7 @@ function hypermedia ( server, req, rep, headers ) {
}

if ( i instanceof Object ) {
parse( i, "item" );
parse( i, "item", req.parsed.pathname.replace( REGEX_TRAILING, "" ).replace( REGEX_LEADING, "" ).replace( REGEX_TRAILING_S, "" ) + "s" );
}
} );

Expand All @@ -878,11 +884,17 @@ function hypermedia ( server, req, rep, headers ) {
}
}
else if ( rep.data.result instanceof Object ) {
rep.data.result = parse( rep.data.result );
parent = req.parsed.pathname.split( "/" ).filter( function( i ){ return i !== ""; } );

if ( parent.length > 1 ) {
parent.pop();
}

rep.data.result = parse( rep.data.result, undefined, array.last( parent ) );
}

if ( rep.data.link !== undefined && rep.data.link.length > 0 ) {
headers.link = rep.data.link.map( function ( i ) {
headers.link = array.keySort( rep.data.link, "rel, uri" ).map( function ( i ) {
return "<" + i.uri + ">; rel=\"" + i.rel + "\"";
} ).join( ", " );
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tenso",
"description": "Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs.",
"version": "0.8.3",
"version": "0.9.0",
"homepage": "http://avoidwork.github.io/tenso",
"author": {
"name": "Jason Mulligan",
Expand Down
40 changes: 25 additions & 15 deletions src/hypermedia.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
* @return {Undefined} undefined
*/
function hypermedia ( server, req, rep, headers ) {
var query, page, page_size, nth, root, remove, rewrite;
var seen = {},
query, page, page_size, nth, root, remove, rewrite, parent;

// Parsing the object for hypermedia properties
function parse ( obj, rel ) {
rel = rel || "related";

function parse ( obj, rel, item_collection ) {
rel = rel || "related";
var keys = array.keys( obj );

if ( keys.length === 0 ) {
Expand All @@ -30,20 +30,24 @@ function hypermedia ( server, req, rep, headers ) {
var collection, uri;

// If ID like keys are found, and are not URIs, they are assumed to be root collections
if ( REGEX_HYPERMEDIA.test( i ) ) {
collection = i.replace( REGEX_TRAILING, "" ).replace( REGEX_TRAILING_S, "" ) + "s";
if ( REGEX_ID.test( i ) || REGEX_HYPERMEDIA.test( i ) ) {
if ( !REGEX_ID.test( i ) ) {
collection = i.replace( REGEX_TRAILING, "" ).replace( REGEX_TRAILING_S, "" ) + "s";
rel = "related";
}
else {
collection = item_collection;
rel = "item";
}

uri = REGEX_SCHEME.test( obj[i] ) ? ( obj[i].indexOf( "//" ) > -1 ? obj[i] : req.parsed.protocol + "//" + req.parsed.host + obj[i] ) : ( req.parsed.protocol + "//" + req.parsed.host + "/" + collection + "/" + obj[i] );

if ( uri !== root ) {
if ( uri !== root && !seen[uri] ) {
rep.data.link.push( {uri: uri, rel: rel} );
delete obj[i];
seen[uri] = 1;
}
}
} );

if ( array.keys( obj ).length === 0 ) {
obj = null;
}
}

return obj;
Expand Down Expand Up @@ -114,7 +118,7 @@ function hypermedia ( server, req, rep, headers ) {
}

if ( i instanceof Object ) {
parse( i, "item" );
parse( i, "item", req.parsed.pathname.replace( REGEX_TRAILING, "" ).replace( REGEX_LEADING, "" ).replace( REGEX_TRAILING_S, "" ) + "s" );
}
} );

Expand All @@ -130,11 +134,17 @@ function hypermedia ( server, req, rep, headers ) {
}
}
else if ( rep.data.result instanceof Object ) {
rep.data.result = parse( rep.data.result );
parent = req.parsed.pathname.split( "/" ).filter( function( i ){ return i !== ""; } );

if ( parent.length > 1 ) {
parent.pop();
}

rep.data.result = parse( rep.data.result, undefined, array.last( parent ) );
}

if ( rep.data.link !== undefined && rep.data.link.length > 0 ) {
headers.link = rep.data.link.map( function ( i ) {
headers.link = array.keySort( rep.data.link, "rel, uri" ).map( function ( i ) {
return "<" + i.uri + ">; rel=\"" + i.rel + "\"";
} ).join( ", " );
}
Expand Down
6 changes: 4 additions & 2 deletions src/intro.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var turtleio = require( "turtle.io" ),
SAMLStrategy = require( "passport-saml" ).Strategy,
TwitterStrategy = require( "passport-twitter" ).Strategy,
RedisStore = require( "connect-redis" )( session ),
REGEX_HYPERMEDIA = /_(guid|uuid|id|url|uri)$/,
REGEX_HYPERMEDIA = /[a-zA-Z]+_(guid|uuid|id|url|uri)$/,
REGEX_TRAILING = /_.*$/,
REGEX_TRAILING_S = /s$/,
REGEX_SCHEME = /^(\w+\:\/\/)|\//,
Expand All @@ -38,4 +38,6 @@ var turtleio = require( "turtle.io" ),
REGEX_BODY = /POST|PUT|PATCH/i,
REGEX_FORMENC = /application\/x-www-form-urlencoded/,
REGEX_JSONENC = /application\/json/,
REGEX_BODY_SPLIT = /&|=/;
REGEX_BODY_SPLIT = /&|=/,
REGEX_LEADING = /.*\//,
REGEX_ID = /^(_id|id)$/i;
6 changes: 3 additions & 3 deletions test/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ module.exports.get = {
"/": ["/items", "/things"],
"/empty": [],
"/items": [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],
"/things": [{thing_id: 1, name: "thing 1"}, {thing_id: 2, name: "thing 2"}, {thing_id: 3, name: "thing 3"}],
"/things": [{id: 1, name: "thing 1", user_id: 1}, {id: 2, name: "thing 2", user_id: 1}, {id: 3, name: "thing 3", user_id: 2}],
"/uuid": function (req, res) {
this.respond( req, res, uuid() );
},
"/somethings/abc": {"something_id": "abc", "user_id": 123, "title": "This is a title", "body": "Where is my body?", "source_url": "http://source.tld"},
"/somethings/def": {"user_id": 123, "source_url": "http://source.tld"}
"/somethings/abc": {"_id": "abc", "user_id": 123, "title": "This is a title", "body": "Where is my body?", "source_url": "http://source.tld"},
"/somethings/def": {"_id": "def", "user_id": 123, "source_url": "http://source.tld"}
}

module.exports.post = {
Expand Down
23 changes: 12 additions & 11 deletions test/tenso_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ describe("Pagination", function () {
api( port )
.get("/items")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port, rel: "collection" }, { uri: "http://localhost:" + port + "/items?page=2&page_size=5", rel: "next" }, { uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last" }])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last"}, {uri: "http://localhost:" + port + "/items?page=2&page_size=5", rel: "next"}])
.expectValue("data.result", [1,2,3,4,5])
.expectValue("error", null)
.expectValue("status", 200)
Expand All @@ -155,7 +155,7 @@ describe("Pagination", function () {
api( port )
.get("/items?page=2&page_size=5")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port, rel: "collection" }, { uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first" }, { uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last" }])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first"}, {uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last"}])
.expectValue("data.result", [6,7,8,9,10])
.expectValue("error", null)
.expectValue("status", 200)
Expand All @@ -171,7 +171,7 @@ describe("Pagination", function () {
api( port )
.get("/items?page=3&page_size=5")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port, rel: "collection" }, { uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first" }, { uri: "http://localhost:" + port + "/items?page=2&page_size=5", rel: "prev" }])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first"}, {uri: "http://localhost:" + port + "/items?page=2&page_size=5", rel: "prev"}])
.expectValue("data.result", [11,12,13,14,15])
.expectValue("error", null)
.expectValue("status", 200)
Expand All @@ -187,7 +187,7 @@ describe("Pagination", function () {
api( port )
.get("/items?page=4&page_size=5")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port, rel: "collection" }, { uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first" }, { uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last" }])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/items?page=1&page_size=5", rel: "first"}, {uri: "http://localhost:" + port + "/items?page=3&page_size=5", rel: "last"}])
.expectValue("data.result", [])
.expectValue("error", null)
.expectValue("status", 200)
Expand All @@ -203,7 +203,7 @@ describe("Pagination", function () {
api( port )
.get("/[email protected]")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port, rel: "collection" }, { uri: "http://localhost:" + port + "/items?email=user%40domain.com&page=2&page_size=5", rel: "next" }, { uri: "http://localhost:" + port + "/items?email=user%40domain.com&page=3&page_size=5", rel: "last" }])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/items?email=user%40domain.com&page=3&page_size=5", rel: "last"}, {uri: "http://localhost:" + port + "/items?email=user%40domain.com&page=2&page_size=5", rel: "next"}])
.expectValue("data.result", [1,2,3,4,5])
.expectValue("error", null)
.expectValue("status", 200)
Expand All @@ -225,8 +225,9 @@ describe("Hypermedia", function () {
api( port )
.get("/things")
.expectStatus(200)
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/things/1", rel: "item"}, {uri: "http://localhost:" + port + "/things/2", rel: "item"}, {uri: "http://localhost:" + port + "/things/3", rel: "item"}])
.expectValue("data.result", [{name:"thing 1"},{name:"thing 2"},{name:"thing 3"}])
.expectValue("data.link", [{uri: "http://localhost:" + port, rel: "collection"}, {uri: "http://localhost:" + port + "/things/1", rel: "item"}, {uri: "http://localhost:" + port + "/things/2", rel: "item"}, {uri: "http://localhost:" + port + "/things/3", rel: "item"}, {uri:"http://localhost:" + port + "/users/1", rel: "related"},
{uri:"http://localhost:" + port + "/users/2", rel: "related"}])
.expectValue("data.result", [{id:1, name:"thing 1", user_id: 1},{id:2, name:"thing 2", user_id: 1},{id:3, name:"thing 3", user_id: 2}])
.expectValue("error", null)
.expectValue("status", 200)
.end(function(err) {
Expand All @@ -241,8 +242,8 @@ describe("Hypermedia", function () {
api( port )
.get("/somethings/abc")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port + "/somethings", rel: "collection" }, { uri: "http://localhost:" + port + "/users/123", rel: "related" }, { uri: "http://source.tld", rel: "related" }])
.expectValue("data.result", {"something_id": "abc", "title": "This is a title", "body": "Where is my body?"})
.expectValue("data.link", [{uri: "http://localhost:" + port + "/somethings", rel: "collection"}, {uri: "http://localhost:" + port + "/users/123", rel: "related"}, {uri: "http://source.tld", rel: "related"}])
.expectValue("data.result", {_id:"abc", user_id: 123, title: "This is a title", body: "Where is my body?", source_url: "http://source.tld"})
.expectValue("error", null)
.expectValue("status", 200)
.end(function(err) {
Expand All @@ -257,8 +258,8 @@ describe("Hypermedia", function () {
api( port )
.get("/somethings/def")
.expectStatus(200)
.expectValue("data.link", [{ uri: "http://localhost:" + port + "/somethings", rel: "collection" }, { uri: "http://localhost:" + port + "/users/123", rel: "related" }, { uri: "http://source.tld", rel: "related" }])
.expectValue("data.result", null)
.expectValue("data.link", [{uri: "http://localhost:" + port + "/somethings", rel: "collection"}, {uri: "http://localhost:" + port + "/users/123", rel: "related"}, {uri: "http://source.tld", rel: "related"}])
.expectValue("data.result", {_id: "def", user_id: 123, source_url: "http://source.tld"})
.expectValue("error", null)
.expectValue("status", 200)
.end(function(err) {
Expand Down

0 comments on commit 7959bd5

Please sign in to comment.