Add path resolution to get helper

refs #5993

- deps: jsonpath@0.2.0
- adds `resolvePaths` method
- supports handlebars style arrays with `.[]`
- supports shorthand post.tags and post.author for common usecases
- adds more tests & improves existing ones
This commit is contained in:
Hannah Wolfe 2015-10-22 11:12:21 +01:00
parent 64d9ce44cf
commit 994a20cf8b
3 changed files with 140 additions and 52 deletions

View File

@ -6,12 +6,20 @@ var _ = require('lodash'),
Promise = require('bluebird'),
errors = require('../errors'),
api = require('../api'),
jsonpath = require('jsonpath'),
resources,
pathAliases,
get;
// Endpoints that the helper is able to access
resources = ['posts', 'tags', 'users'];
// Short forms of paths which we should understand
pathAliases = {
'post.tags': 'post.tags[*].slug',
'post.author': 'post.author.slug'
};
/**
* ## Is Browse
* Is this a Browse request or a Read request?
@ -29,6 +37,34 @@ function isBrowse(context, options) {
return browse;
}
/**
* ## Resolve Paths
* Find and resolve path strings
*
* @param {Object} data
* @param {String} value
* @returns {String}
*/
function resolvePaths(data, value) {
var regex = /\{\{(.*?)\}\}/g;
value = value.replace(regex, function (match, path) {
var result;
// Handle aliases
path = pathAliases[path] ? pathAliases[path] : path;
// Handle Handlebars .[] style arrays
path = path.replace(/\.\[/g, '[');
// Do the query, and convert from array to string
result = jsonpath.query(data, path).join(',');
return result;
});
return value;
}
/**
* ## Parse Options
* Ensure options passed in make sense
@ -46,6 +82,10 @@ function parseOptions(data, options) {
options.author = options.author.slug;
}
if (_.isString(options.filter)) {
options.filter = resolvePaths(data, options.filter);
}
return options;
}

View File

@ -9,17 +9,20 @@ var should = require('should'),
// Stuff we are testing
handlebars = hbs.handlebars,
helpers = require('../../../server/helpers'),
api = require('../../../server/api');
api = require('../../../server/api'),
sandbox = sinon.sandbox.create();
describe('{{#get}} helper', function () {
var sandbox;
var fn, inverse;
before(function () {
utils.loadHelpers();
});
beforeEach(function () {
sandbox = sinon.sandbox.create();
fn = sandbox.spy();
inverse = sandbox.spy();
});
afterEach(function () {
@ -31,18 +34,19 @@ describe('{{#get}} helper', function () {
});
describe('posts', function () {
var testPostsArr = [
{id: 1, title: 'Test Post 1', author: 'cameron'},
{id: 2, title: 'Test Post 2', author: 'cameron', featured: true},
var browseStub, readStub, testPostsArr = [
{id: 1, title: 'Test Post 1', author: {slug: 'cameron'}},
{id: 2, title: 'Test Post 2', author: {slug: 'cameron'}, featured: true},
{id: 3, title: 'Test Post 3', tags: [{slug: 'test'}]},
{id: 4, title: 'Test Post 4'}
];
],
meta = {pagination: {}};
beforeEach(function () {
var browseStub = sandbox.stub(api.posts, 'browse'),
readStub = sandbox.stub(api.posts, 'read');
browseStub = sandbox.stub(api.posts, 'browse');
readStub = sandbox.stub(api.posts, 'read');
browseStub.returns(new Promise.resolve({posts: testPostsArr}));
browseStub.withArgs({limit: '3'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 3)}));
browseStub.withArgs({limit: '3'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 3), meta: meta}));
browseStub.withArgs({limit: '1'}).returns(new Promise.resolve({posts: testPostsArr.slice(0, 1)}));
browseStub.withArgs({tag: 'test'}).returns(new Promise.resolve({posts: testPostsArr.slice(2, 3)}));
browseStub.withArgs({tag: 'none'}).returns(new Promise.resolve({posts: []}));
@ -52,9 +56,6 @@ describe('{{#get}} helper', function () {
});
it('should handle default browse posts call', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -71,9 +72,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse posts call with limit 3', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -90,9 +88,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse posts call with limit 1', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -109,9 +104,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse posts call with limit 1', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -128,9 +120,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse post call with explicit tag', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -146,9 +135,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse post call with relative tag', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -164,9 +150,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse post call with explicit author', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -182,9 +165,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse post call with relative author', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -200,9 +180,6 @@ describe('{{#get}} helper', function () {
});
it('should handle browse post call with featured:true', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -218,9 +195,6 @@ describe('{{#get}} helper', function () {
});
it('should handle read post by id call', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -237,9 +211,6 @@ describe('{{#get}} helper', function () {
});
it('should handle empty result set', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -257,9 +228,6 @@ describe('{{#get}} helper', function () {
describe('general error handling', function () {
it('should return an error for an unknown resource', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'magic',
@ -276,9 +244,6 @@ describe('{{#get}} helper', function () {
});
it('should handle error from the API', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts',
@ -295,9 +260,6 @@ describe('{{#get}} helper', function () {
});
it('should show warning for call without any options', function (done) {
var fn = sinon.spy(),
inverse = sinon.spy();
helpers.get.call(
{},
'posts'
@ -309,4 +271,89 @@ describe('{{#get}} helper', function () {
}).catch(done);
});
});
describe('path resolution', function () {
var browseStub, readStub, data = {
post: {id: 3, title: 'Test 3', author: {slug: 'cameron'}, tags: [{slug: 'test'}, {slug: 'magic'}]}
};
beforeEach(function () {
browseStub = sandbox.stub(api.posts, 'browse').returns(new Promise.resolve());
readStub = sandbox.stub(api.posts, 'read').returns(new Promise.resolve());
});
it('should resolve post.tags alias', function (done) {
helpers.get.call(
data,
'posts',
{hash: {filter: 'tags:[{{post.tags}}]'}, fn: fn, inverse: inverse}
).then(function () {
browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1);
browseStub.firstCall.args[0].should.be.an.Object.with.property('filter');
browseStub.firstCall.args[0].filter.should.eql('tags:[test,magic]');
done();
}).catch(done);
});
it('should resolve post.author alias', function (done) {
helpers.get.call(
data,
'posts',
{hash: {filter: 'author:{{post.author}}'}, fn: fn, inverse: inverse}
).then(function () {
browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1);
browseStub.firstCall.args[0].should.be.an.Object.with.property('filter');
browseStub.firstCall.args[0].filter.should.eql('author:cameron');
done();
}).catch(done);
});
it('should resolve basic path', function (done) {
helpers.get.call(
data,
'posts',
{hash: {filter: 'id:-{{post.id}}'}, fn: fn, inverse: inverse}
).then(function () {
browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1);
browseStub.firstCall.args[0].should.be.an.Object.with.property('filter');
browseStub.firstCall.args[0].filter.should.eql('id:-3');
done();
}).catch(done);
});
it('should handle arrays the same as handlebars', function (done) {
var tpl = handlebars.compile('{{post.tags.[0].slug}}'),
output = tpl(data);
helpers.get.call(
data,
'posts',
{hash: {filter: 'tags:{{post.tags.[0].slug}}'}, fn: fn, inverse: inverse}
).then(function () {
browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1);
browseStub.firstCall.args[0].should.be.an.Object.with.property('filter');
browseStub.firstCall.args[0].filter.should.eql('tags:' + output);
done();
}).catch(done);
});
it('should output nothing if path does not resolve', function (done) {
helpers.get.call(
data,
'posts',
{hash: {filter: 'id:{{post.thing}}'}, fn: fn, inverse: inverse}
).then(function () {
browseStub.firstCall.args.should.be.an.Array.with.lengthOf(1);
browseStub.firstCall.args[0].should.be.an.Object.with.property('filter');
browseStub.firstCall.args[0].filter.should.eql('id:');
done();
}).catch(done);
});
});
});

View File

@ -46,6 +46,7 @@
"html-to-text": "1.3.2",
"intl": "1.0.0",
"intl-messageformat": "1.1.0",
"jsonpath": "0.2.0",
"knex": "0.7.3",
"lodash": "3.10.1",
"moment": "2.10.6",