From bdb24caa394c9b656125f12af6a9faa84b6016d3 Mon Sep 17 00:00:00 2001 From: Josh Rowley Date: Wed, 23 Jul 2014 12:34:35 -0400 Subject: [PATCH 1/7] begin implementation of contentful view helper - raise error if missing required config options - set up contentful client - configure content types from contentful api if `name` option is missing (default to underscored and pluralized output of content type name) --- lib/index.coffee | 111 ++++++++++++++++++++++++--------- package.json | 8 ++- readme.md | 6 +- test/fixtures/basic/app.coffee | 17 ++++- test/test.coffee | 2 +- 5 files changed, 106 insertions(+), 38 deletions(-) diff --git a/lib/index.coffee b/lib/index.coffee index 51ce43c..6b9bea2 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -1,42 +1,93 @@ -RootsUtil = require 'roots-util' -path = require 'path' +_ = require 'lodash' +W = require 'when' +S = require 'string' +contentful = require 'contentful' +pluralize = require 'pluralize' -# This is meant to serve as an example... +errors = + no_token: 'Missing required options for roots-contentful. Please ensure `access_token` and `space_id` are present.' + no_type_id: 'One or more of your content types is missing an `id` value' + sys_conflict: 'One of your content types has `sys` as a field. This is reserved for storing Contentful system metadata, please rename this field to a different value.' -# Full Roots Extension API documentation located: -# http://roots.readthedocs.org/en/latest/extensions.html +module.exports = (opts) -> + # throw error if missing required config + if not (opts.access_token && opts.space_id) + throw new Error errors.no_token -module.exports = -> - class RootsContentful + # setup contentful api client + client = contentful.createClient + accessToken: opts.access_token + space: opts.space_id - constructor: (@roots) -> - # console.log @roots + ###* + * Configures content types set in app.coffee. Sets default values if + * optional config options are missing. + * @param {Array} types - content_types set in app.coffee extension config + * @return {Promise} - returns an array of configured content types + ### - fs: -> - # category: 'foo' - # extract: true - # detect: (f) => - # path.extname(f.relative) == 'js' + configure_content = (types) -> + W.map types, (t) -> + if not t.id then throw new Error errors.no_type_id + t.filters ?= {} + if t.name then return W.resolve() + return client.contentType(t.id).then (res) -> + t.name = pluralize(S(res.name).toLowerCase().underscore().s) + return t - compile_hooks: -> - # category: 'foo' + ###* + * Fetches data from Contentful API, formats the raw data, and constructs + * the locals object + * @param {Array} types - configured content_type objects + * @return {Promise} - returns formatted locals object with all content + ### + + get_all_content = (types) -> + locals = {} + W.map(types, (t) -> + fetch_content(t) + .then(format_content) + .then (c) -> locals[t.name] = c + ).yield(locals) + + ###* + * Fetch entries for a single content type object + * @param {Object} type - content type object + * @return {Promise} - returns response from Contentful API + ### - # before_file: (ctx) => - # ctx.content = ctx.content.toUpperCase() + fetch_content = (type) -> + client.entries(_.merge(type.filters, content_type: type.id)) - # after_file: (ctx) => - # ctx.content = ctx.content.toUpperCase() + ###* + * Formats raw response from Contentful + * @param {Object} content - entries API response for a content type + * @return {Promise} - returns formatted content type entries object + ### - # before_pass: (ctx) => - # ctx.content = ctx.content.toUpperCase() + format_content = (content) -> + W.map(content, format_entry) - # after_pass: (ctx) => - # ctx.content = ctx.content.toUpperCase() + ###* + * Formats a single entry object from Contentful API response + * @param {Object} e - single entry object from API response + * @return {Promise} - returns formatted entry object + ### - # write: -> - # false + format_entry = (e) -> + if _.has(e.fields, 'sys') then throw new Error errors.sys_conflict + _.assign(_.omit(e, 'fields'), e.fields) - category_hooks: -> - # after: (ctx) => - # output = path.join(ctx.roots.config.output_path(), 'build.js') - # nodefn.call(fs.writeFile, output, @contents) + # load content + promise = configure_content(opts.content_types).then(get_all_content) + + class RootsContentful + constructor: (@roots) -> + @roots.config.locals ||= {} + + compile_hooks: -> + before_pass: (ctx) => + # once content is loaded, pass contentful data into locals + promise.then (locals) => + return if @roots.config.locals.contentful + @roots.config.locals.contentful = locals diff --git a/package.json b/package.json index cdd8eb3..4287ef1 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,13 @@ "test": "test" }, "dependencies": { - "lodash": "2.x", + "lodash": "~2.4.1", "minimatch": "0.2.x", - "roots-util": "0.0.4" + "roots-util": "0.0.4", + "contentful": "~0.1.2", + "when": "~3.4.2", + "string": "~1.9.0", + "pluralize": "0.0.10" }, "devDependencies": { "coffee-script": "1.7.x", diff --git a/readme.md b/readme.md index e19f006..bbefd8a 100644 --- a/readme.md +++ b/readme.md @@ -34,7 +34,7 @@ module.exports = id: 'xxxxxx', name: 'posts', template: 'views/_post.jade', - filter: { + filters: { 'fields.environment[in]': ['staging', 'production'] }, path: (e) -> "blogging/#{e.category}/#{slugify(e.title)}" @@ -125,13 +125,13 @@ Required. The Content Type's ID on Contentful. #### name -Optional. This is the name of the key the entries will be attached to on the `contentful` object in your views. Defaults to a [pluralized](https://github.com/blakeembrey/pluralize), [camelized](http://stringjs.com/#methods/camelize) representation of the Content Type name (e.g. 'Blog Post' => `contentful.blogPosts`) +Optional. This is the name of the key the entries will be attached to on the `contentful` object in your views. Defaults to a [pluralized](https://github.com/blakeembrey/pluralize), [underscored](http://stringjs.com/#methods/underscore) representation of the Content Type name (e.g. 'Blog Post' => `contentful.blogPosts`) #### template Optional. Path relative to the roots project of a template for a single entry view. Each entry in the Content Type will be passed into the template in an `entry` variable. If not given, the Content Type will not be compiled into single entry views and will only be attached to the `contentful` view helper object. -#### filter +#### filters Optional. Takes an object with different filter criteria, see examples of how to structure the object in [Contentful's docs](https://www.contentful.com/developers/documentation/content-delivery-api/javascript/#search-filter). diff --git a/test/fixtures/basic/app.coffee b/test/fixtures/basic/app.coffee index 68ca425..6eaeddf 100644 --- a/test/fixtures/basic/app.coffee +++ b/test/fixtures/basic/app.coffee @@ -1,5 +1,18 @@ -roots_contentful = require '../../..' +contentful = require '../../..' module.exports = ignores: ["**/_*", "**/.DS_Store"] - extensions: [new roots_contentful] + extensions: [ + contentful( + access_token: '4e68a90eac414b8e9ccfb504651dbbee8d338dac33ea89579424a2885948905d' + space_id: 'aqzq2qya2jm4' + content_types: [ + { + id: '6BYT1gNiIEyIw8Og8aQAO6' + } + { + id: '7CDlVsacqQc88cmIEGYWMa' + } + ] + ) + ] diff --git a/test/test.coffee b/test/test.coffee index f64987d..2239a05 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -22,7 +22,7 @@ after -> describe 'development', -> - before (done) -> compile_fixture.call(@, 'basic', done) + before (done) -> compile_fixture.call(@, 'basic', -> done()) it 'compiles basic project', -> p = path.join(@public, 'index.html') From 8f0132099d866eeaedf2819ffb98990738db1c57 Mon Sep 17 00:00:00 2001 From: Josh Rowley Date: Thu, 24 Jul 2014 17:03:54 -0400 Subject: [PATCH 2/7] test view helper, stub contentful --- package.json | 3 ++- test/fixtures/basic/app.coffee | 2 +- test/fixtures/basic/index.jade | 6 +++++- test/test.coffee | 31 +++++++++++++++++++++++++++++-- 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4287ef1..720a1bd 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "coffee-script": "1.7.x", "mocha": "*", "should": "*", - "roots": "3.x" + "roots": "3.x", + "sinon": "~1.10.3" }, "peerDependencies": { "roots": "3.x" diff --git a/test/fixtures/basic/app.coffee b/test/fixtures/basic/app.coffee index 6eaeddf..b303f3c 100644 --- a/test/fixtures/basic/app.coffee +++ b/test/fixtures/basic/app.coffee @@ -4,7 +4,7 @@ module.exports = ignores: ["**/_*", "**/.DS_Store"] extensions: [ contentful( - access_token: '4e68a90eac414b8e9ccfb504651dbbee8d338dac33ea89579424a2885948905d' + access_token: 'YOUR_ACCESS_TOKEN' space_id: 'aqzq2qya2jm4' content_types: [ { diff --git a/test/fixtures/basic/index.jade b/test/fixtures/basic/index.jade index 4bb415d..4769500 100644 --- a/test/fixtures/basic/index.jade +++ b/test/fixtures/basic/index.jade @@ -1 +1,5 @@ -p hello world +ul + - for p in contentful.blog_posts + li + h1= p.title + p= p.body diff --git a/test/test.coffee b/test/test.coffee index 2239a05..c648f8c 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,6 +1,8 @@ path = require 'path' fs = require 'fs' should = require 'should' +sinon = require 'sinon' +W = require 'when' Roots = require 'roots' _path = path.join(__dirname, 'fixtures') RootsUtil = require 'roots-util' @@ -12,6 +14,20 @@ compile_fixture = (fixture_name, done) -> @public = path.join(fixture_name, 'public') h.project.compile(Roots, fixture_name, done) +stub_contentful = (opts = {}) -> + contentful = require 'contentful' + sinon.stub(contentful, 'createClient').returns + contentType: -> W.resolve(opts.content_type || {name: 'Blog Post'}) + entries: -> W.resolve [ + opts.entry || { + sys: {'sys': 'data'}, + fields: { + title: 'Default Title' + body: 'Default Body' + } + } + ] + before (done) -> h.project.install_dependencies('*', done) @@ -21,9 +37,20 @@ after -> # tests describe 'development', -> - - before (done) -> compile_fixture.call(@, 'basic', -> done()) + before (done) -> + @title = 'Throw Some Ds' + @body = 'Rich Boy selling crack' + @stub = stub_contentful(entry: {fields: {title: @title, body: @body}}) + compile_fixture.call(@, 'basic', -> done()) it 'compiles basic project', -> p = path.join(@public, 'index.html') h.file.exists(p).should.be.ok + + it 'has contentful data available in views', -> + p = path.join(@public, 'index.html') + h.file.contains(p, @title).should.be.true + h.file.contains(p, @body).should.be.true + + after -> + @stub.restore() From fba418d55b1a5cb68606541f3527031222585743 Mon Sep 17 00:00:00 2001 From: Josh Rowley Date: Thu, 24 Jul 2014 18:25:59 -0400 Subject: [PATCH 3/7] set up coverall code coverage --- .gitignore | 1 + .travis.yml | 2 ++ Makefile | 3 +++ package.json | 8 ++++++-- readme.md | 2 +- 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 9daa824..032441d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .DS_Store node_modules +coverage diff --git a/.travis.yml b/.travis.yml index 6e5919d..8b95a9f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,5 @@ language: node_js node_js: - "0.10" +after_script: + - npm run coveralls diff --git a/Makefile b/Makefile index 3a90bd8..014757a 100644 --- a/Makefile +++ b/Makefile @@ -10,3 +10,6 @@ publish: make build npm publish . make unbuild + +coveralls: + NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/package.json b/package.json index 720a1bd..60827f9 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,17 @@ "mocha": "*", "should": "*", "roots": "3.x", - "sinon": "~1.10.3" + "sinon": "~1.10.3", + "coveralls": "~2.11.1", + "istanbul": "~0.3.0" }, "peerDependencies": { "roots": "3.x" }, "scripts": { - "test": "mocha" + "test": "mocha", + "coverage": "make build; NODE_ENV=test istanbul cover ./node_modules/mocha/bin/_mocha; make unbuild; open coverage/lcov-report/index.html;", + "coveralls": "make build; make coveralls; make unbuild;" }, "repository": { "type": "git", diff --git a/readme.md b/readme.md index bbefd8a..0300f1d 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ Roots Contentful ================ -[![npm](https://badge.fury.io/js/roots-contentful.png)](http://badge.fury.io/js/roots-contentful) [![tests](https://travis-ci.org/carrot/roots-contentful.png?branch=master)](https://travis-ci.org/carrot/roots-contentful) [![dependencies](https://david-dm.org/carrot/roots-contentful.png?theme=shields.io)](https://david-dm.org/carrot/roots-contentful) +[![npm](https://badge.fury.io/js/roots-contentful.png)](http://badge.fury.io/js/roots-contentful) [![tests](https://travis-ci.org/carrot/roots-contentful.png?branch=master)](https://travis-ci.org/carrot/roots-contentful) [![dependencies](https://david-dm.org/carrot/roots-contentful.png?theme=shields.io)](https://david-dm.org/carrot/roots-contentful) [![Coverage Status](https://img.shields.io/coveralls/carrot/roots-contentful.svg)](https://coveralls.io/r/carrot/roots-contentful?branch=implement-view-helper) An extension for using roots with the Contentful CMS API. From 45fe12a445e59bd651f02579c7a2fa63291fa64a Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Thu, 24 Jul 2014 18:36:49 -0400 Subject: [PATCH 4/7] a couple small edits, untested --- lib/index.coffee | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/index.coffee b/lib/index.coffee index 6b9bea2..1e4503f 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -5,9 +5,12 @@ contentful = require 'contentful' pluralize = require 'pluralize' errors = - no_token: 'Missing required options for roots-contentful. Please ensure `access_token` and `space_id` are present.' - no_type_id: 'One or more of your content types is missing an `id` value' - sys_conflict: 'One of your content types has `sys` as a field. This is reserved for storing Contentful system metadata, please rename this field to a different value.' + no_token: 'Missing required options for roots-contentful. Please ensure + `access_token` and `space_id` are present.' + no_type_id: 'One or more of your content types is missing an `id` value' + sys_conflict:'One of your content types has `sys` as a field. This is reserved + for storing Contentful system metadata, please rename this field to a + different value.' module.exports = (opts) -> # throw error if missing required config @@ -28,10 +31,11 @@ module.exports = (opts) -> configure_content = (types) -> W.map types, (t) -> - if not t.id then throw new Error errors.no_type_id - t.filters ?= {} + if not t.id then W.reject(errors.no_type_id) if t.name then return W.resolve() - return client.contentType(t.id).then (res) -> + + t.filters ?= {} + W client.contentType(t.id).then (res) -> t.name = pluralize(S(res.name).toLowerCase().underscore().s) return t @@ -43,12 +47,12 @@ module.exports = (opts) -> ### get_all_content = (types) -> - locals = {} - W.map(types, (t) -> + W.reduce types, (m, t) -> fetch_content(t) .then(format_content) - .then (c) -> locals[t.name] = c - ).yield(locals) + .then((c) -> m[t.name] = c) + .yield(m) + , {} ###* * Fetch entries for a single content type object @@ -57,7 +61,7 @@ module.exports = (opts) -> ### fetch_content = (type) -> - client.entries(_.merge(type.filters, content_type: type.id)) + W client.entries(_.merge(type.filters, content_type: type.id)) ###* * Formats raw response from Contentful @@ -75,7 +79,7 @@ module.exports = (opts) -> ### format_entry = (e) -> - if _.has(e.fields, 'sys') then throw new Error errors.sys_conflict + if _.has(e.fields, 'sys') then W.reject(errors.sys_conflict) _.assign(_.omit(e, 'fields'), e.fields) # load content @@ -89,5 +93,5 @@ module.exports = (opts) -> before_pass: (ctx) => # once content is loaded, pass contentful data into locals promise.then (locals) => - return if @roots.config.locals.contentful + if @roots.config.locals.contentful then return @roots.config.locals.contentful = locals From a1568be32207abedbf8a74f61396e40071a8b88f Mon Sep 17 00:00:00 2001 From: Josh Rowley Date: Fri, 25 Jul 2014 13:42:14 -0400 Subject: [PATCH 5/7] test custom name and missing access token --- lib/index.coffee | 10 ++--- test/fixtures/basic/about.jade | 1 + test/fixtures/custom_name/about.jade | 1 + test/fixtures/custom_name/app.coffee | 19 ++++++++ test/fixtures/custom_name/index.jade | 5 +++ test/fixtures/custom_name/package.json | 6 +++ test/fixtures/missing_config/app.coffee | 18 ++++++++ test/fixtures/missing_config/index.jade | 5 +++ test/fixtures/missing_config/package.json | 6 +++ test/test.coffee | 55 ++++++++++++++++++----- 10 files changed, 111 insertions(+), 15 deletions(-) create mode 100644 test/fixtures/basic/about.jade create mode 100644 test/fixtures/custom_name/about.jade create mode 100644 test/fixtures/custom_name/app.coffee create mode 100644 test/fixtures/custom_name/index.jade create mode 100644 test/fixtures/custom_name/package.json create mode 100644 test/fixtures/missing_config/app.coffee create mode 100644 test/fixtures/missing_config/index.jade create mode 100644 test/fixtures/missing_config/package.json diff --git a/lib/index.coffee b/lib/index.coffee index 1e4503f..7cefa06 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -31,9 +31,8 @@ module.exports = (opts) -> configure_content = (types) -> W.map types, (t) -> - if not t.id then W.reject(errors.no_type_id) - if t.name then return W.resolve() - + if not t.id then return W.reject(errors.no_type_id) + if t.name then return W.resolve(t) t.filters ?= {} W client.contentType(t.id).then (res) -> t.name = pluralize(S(res.name).toLowerCase().underscore().s) @@ -79,11 +78,12 @@ module.exports = (opts) -> ### format_entry = (e) -> - if _.has(e.fields, 'sys') then W.reject(errors.sys_conflict) + if _.has(e.fields, 'sys') then return W.reject(errors.sys_conflict) _.assign(_.omit(e, 'fields'), e.fields) # load content - promise = configure_content(opts.content_types).then(get_all_content) + promise = configure_content(opts.content_types) + .then(get_all_content) class RootsContentful constructor: (@roots) -> diff --git a/test/fixtures/basic/about.jade b/test/fixtures/basic/about.jade new file mode 100644 index 0000000..8240f0e --- /dev/null +++ b/test/fixtures/basic/about.jade @@ -0,0 +1 @@ +h1 wow diff --git a/test/fixtures/custom_name/about.jade b/test/fixtures/custom_name/about.jade new file mode 100644 index 0000000..8240f0e --- /dev/null +++ b/test/fixtures/custom_name/about.jade @@ -0,0 +1 @@ +h1 wow diff --git a/test/fixtures/custom_name/app.coffee b/test/fixtures/custom_name/app.coffee new file mode 100644 index 0000000..be6dbd9 --- /dev/null +++ b/test/fixtures/custom_name/app.coffee @@ -0,0 +1,19 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + access_token: 'YOUR_ACCESS_TOKEN' + space_id: 'aqzq2qya2jm4' + content_types: [ + { + id: '6BYT1gNiIEyIw8Og8aQAO6' + name: 'press_links' + }, + { + id: '7CDlVsacqQc88cmIEGYWMa' + } + ] + ) + ] diff --git a/test/fixtures/custom_name/index.jade b/test/fixtures/custom_name/index.jade new file mode 100644 index 0000000..190e01d --- /dev/null +++ b/test/fixtures/custom_name/index.jade @@ -0,0 +1,5 @@ +ul + - for p in contentful.press_links + li + h1= p.title + p= p.body diff --git a/test/fixtures/custom_name/package.json b/test/fixtures/custom_name/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/custom_name/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/fixtures/missing_config/app.coffee b/test/fixtures/missing_config/app.coffee new file mode 100644 index 0000000..9428575 --- /dev/null +++ b/test/fixtures/missing_config/app.coffee @@ -0,0 +1,18 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + access_token: 'YOUR_ACCESS_TOKEN' + space_id: 'aqzq2qya2jm4' + content_types: [ + { + name: 'test' + } + { + id: '7CDlVsacqQc88cmIEGYWMa' + } + ] + ) + ] diff --git a/test/fixtures/missing_config/index.jade b/test/fixtures/missing_config/index.jade new file mode 100644 index 0000000..4769500 --- /dev/null +++ b/test/fixtures/missing_config/index.jade @@ -0,0 +1,5 @@ +ul + - for p in contentful.blog_posts + li + h1= p.title + p= p.body diff --git a/test/fixtures/missing_config/package.json b/test/fixtures/missing_config/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/missing_config/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/test.coffee b/test/test.coffee index c648f8c..5bc7e01 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,12 +1,13 @@ -path = require 'path' -fs = require 'fs' -should = require 'should' -sinon = require 'sinon' -W = require 'when' -Roots = require 'roots' -_path = path.join(__dirname, 'fixtures') -RootsUtil = require 'roots-util' -h = new RootsUtil.Helpers(base: _path) +path = require 'path' +fs = require 'fs' +should = require 'should' +sinon = require 'sinon' +W = require 'when' +Roots = require 'roots' +_path = path.join(__dirname, 'fixtures') +RootsUtil = require 'roots-util' +h = new RootsUtil.Helpers(base: _path) +contentful = require '../lib' # setup, teardown, and utils @@ -36,7 +37,26 @@ after -> # tests -describe 'development', -> +describe 'config', -> + it 'should throw an error when missing an access token', -> + (-> contentful({})).should.throw() + + it 'should throw an error without content type id', (done) -> + (-> compile_fixture.call(@, 'missing_config', (e) -> done(new Error e))) + .should.throw() + + describe 'contentful content type fields', -> + before -> + @stub = stub_contentful(entry: {fields: {sys: 'test'}}) + + it 'should throw an error if `sys` is a field name', -> + (-> compile_fixture.call(@, 'basic', (e) -> done(new Error e))) + .should.throw() + + after -> + @stub.restore() + +describe 'basic compile', -> before (done) -> @title = 'Throw Some Ds' @body = 'Rich Boy selling crack' @@ -54,3 +74,18 @@ describe 'development', -> after -> @stub.restore() + +describe 'custom name for view helper local', -> + before (done) -> + @title = 'Throw Some Ds' + @body = 'Rich Boy selling crack' + @stub = stub_contentful(entry: {fields: {title: @title, body: @body}}) + compile_fixture.call(@, 'custom_name', -> done()) + + it 'has contentful data available in views under a custom name', -> + p = path.join(@public, 'index.html') + h.file.contains(p, @title).should.be.true + h.file.contains(p, @body).should.be.true + + after -> + @stub.restore() From de82d728a9975bc763f3693113f6ab9635004a54 Mon Sep 17 00:00:00 2001 From: Jeff Escalante Date: Mon, 28 Jul 2014 11:40:20 -0400 Subject: [PATCH 6/7] promise fix in progress --- lib/index.coffee | 12 +++++++++--- package.json | 11 ++++++----- test/mocha.opts | 1 + test/support/helpers.js | 17 +++++++++++++++++ test/test.coffee | 31 +++++++++++-------------------- 5 files changed, 44 insertions(+), 28 deletions(-) create mode 100644 test/support/helpers.js diff --git a/lib/index.coffee b/lib/index.coffee index 7cefa06..3b51231 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -81,14 +81,20 @@ module.exports = (opts) -> if _.has(e.fields, 'sys') then return W.reject(errors.sys_conflict) _.assign(_.omit(e, 'fields'), e.fields) - # load content - promise = configure_content(opts.content_types) - .then(get_all_content) + content = [] class RootsContentful constructor: (@roots) -> @roots.config.locals ||= {} + setup: -> + if content.length + configure_content(opts.content_types) + .then(get_all_content) + .then (res) -> content = res + else + W.resolve() + compile_hooks: -> before_pass: (ctx) => # once content is loaded, pass contentful data into locals diff --git a/package.json b/package.json index 60827f9..152c55c 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,14 @@ "pluralize": "0.0.10" }, "devDependencies": { + "chai": "1.x", + "chai-as-promised": "4.x", "coffee-script": "1.7.x", - "mocha": "*", - "should": "*", + "coveralls": "2.x", + "istanbul": "0.3.x", + "mocha": "1.x", "roots": "3.x", - "sinon": "~1.10.3", - "coveralls": "~2.11.1", - "istanbul": "~0.3.0" + "sinon": "1.x" }, "peerDependencies": { "roots": "3.x" diff --git a/test/mocha.opts b/test/mocha.opts index ae10b52..abe3301 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ --reporter spec --compilers coffee:coffee-script/register +--require test/support/helpers --timeout 30000 diff --git a/test/support/helpers.js b/test/support/helpers.js new file mode 100644 index 0000000..fee43b1 --- /dev/null +++ b/test/support/helpers.js @@ -0,0 +1,17 @@ +var chai = require('chai'), + chai_promise = require('chai-as-promised'), + sinon = require('sinon'), + path = require('path'), + _path = path.join(__dirname, '../fixtures'), + RootsUtil = require('roots-util'), + h = new RootsUtil.Helpers({ base: _path }), + roots_contentful = require('../../lib'); + +var should = chai.should(); +chai.use(chai_promise); + +global.should = should; +global.sinon = sinon; +global._path = _path; +global.h = h; +global.roots_contentful = roots_contentful; diff --git a/test/test.coffee b/test/test.coffee index 5bc7e01..47c2284 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,24 +1,19 @@ path = require 'path' fs = require 'fs' -should = require 'should' -sinon = require 'sinon' W = require 'when' +node = require 'when/node' Roots = require 'roots' -_path = path.join(__dirname, 'fixtures') -RootsUtil = require 'roots-util' -h = new RootsUtil.Helpers(base: _path) -contentful = require '../lib' # setup, teardown, and utils compile_fixture = (fixture_name, done) -> @public = path.join(fixture_name, 'public') - h.project.compile(Roots, fixture_name, done) + node.call(h.project.compile.bind(h), Roots, fixture_name) stub_contentful = (opts = {}) -> contentful = require 'contentful' sinon.stub(contentful, 'createClient').returns - contentType: -> W.resolve(opts.content_type || {name: 'Blog Post'}) + contentType: -> W.resolve(opts.content_type || { name: 'Blog Post' }) entries: -> W.resolve [ opts.entry || { sys: {'sys': 'data'}, @@ -39,29 +34,25 @@ after -> describe 'config', -> it 'should throw an error when missing an access token', -> - (-> contentful({})).should.throw() + (-> roots_contentful()).should.throw() - it 'should throw an error without content type id', (done) -> - (-> compile_fixture.call(@, 'missing_config', (e) -> done(new Error e))) - .should.throw() + it 'should throw an error without content type id', -> + compile_fixture.call(@, 'missing_config').should.be.rejected describe 'contentful content type fields', -> - before -> - @stub = stub_contentful(entry: {fields: {sys: 'test'}}) + before -> @stub = stub_contentful(entry: {fields: {sys: 'test'}}) it 'should throw an error if `sys` is a field name', -> - (-> compile_fixture.call(@, 'basic', (e) -> done(new Error e))) - .should.throw() + compile_fixture.call(@, 'basic').should.be.rejected - after -> - @stub.restore() + after -> @stub.restore() describe 'basic compile', -> before (done) -> @title = 'Throw Some Ds' - @body = 'Rich Boy selling crack' + @body = 'Rich Boy selling crick' @stub = stub_contentful(entry: {fields: {title: @title, body: @body}}) - compile_fixture.call(@, 'basic', -> done()) + compile_fixture.call(@, 'basic').then(-> done()) it 'compiles basic project', -> p = path.join(@public, 'index.html') From 742b06f1a41554c5d68ef686baa55afc33d576b8 Mon Sep 17 00:00:00 2001 From: Josh Rowley Date: Mon, 28 Jul 2014 15:13:47 -0400 Subject: [PATCH 7/7] repace sinon with mockery, :100: tests --- lib/index.coffee | 135 +++++++++++------------ package.json | 5 +- test/fixtures/missing_token/app.coffee | 16 +++ test/fixtures/missing_token/index.jade | 1 + test/fixtures/missing_token/package.json | 6 + test/support/helpers.js | 4 +- test/test.coffee | 72 +++++++----- 7 files changed, 132 insertions(+), 107 deletions(-) create mode 100644 test/fixtures/missing_token/app.coffee create mode 100644 test/fixtures/missing_token/index.jade create mode 100644 test/fixtures/missing_token/package.json diff --git a/lib/index.coffee b/lib/index.coffee index 3b51231..0237fb9 100644 --- a/lib/index.coffee +++ b/lib/index.coffee @@ -22,82 +22,71 @@ module.exports = (opts) -> accessToken: opts.access_token space: opts.space_id - ###* - * Configures content types set in app.coffee. Sets default values if - * optional config options are missing. - * @param {Array} types - content_types set in app.coffee extension config - * @return {Promise} - returns an array of configured content types - ### - - configure_content = (types) -> - W.map types, (t) -> - if not t.id then return W.reject(errors.no_type_id) - if t.name then return W.resolve(t) - t.filters ?= {} - W client.contentType(t.id).then (res) -> - t.name = pluralize(S(res.name).toLowerCase().underscore().s) - return t - - ###* - * Fetches data from Contentful API, formats the raw data, and constructs - * the locals object - * @param {Array} types - configured content_type objects - * @return {Promise} - returns formatted locals object with all content - ### - - get_all_content = (types) -> - W.reduce types, (m, t) -> - fetch_content(t) - .then(format_content) - .then((c) -> m[t.name] = c) - .yield(m) - , {} - - ###* - * Fetch entries for a single content type object - * @param {Object} type - content type object - * @return {Promise} - returns response from Contentful API - ### - - fetch_content = (type) -> - W client.entries(_.merge(type.filters, content_type: type.id)) - - ###* - * Formats raw response from Contentful - * @param {Object} content - entries API response for a content type - * @return {Promise} - returns formatted content type entries object - ### - - format_content = (content) -> - W.map(content, format_entry) - - ###* - * Formats a single entry object from Contentful API response - * @param {Object} e - single entry object from API response - * @return {Promise} - returns formatted entry object - ### - - format_entry = (e) -> - if _.has(e.fields, 'sys') then return W.reject(errors.sys_conflict) - _.assign(_.omit(e, 'fields'), e.fields) - - content = [] - class RootsContentful constructor: (@roots) -> @roots.config.locals ||= {} setup: -> - if content.length - configure_content(opts.content_types) + configure_content(opts.content_types) .then(get_all_content) - .then (res) -> content = res - else - W.resolve() - - compile_hooks: -> - before_pass: (ctx) => - # once content is loaded, pass contentful data into locals - promise.then (locals) => - if @roots.config.locals.contentful then return - @roots.config.locals.contentful = locals + .then (res) => + @roots.config.locals.contentful = res + + ###* + * Configures content types set in app.coffee. Sets default values if + * optional config options are missing. + * @param {Array} types - content_types set in app.coffee extension config + * @return {Promise} - returns an array of configured content types + ### + + configure_content = (types) -> + W.map types, (t) -> + if not t.id then return W.reject(errors.no_type_id) + if t.name then return W.resolve(t) + t.filters ?= {} + W client.contentType(t.id).then (res) -> + t.name = pluralize(S(res.name).toLowerCase().underscore().s) + return t + + ###* + * Fetches data from Contentful API, formats the raw data, and constructs + * the locals object + * @param {Array} types - configured content_type objects + * @return {Promise} - returns formatted locals object with all content + ### + + get_all_content = (types) -> + W.reduce types, (m, t) -> + fetch_content(t) + .then(format_content) + .then((c) -> m[t.name] = c) + .yield(m) + , {} + + ###* + * Fetch entries for a single content type object + * @param {Object} type - content type object + * @return {Promise} - returns response from Contentful API + ### + + fetch_content = (type) -> + W client.entries(_.merge(type.filters, content_type: type.id)) + + ###* + * Formats raw response from Contentful + * @param {Object} content - entries API response for a content type + * @return {Promise} - returns formatted content type entries object + ### + + format_content = (content) -> + W.map(content, format_entry) + + ###* + * Formats a single entry object from Contentful API response + * @param {Object} e - single entry object from API response + * @return {Promise} - returns formatted entry object + ### + + format_entry = (e) -> + if _.has(e.fields, 'sys') then return W.reject(errors.sys_conflict) + _.assign(_.omit(e, 'fields'), e.fields) diff --git a/package.json b/package.json index 152c55c..0761c3f 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ }, "dependencies": { "lodash": "~2.4.1", - "minimatch": "0.2.x", - "roots-util": "0.0.4", + "roots-util": "~0.0.5", "contentful": "~0.1.2", "when": "~3.4.2", "string": "~1.9.0", @@ -23,7 +22,7 @@ "istanbul": "0.3.x", "mocha": "1.x", "roots": "3.x", - "sinon": "1.x" + "mockery": "~1.4.0" }, "peerDependencies": { "roots": "3.x" diff --git a/test/fixtures/missing_token/app.coffee b/test/fixtures/missing_token/app.coffee new file mode 100644 index 0000000..96412f9 --- /dev/null +++ b/test/fixtures/missing_token/app.coffee @@ -0,0 +1,16 @@ +contentful = require '../../..' + +module.exports = + ignores: ["**/_*", "**/.DS_Store"] + extensions: [ + contentful( + content_types: [ + { + name: 'test' + } + { + id: '7CDlVsacqQc88cmIEGYWMa' + } + ] + ) + ] diff --git a/test/fixtures/missing_token/index.jade b/test/fixtures/missing_token/index.jade new file mode 100644 index 0000000..8240f0e --- /dev/null +++ b/test/fixtures/missing_token/index.jade @@ -0,0 +1 @@ +h1 wow diff --git a/test/fixtures/missing_token/package.json b/test/fixtures/missing_token/package.json new file mode 100644 index 0000000..2d0ae2c --- /dev/null +++ b/test/fixtures/missing_token/package.json @@ -0,0 +1,6 @@ +{ + "name": "test", + "dependencies": { + "jade": "*" + } +} diff --git a/test/support/helpers.js b/test/support/helpers.js index fee43b1..6ca23af 100644 --- a/test/support/helpers.js +++ b/test/support/helpers.js @@ -1,6 +1,6 @@ var chai = require('chai'), chai_promise = require('chai-as-promised'), - sinon = require('sinon'), + mockery = require('mockery'), path = require('path'), _path = path.join(__dirname, '../fixtures'), RootsUtil = require('roots-util'), @@ -11,7 +11,7 @@ var should = chai.should(); chai.use(chai_promise); global.should = should; -global.sinon = sinon; +global.mockery = mockery; global._path = _path; global.h = h; global.roots_contentful = roots_contentful; diff --git a/test/test.coffee b/test/test.coffee index 47c2284..e83576a 100644 --- a/test/test.coffee +++ b/test/test.coffee @@ -1,3 +1,4 @@ +_ = require 'lodash' path = require 'path' fs = require 'fs' W = require 'when' @@ -8,21 +9,31 @@ Roots = require 'roots' compile_fixture = (fixture_name, done) -> @public = path.join(fixture_name, 'public') - node.call(h.project.compile.bind(h), Roots, fixture_name) - -stub_contentful = (opts = {}) -> - contentful = require 'contentful' - sinon.stub(contentful, 'createClient').returns - contentType: -> W.resolve(opts.content_type || { name: 'Blog Post' }) - entries: -> W.resolve [ - opts.entry || { - sys: {'sys': 'data'}, - fields: { - title: 'Default Title' - body: 'Default Body' - } - } - ] + h.project.compile(Roots, fixture_name) + +mock_contentful = (opts = {}) -> + mockery.enable + warnOnUnregistered: false + useCleanCache: true + + opts = _.defaults opts, + entry: + sys: + sys: 'data' + fields: + title: 'Default Title' + body: 'Default Body' + content_type: + name: 'Blog Post' + + mockery.registerMock 'contentful', + createClient: -> + contentType: -> W.resolve(opts.content_type) + entries: -> W.resolve [ opts.entry ] + +unmock_contentful = -> + mockery.deregisterAll() + mockery.disable() before (done) -> h.project.install_dependencies('*', done) @@ -33,26 +44,30 @@ after -> # tests describe 'config', -> + before -> mock_contentful() + it 'should throw an error when missing an access token', -> - (-> roots_contentful()).should.throw() + (-> compile_fixture.call(@, 'missing_token')).should.throw() it 'should throw an error without content type id', -> compile_fixture.call(@, 'missing_config').should.be.rejected - describe 'contentful content type fields', -> - before -> @stub = stub_contentful(entry: {fields: {sys: 'test'}}) + after -> unmock_contentful() + +describe 'contentful content type fields', -> + before -> mock_contentful(entry: {fields: {sys: 'test'}}) - it 'should throw an error if `sys` is a field name', -> - compile_fixture.call(@, 'basic').should.be.rejected + it 'should throw an error if `sys` is a field name', -> + compile_fixture.call(@, 'basic').should.be.rejected - after -> @stub.restore() + after -> unmock_contentful() describe 'basic compile', -> before (done) -> @title = 'Throw Some Ds' @body = 'Rich Boy selling crick' - @stub = stub_contentful(entry: {fields: {title: @title, body: @body}}) - compile_fixture.call(@, 'basic').then(-> done()) + mock_contentful(entry: {fields: {title: @title, body: @body}}) + compile_fixture.call(@, 'basic').then(-> done()).catch(done) it 'compiles basic project', -> p = path.join(@public, 'index.html') @@ -63,20 +78,19 @@ describe 'basic compile', -> h.file.contains(p, @title).should.be.true h.file.contains(p, @body).should.be.true - after -> - @stub.restore() + after -> unmock_contentful() describe 'custom name for view helper local', -> before (done) -> @title = 'Throw Some Ds' @body = 'Rich Boy selling crack' - @stub = stub_contentful(entry: {fields: {title: @title, body: @body}}) - compile_fixture.call(@, 'custom_name', -> done()) + mock_contentful(entry: {fields: {title: @title, body: @body}}) + compile_fixture.call(@, 'custom_name').then(-> done()) + .catch(done) it 'has contentful data available in views under a custom name', -> p = path.join(@public, 'index.html') h.file.contains(p, @title).should.be.true h.file.contains(p, @body).should.be.true - after -> - @stub.restore() + after -> unmock_contentful()