diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..4be9b23 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 4 \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..da645ad --- /dev/null +++ b/.jshintrc @@ -0,0 +1,28 @@ +{ + "globals": { + "spyOn": false, + "it": false, + "console": false, + "describe": false, + "beforeEach": false, + "afterEach": false, + "before": false, + "after": false, + "waits": false, + "waitsFor": false, + "runs": false, + "injector": false + }, + "node": true, + "camelcase": false, + "curly": true, + "forin": true, + "indent": 4, + "unused": true, + "asi": true, + "evil": false, + "laxcomma": true, + "quotmark": false, + "strict": false, + "expr": true +} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..76dc1f2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +# A Word on Contributing + +Contributing to a project is not only adding slabs of code, fixing bugs, or creating optimizations, but it is also contributing to programmers, CTOs, fathers, mothers, sons and daughters. This project isn't about a team of developers or a "we the programmers" yacht club. It really about "us" and helpings others climb that same mountain. Whether the top of the mountain is making more money, improving your developing skills, or spending more time with your family we're all climbing the same rocks. + +> CleverStack aims to not be a framework or a stack, but an ecosystem. [An ecosystem is a community of living organisms (plants, animals and microbes) in conjunction with the nonliving components of their environment (things like air, water and mineral soil), interacting as a system](http://en.wikipedia.org/wiki/Ecosystem). + +> The vision for CleverStack is not to be the best performance framework (although, there is certainly concern for that), including support for all of the gadgets and gizmos (this too, is welcomed), or to be the next buzzword (it would be cool to see others talking about it though) it's more about shifting a developer into a different mindset. A mindset in which not only takes the [modularization principles](http://en.wikipedia.org/wiki/Modular_programming) on a vertical scale (e.g. reducing developing time), but a horizontal scale too (e.g. not needing to focus on every single detail, but being able to take a step back and look at your environment across the horizon). + +> I just wanted to thank you for not only looking into contributing towards CleverStack, but for even taking your time to considering it. I'm personally always available to lend out a hand, and I hope you never feel as if you can't reach out to me. I always look forward to discussing ideas, solving problems, and colloborating with others. + +> - Richard Gustin + +## Submitting bug reports, issues, or enhancement requests. + +There are currently a few resources available to reach out to us: + +1. [GitHub Issues](https://github.com/CleverStack/clever-controller/issues) +2. [Stack Overflow](http://stackoverflow.com/questions/tagged/cleverstack) +3. [Google Groups/Mailing list](https://groups.google.com/forum/#!forum/cleverstack) +4. [IRC](http://webchat.freenode.net/?randomnick=1&channels=%23cleverstack&uio=d4) + +## Submitting Pull Requests + +1. Fork the repo +2. Create a new branch following the [Gitflow](https://www.atlassian.com/git/workflows#!workflow-gitflow) structure. +3. Refactors and documentation changes do not require a test, but if you're adding a new function please write a tests for that feature. +4. Please follow the [coding guidelines](https://github.com/CleverStack/clever-controller/blob/master/.jshintrc) +5. Push your changes and submit a [pull request](https://github.com/CleverStack/clever-controller/compare/) to the master branch. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d83543b --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 CleverStack + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index bd57f33..94e5d74 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,27 @@ -# Clever-Controller +Clever Controller +==================== +[![GitHub version](https://badge.fury.io/gh/cleverstack%2Fclever-controller.png)](http://badge.fury.io/gh/cleverstack%2Fclever-controller) [![Dependency Status](https://david-dm.org/CleverStack/clever-controller.png)](https://david-dm.org/CleverStack/clever-controller) [![devDependency Status](https://david-dm.org/CleverStack/clever-controller/dev-status.png)](https://david-dm.org/CleverStack/clever-controller#info=devDependencies) [![Code Climate](https://codeclimate.com/github/CleverStack/clever-controller.png)](https://codeclimate.com/github/CleverStack/clever-controller) +[![Build Status](https://secure.travis-ci.org/CleverStack/clever-controller.png?branch=master)](https://travis-ci.org/CleverStack/clever-controller) +[![Coverage](https://codeclimate.com/github/CleverStack/clever-controller/coverage.png)](https://codeclimate.com/github/CleverStack/clever-controller) -[![Build Status](http://img.shields.io/travis/CleverStack/clever-controller.svg)](https://travis-ci.org/CleverStack/clever-controller) [![Code Climate](https://codeclimate.com/github/CleverStack/clever-controller.png)](https://codeclimate.com/github/CleverStack/clever-controller) [![Dependency Status](https://david-dm.org/CleverStack/clever-controller.svg?theme=shields.io)](https://david-dm.org/CleverStack/clever-controller) [![devDependency Status](https://david-dm.org/CleverStack/clever-controller/dev-status.svg?theme=shields.io)](https://david-dm.org/CleverStack/clever-controller#info=devDependencies) - -![CleverStack Node Seed](http://cleverstack.github.io/assets/img/logos/node-seed-logo-clean.png "CleverStack Node Seed") - -The Controller used for the [CleverStack](http://cleverstack.io) ecosystem. (You can use this module without CleverStack) +![Clever Controller](http://cleverstack.github.io/assets/img/logos/node-seed-logo-clean.png "Clever Controller") +
+This is the Controller used in the Full-Stack JS Framework CleverStack, however you can use it on its own. +## Lightning-fast flexible controller prototype The main aim of the controller is to help simplify the most common tasks that you need to do when setting up routes and functions/classes to handle them. +### Note: +The documentation is slightly out of date, please refer to the docblocks or make contact with me (richard/pilsy) + ### Installation: It is published in `npm` so a simple `npm install clever-controller` will suffice. ### Routing: +Controllers by default have autoRouting enabled, this means you get RESTful style routes out of the box, override the route by using route: [ '/some/route' ] + ```javascript // Default route setup ~ '/example' or '/example/' or '/example/hello' app.all('/example/?:action?', ExampleController.attach()) diff --git a/controller.js b/controller.js index d32163f..d9bf095 100644 --- a/controller.js +++ b/controller.js @@ -1,51 +1,124 @@ /* jshint node: true */ 'use strict'; -var NoActionException = require('./exceptions/NoAction') - , Class = require( 'uberclass' ) - , path = require( 'path' ) - , util = require( 'util' ) - , i = require( 'i' )() - , debug = require( 'debug' )( 'clever-controller' ) - , routedControllers = []; - -module.exports = Class.extend( +var Class = require( 'uberclass' ) + , path = require( 'path' ) + , util = require( 'util' ) + , i = require( 'i' )() + , debug = require( 'debug' )( 'clever-controller' ) + , NoActionException = require('./exceptions/NoAction') + , routedControllers = []; + +/** + * Clever Controller - lightning-fast flexible controller prototype + * + * @define {CleverController} Clever Controller Class + * @type {Class} + */ +var Controller = Class.extend( /* @Static */ { + /** + * Defines any (string) route or (array) routes to be used in conjuction with autoRouting + * + * Note: + * You do not need to provide a value for this, if autoRouting is enabled, + * and you haven't defined a route one will be assigned based on the filename of your Controller. + * + * @examples + * route: false + * route: '[POST] /example/:id' + * route: [ + * '[POST] /example/?', + * '/example/:id/?', + * '/example/:id/:action/?', + * '/examples/?', + * '/examples/:action/?' + * ] + * + * @default false + * @type {Boolean|String|Array} + */ route: false, + /** + * Turns autoRouting on when not set to false, and when set to an array provides an + * easy way to define middleware for the controller + * + * @examples + * autoRouting: false + * autoRouting: [ + * function( req, res, next ) { + * // define middleware here + * }, + * PermissionController.requiresPermission({ + * all: 'Permission.*' + * }), + * 'controllerFunction' // Where the controller has a function with this name + * ] + * + * @default true + * @type {Boolean|Array} + */ autoRouting: true, + /** + * Turns action based routing on or off + * + * @default true + * @type {Boolean} + */ actionRouting: true, + /** + * Turns restful method based routing on or off + * + * @default true + * @type {Boolean} + */ restfulRouting: true, + /** + * Use this function to attach your controller's to routes (either express or restify are supported) + * @return {Function} returns constructor function + */ attach: function() { return this.callback( 'newInstance' ); }, + /** + * Class (Static) constructor + * + * @constructor + * @return {undefined} + */ setup: function() { - var self = this; if ( this.autoRouting !== false && this.route !== false && routedControllers.indexOf( this.route ) === -1 ) { - // Do not route multiple times + routedControllers.push( this.route ); if ( typeof this.app !== 'undefined' ) { - - // app has been provided for us in the Static of this Controller so we can attach our routes to it this.autoRoute( this.app ); - - } else if ( typeof injector !== 'undefined' ) { - - // Use the clever-injector to get the express app so we can attach our routes to it - injector.inject( function( app ) { - self.autoRoute( app ); - }); + } else { + try { + var injector = require( 'clever-injector' ); + injector.inject( this.callback( function( app ) { + this.autoRoute( app ); + })); + } catch( e ) { + debug( 'Unable to autoRoute, Controller.app is not defined and clever-injector attempt failed with: ' + e + ( e.stack || ' Without a StackTrace') ); + } } } }, + /** + * Attaches controllers routes to the app if autoRouting is enabled and routes have been defined + * + * @param {Object} app Either express.app or restify + * @return {undefined} + */ autoRoute: function( app ) { var middleware = [] , routes = this.route instanceof Array ? this.route : this.route.split( '|' ); @@ -56,12 +129,12 @@ module.exports = Class.extend( if ( this.autoRouting instanceof Array ) { debug( 'Found middleware for ' + routes.join( ', ' ) + ' - ' + util.inspect( this.autoRouting ).replace( /\n/ig, ' ' ) ); - this.autoRouting.forEach(function( mw ) { + this.autoRouting.forEach( this.callback( function( mw ) { middleware.push( typeof mw === 'string' ? this.callback( mw ) : mw ); - }.bind( this )); + })); } - // Add our attach() function + // Add our attach() function to handle requests middleware.push( this.attach() ); // Bind the actual routes @@ -86,6 +159,10 @@ module.exports = Class.extend( }); }, + /** + * Use this function to create a new controller that extends from Controller + * @return {Controller} the newly created controller class + */ extend: function() { var extendingArgs = [].slice.call( arguments ) , autoRouting = ( extendingArgs.length === 2 ) ? extendingArgs[ 0 ].autoRouting !== false : this.autoRouting @@ -117,6 +194,7 @@ module.exports = Class.extend( , plural = i.pluralize( singular ); route = []; + route.push( '[POST] /' + singular + '/?' ) route.push( '/' + singular + '/:id/?' ); route.push( '/' + singular + '/:id/:action/?' ); route.push( '/' + plural + '/?' ); @@ -138,12 +216,48 @@ module.exports = Class.extend( }, /* @Prototype */ { - req: null, - res: null, - next: null, - resFunc: 'json', - action: null, - + /** + * The Request Object + * @type {Request} + */ + req: null, + + /** + * The Response Object + * @type {Response} + */ + res: null, + + /** + * The next function provided by connect, used to continue past this controller + * @type {Function} + */ + next: null, + + /** + * Is set to the most recent action function that was called + * @type {String} + */ + action: null, + + /** + * The name of the default Response handler function + * + * @default 'json' + * @type {String} + */ + resFunc: 'json', + + /** + * This will wrap the performanceSafeSetup() function with a try/catch for safety, + * most of the actual construction is done inside of Controller.performanceSafeSetup() + * + * @constructor + * @param {Request} req the Request Object + * @param {Response} res the Response Object + * @param {Function} next connects next() function + * @return {Array} arguments that will be passed to init() to complete the constructor loop + */ setup: function( req, res, next ) { this.next = next; this.req = req; @@ -156,10 +270,19 @@ module.exports = Class.extend( } }, + /** + * This is effectively what would be in setup() but instead lives here outside of the try/catch + * so google v8 can optimise the code within + * + * @param {Request} req the Request Object + * @param {Response} res the Response Object + * @param {Function} next connects next() function + * @return {Array} arguments that will be passed to init() to complete the constructor loop + */ performanceSafeSetup: function( req, res, next ) { var methodAction = req.method.toLowerCase() + 'Action' , actionRouting = this.Class.actionRouting - , actionMethod = /\/([a-zA-z]+)(\/?|\?.*|\#.*)?$/ig.test( req.url ) ? RegExp.$1 + 'Action' : ( req.params.action !== undefined ? req.params.action : false ) + , actionMethod = /\/([a-zA-z\.]+)(\/?|\?.*|\#.*)?$/ig.test( req.url ) ? RegExp.$1 + 'Action' : ( req.params.action !== undefined ? req.params.action : false ) , restfulRouting = this.Class.restfulRouting , idRegex = /(^[0-9]+$|^[0-9a-fA-F]{24}$)/ , hasIdParam = req.params && req.params.id !== undefined ? true : false @@ -167,15 +290,15 @@ module.exports = Class.extend( , hasActionParam = req.params && req.params.action !== undefined ? true : false , action = !!hasActionParam && !idRegex.test( req.params.action ) ? req.params.action + 'Action' : false; - // console.log( 'methodAction:' + methodAction ); - // console.log( 'actionMethod:' + actionMethod ); - // console.log( 'actionRouting:' + actionRouting ); - // console.log( 'actionMethod:' + actionMethod ); - // console.log( 'restfulRouting:' + restfulRouting ); - // console.log( 'hasIdParam:' + hasIdParam ); - // console.log( 'id:' + id ); - // console.log( 'hasActionParam:' + hasActionParam ); - // console.log( 'action:' + action ); + debug( 'methodAction:' + methodAction ); + debug( 'actionMethod:' + actionMethod ); + debug( 'actionRouting:' + actionRouting ); + debug( 'actionMethod:' + actionMethod ); + debug( 'restfulRouting:' + restfulRouting ); + debug( 'hasIdParam:' + hasIdParam ); + debug( 'id:' + id ); + debug( 'hasActionParam:' + hasActionParam ); + debug( 'action:' + action ); if ( !!actionRouting && !!hasActionParam && action !== false && typeof this[ action ] === 'function' ) { debug( 'actionRouting: mapped by url to ' + action ); @@ -201,15 +324,23 @@ module.exports = Class.extend( return [ new NoActionException(), null, next ]; }, + /** + * The final function in the constructor routine, called eventually after setup() has finished + * + * @param {Error} error any errors encountered during the setup() portion of the constructor + * @param {String} method the name of the method to call on this controller + * @param {Function} next connects next() function + * @return {undefined} + */ init: function( error, method, next ) { if ( error && error instanceof NoActionException ) { debug( 'No route mapping found, calling next()' ); - - this.next(); + next(); } else { try { - if ( error ) + if ( error ) { throw error; + } if ( method !== null ) { this.action = method; @@ -240,7 +371,11 @@ module.exports = Class.extend( }, handleException: function( exception ) { - this.send( { error: 'Unhandled exception: ' + exception, stack: exception.stack }, 500 ); + this.send({ + message: 'Unhandled exception: ' + exception, + stack: exception.stack ? exception.stack.split( '\n' ) : undefined + }, + exception.statusCode || 500 ); }, isGet: function() { @@ -254,4 +389,6 @@ module.exports = Class.extend( isPut: function() { return this.req.method.toLowerCase() === 'put'; } -}); \ No newline at end of file +}); + +module.exports = Controller; diff --git a/index.js b/index.js deleted file mode 100644 index f784ab6..0000000 --- a/index.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require('./controller.js'); \ No newline at end of file diff --git a/package.json b/package.json index d151727..38bbcc7 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,45 @@ { - "name": "clever-controller", - "description": "Lightning-fast flexible controller prototype", - "version": "1.1.5", - "private": false, + "name": "clever-controller", + "description": "Lightning-fast flexible controller prototype", + "version": "1.1.6", + "main": "controller.js", "repository": { - "type": "git", - "url": "http://github.com/CleverStack/clever-controller.git" + "type": "git", + "url": "http://github.com/CleverStack/clever-controller.git" }, - "scripts": { - "test": "mocha --require should --reporter spec" - }, - "dependencies": { - "debug": "^0.8.1", - "i": "^0.3.2", - "uberclass": "" - }, - "devDependencies": { - "mocha": "*", - "should": "*", - "sinon": "*" + "bugs": { + "url": "https://github.com/CleverStack/clever-controller/issues" }, "author": { - "name": "Richard Gustin (PilsY)", - "email": "richard@clevertech.biz" + "name": "CleverStack", + "email": "admin@cleverstack.io", + "web": "http://cleverstack.io" }, + "collaborators": [ + "Richard Gustin