bootstruct
Advanced tools
Comparing version 1.2.4 to 1.2.5
@@ -38,3 +38,3 @@ 'use strict'; | ||
RC = new Ctrl(rootMap, 'RC', null); | ||
RC = new Ctrl(rootMap, '/', null); | ||
@@ -41,0 +41,0 @@ return function handler (req, res) { |
@@ -52,24 +52,7 @@ 'use strict'; | ||
/* | ||
┌───────────────────────── | ||
│ removes file extension | ||
*/ | ||
function removeExt (entry) { | ||
var lastDot = entry.lastIndexOf('.'); | ||
var entryHandlers = { | ||
first: function (first) { | ||
this.first = require(first.path); | ||
}, | ||
return entry.substr(0, lastDot); | ||
} | ||
/* | ||
┌────────────────────────────────────────────── | ||
│ returns lowerCased extension-less entryname | ||
*/ | ||
module.exports.normalizeEntryName = function (entryName, isFile) { | ||
var name = entryName.toLowerCase(); | ||
return (isFile) ? removeExt(name) : name; | ||
}; | ||
module.exports.handlers = { | ||
index: function (index) { | ||
@@ -79,7 +62,8 @@ this.index = require(index.path); | ||
first: function (first) { | ||
this.first = require(first.path); | ||
all: function (all) { | ||
if (!this.index) { | ||
this.index = require(all.path); | ||
} | ||
}, | ||
/* todo: "verbs" handler currently supports only a folder entry */ | ||
verbs: function (verbs) { | ||
@@ -89,19 +73,12 @@ var self = this; | ||
forIn(verbs.entries, function (verbName, verbMap) { | ||
if (verbMap.type === 1) { | ||
// -3 remove tail '.js' | ||
verbName = verbName.substr(0, verbName.length-3); | ||
} | ||
verbName = normalizeEntryName(verbName, verbMap.type); | ||
// set the verb only if not set yet. | ||
// this makes verbs[verbName] overriden-able by a specific verb entry | ||
if (verbName === 'all' || verbName === 'noVerb') { | ||
if (!self[verbName]) { | ||
self[verbName] = require(verbMap.path); | ||
} | ||
/* | ||
│ set only if not set yet. | ||
│ this makes 'verbs' overriden-able by a specific verb entry. | ||
│ 'all' is an exception because it's actually an 'index' and not stored in ctrl.verbs. | ||
*/ | ||
if (!self[verbName] && !self.verbs[verbName]) { | ||
entryHandlers[verbName.toLowerCase()].call(self, verbMap); | ||
} | ||
else { | ||
if (!hasOwn.call(self.verbs, verbName)) { | ||
self.verbs[verbName] = require(verbMap.path); | ||
} | ||
} | ||
}); | ||
@@ -126,4 +103,4 @@ }, | ||
noverb: function (noverb) { | ||
this.noVerb = require(noverb.path); | ||
noverb: function (noVerb) { | ||
this.verbs.noVerb = require(noVerb.path); | ||
}, | ||
@@ -134,2 +111,29 @@ | ||
} | ||
}; | ||
}; | ||
/* | ||
┌───────────────────────── | ||
│ removes file extension | ||
*/ | ||
function removeExt (entry) { | ||
var lastDot = entry.lastIndexOf('.'); | ||
return entry.substr(0, lastDot); | ||
} | ||
/* | ||
┌──────────────────────────────────────────────── | ||
│ returns lowerCased extension-less entryname. | ||
*/ | ||
function normalizeEntryName (entryName, isFile) { | ||
var name = entryName.toLowerCase(); | ||
return (isFile) ? removeExt(name) : name; | ||
} | ||
// ---------------------------------------------------- | ||
module.exports.entryHandlers = entryHandlers; | ||
module.exports.normalizeEntryName = normalizeEntryName; |
@@ -6,17 +6,21 @@ 'use strict'; | ||
=========== | ||
private methods: | ||
parseFolderMap - on init | ||
setupChains - on init | ||
removeSelfName - on request (checkIn) | ||
isTarget - on request (checkIn) | ||
runVerb - called with ctx: this = ctrl | ||
runSub - called with ctx: this = ctrl | ||
run = { | ||
.sub() - on request (in chain) | ||
.verb() - on request (in chain) | ||
} | ||
both being pushed to chains and called with: .call(ctrl, io) | ||
parseFolderMap() - on init | ||
setupChains() - on init | ||
removeSelfName() - on request (checkIn) | ||
constructor (exported) | ||
Constructor (exported) | ||
exposed API (constructor.prototype): | ||
checkIn | ||
next | ||
checkOut | ||
exposed API: | ||
Constructor.prototype = { | ||
checkIn() | ||
isTarget() | ||
next() | ||
checkOut() | ||
} | ||
@@ -26,6 +30,6 @@ */ | ||
var CtrlProto; | ||
var forIn = require('../utils/forIn'); | ||
var entriesHandler = require('./entriesHandler'); | ||
var handlers = entriesHandler.handlers; | ||
var normalize = entriesHandler.normalizeEntryName; | ||
var forIn = require('../utils/forIn'); | ||
var entriesHandler = require('./entriesHandler'); | ||
var entryHandlers = entriesHandler.entryHandlers; | ||
var normalizeEntryName = entriesHandler.normalizeEntryName; | ||
@@ -46,8 +50,78 @@ var hasOwn = Object.prototype.hasOwnProperty; | ||
//----------------- | ||
// private methods | ||
//----------------- | ||
// both called with: .call(ctrl, io) from chain | ||
var run = { | ||
sub: function (io) { | ||
var subCtrl; | ||
var next = io.params[0]; | ||
// get called on init | ||
subCtrl = this.subCtrls[next.toLowerCase()]; | ||
if (typeof subCtrl === 'function') { // method | ||
subCtrl.call(this, io); | ||
} | ||
else { // subCtrl | ||
subCtrl.checkIn(io); | ||
} | ||
}, | ||
verb: function (io) { | ||
var verbs = this.verbs; | ||
var verb = verbs[io.req.method.toLowerCase()]; | ||
if (verb) { | ||
verb.call(this, io); | ||
} | ||
else if (verbs.noVerb) { | ||
verbs.noVerb.call(this, io); | ||
} | ||
else { | ||
this.next(io); | ||
} | ||
} | ||
}; | ||
/* | ||
│ because ctrl.verbs also contain 'noVerb' | ||
*/ | ||
function hasVerbs (ctrl) { | ||
var verbs = ctrl.verbs; | ||
if (verbs.get || verbs.post || verbs.put || verbs.delete){ | ||
return true; | ||
} | ||
return false; | ||
} | ||
function hasSubCtrls (ctrl) { | ||
return ctrl.subKeys.length > 0; | ||
} | ||
/* | ||
┌─────────────────────────────────── | ||
│ id is unique, name isn't. | ||
│ examples: | ||
│ id: '/', '//foo', '//foo/bar' | ||
│ name: '/', 'foo', 'bar' | ||
*/ | ||
function setMetaProps (ctrl, name, parent) { | ||
// RC stands for the Root Controller | ||
if (!parent) { // RC | ||
ctrl.RC = ctrl; | ||
ctrl.isRoot = true; | ||
ctrl.id = '/'; | ||
} | ||
else { | ||
ctrl.RC = parent.RC; | ||
ctrl.isRoot = false; | ||
ctrl.id = parent.id + '/' + name; | ||
} | ||
} | ||
// runs on init | ||
/* | ||
┌───────────────────────────────────────── | ||
@@ -73,26 +147,48 @@ │ the main ctrl init function. | ||
// normalize = lowerCase, remove extension | ||
name = normalize(key, isFile); | ||
// normalizeEntryName = lowerCase, remove extension | ||
name = normalizeEntryName(key, isFile); | ||
// if such entryHandler exists | ||
if (hasOwn.call(handlers, name)) { | ||
handlers[name].call(ctrl, ctrlEntries[key]); | ||
return; | ||
if (hasOwn.call(entryHandlers, name)) { | ||
entryHandlers[name].call(ctrl, entryMap); | ||
} | ||
else if (isFile){ | ||
ctrl.methods[name] = require(entryMap.path); | ||
else { | ||
ctrl.subCtrls[name] = (isFile)? require(entryMap.path) : entryMap; | ||
ctrl.subKeys.push(name); | ||
} | ||
else { // folder | ||
ctrl.subCtrls[name] = new Ctrl(entryMap, key, ctrl); | ||
} | ||
ctrl.subKeys.push(name); | ||
}); | ||
} | ||
// get called on init | ||
/* | ||
┌────────────────────────────────────────────────────────────────────────────────────────── | ||
│ delegated means when exists in a ctrl, all of its sub-ctrl will get it too (inheritance) | ||
*/ | ||
function delegateNoVerb (ctrl) { | ||
var verbs = ctrl.verbs; | ||
// if doesn't own: take from parent | ||
if (!verbs.noVerb && !ctrl.isRoot) { | ||
verbs.noVerb = ctrl.parent.verbs.noVerb; | ||
} | ||
} | ||
// runs on init | ||
/* | ||
┌──────────────────────────────────────────────────────────────────────────────────────────── | ||
│ Controllers can be used in 2 ways: as target or not | ||
│ so they can have 2 different chains of methods, 1 for each case. | ||
│ | ||
│ examples: | ||
│ 0. subCHain = [first, runSub, last] | ||
│ 1. targetChain = [first, index, runVerb, last] | ||
│ | ||
│ "setupChains" fn creates an array of 2 chains (2 arrays) of methods to run in both cases. | ||
│ | ||
│ ends with: | ||
│ this.chains = [subChain, targetChain]; | ||
│ | ||
*/ | ||
function setupChains (ctrl) { | ||
var subChain = ctrl.subChain = []; | ||
var targetChain = ctrl.targetChain = []; | ||
var subChain = []; | ||
var targetChain = []; | ||
@@ -110,9 +206,8 @@ | ||
} | ||
if (hasProps(ctrl.verbs)) { | ||
targetChain.push(runVerb); | ||
if (hasVerbs(ctrl)) { | ||
targetChain.push(run.verb); | ||
} | ||
// subCtrls or methods | ||
if (ctrl.subKeys.length > 0) { | ||
subChain.push(runSub); | ||
if (hasSubCtrls(ctrl)) { | ||
subChain.push(run.sub); | ||
} | ||
@@ -126,7 +221,18 @@ | ||
ctrl.targetChainLen = targetChain.length; | ||
ctrl.subChainLen = subChain.length; | ||
// 0: subChain | ||
// 1: targetChain | ||
ctrl.chains = [subChain, targetChain]; | ||
} | ||
// get called on each request (checkIn) | ||
function initSubCtrls (ctrl) { | ||
var subCtrls = ctrl.subCtrls; | ||
forIn(subCtrls, function (name, subCtrlMap) { | ||
if (typeof subCtrlMap != 'function') { | ||
subCtrls[name] = new Ctrl(subCtrlMap, name, ctrl); | ||
} | ||
}); | ||
} | ||
/* | ||
@@ -151,3 +257,3 @@ ┌───────────────────────────────────────────────────────────────────── | ||
// if (first == ctrl.name) | ||
if (first && first.toLowerCase() === ctrl.name.toLowerCase()) { | ||
if (first && first.toLowerCase() === ctrl.name) { | ||
@@ -159,62 +265,6 @@ // remove first | ||
// get called on each request (checkIn) | ||
/* | ||
based on the next param, is this ctrl is the target ctrl for the current request? | ||
*/ | ||
function isTarget (ctrl, io) { | ||
var next = io.params[0]; | ||
// if next is in ctrl.subKeys | ||
if (next && ctrl.subKeys.indexOf(next.toLowerCase()) > -1) { | ||
return false; | ||
} | ||
return true; | ||
} | ||
// ------------------------------------------ | ||
// on init: this fn is pushed in targetChain | ||
// on request: called with .call(ctrl, io) | ||
// ------------------------------------------ | ||
function runVerb (io) { | ||
var verb = this.verbs[io.req.method.toLowerCase()]; | ||
if (verb) { | ||
verb.call(this, io); | ||
} | ||
else { | ||
this.next(io); | ||
} | ||
} | ||
// ---------------------------------------- | ||
// on init: this fn is pushed in subchain | ||
// on request: called with .call(ctrl, io) | ||
// ---------------------------------------- | ||
function runSub (io) { | ||
var sub; | ||
var next = io.params[0]; | ||
next && (next = next.toLowerCase()); | ||
if (next) { | ||
// is(next = subCtrls)? | ||
if (hasOwn.call(this.subCtrls, next)) { | ||
this.subCtrls[next].checkIn(io); | ||
} | ||
// is(next = method)? | ||
else if (hasOwn.call(this.methods, next)){ | ||
this.methods[next].call(this, io); | ||
} | ||
// todo: shared method? | ||
} | ||
} | ||
// ========================================== | ||
@@ -224,6 +274,7 @@ // Ctrl Constructor & Constructor.prototype | ||
function Ctrl (folderMap, name, parent) { | ||
// defaults | ||
name = name || '/' | ||
parent = parent || null; | ||
// meta | ||
this.id = (parent) ? (parent.id+'/'+name) : 'RC'; | ||
this.parent = parent; | ||
@@ -239,12 +290,16 @@ this.name = name; | ||
this.index = null; | ||
this.verbs = {}; | ||
this.noVerb = {}; | ||
this.subCtrls = {}; | ||
this.methods = {}; | ||
this.last = null; | ||
this.verbs = Object.create(null); | ||
this.subCtrls = Object.create(null); | ||
// init | ||
setMetaProps(this, name, parent); | ||
parseFolderMap(this, folderMap); | ||
delegateNoVerb(this); | ||
setupChains(this); | ||
initSubCtrls(this); | ||
} | ||
@@ -267,3 +322,3 @@ | ||
idx : 0, | ||
isTarget : isTarget(this, io) | ||
isTarget : this.isTarget(io) | ||
}; | ||
@@ -276,5 +331,25 @@ | ||
/* | ||
┌──────────────────────────────────────────────────────────────────────────── | ||
│ test on checkIn, if this ctrl is the target ctrl for the current request. | ||
│ checks if the next param is an existing sub | ||
│ | ||
│ returns | ||
│ 0: sub | ||
│ 1: target | ||
*/ | ||
CtrlProto.isTarget = function (io) { | ||
var next = io.params[0]; | ||
// if next is in this.subKeys | ||
if (next && this.subKeys.indexOf(next.toLowerCase()) > -1) { | ||
return 0; | ||
} | ||
return 1; | ||
}; | ||
/* | ||
┌────────────────────────────────────────────────── | ||
│ called by io.next() | ||
│ runs the next fn in chain, based on the profile | ||
*/ | ||
@@ -284,23 +359,23 @@ CtrlProto.next = function(io, profile) { | ||
// set this as the current handling ctrl | ||
// set current handling ctrl | ||
io._ctrl = this; | ||
// get profile | ||
profile = profile || io._profiles[this.id]; | ||
// is target? | ||
i = profile.idx++; | ||
// is target? 0: sub, 1: target | ||
isTarget = (profile.isTarget); | ||
// get corresponding chain | ||
chain = (isTarget) ? this.targetChain : this.subChain; | ||
chain = this.chains[isTarget]; | ||
i = profile.idx++; | ||
// if there's more in the chain to run - run it. | ||
if (i < chain.length) { | ||
// if there's more in the chain to run - run it. else - checkOut | ||
if (chain[i]) { | ||
chain[i].call(this, io); | ||
return; | ||
} | ||
this.checkOut(io); | ||
else { | ||
this.checkOut(io); | ||
} | ||
}; | ||
@@ -314,6 +389,4 @@ | ||
CtrlProto.checkOut = function(io) { | ||
var parent = this.parent; | ||
if (parent) { | ||
parent.next(io); | ||
if (!this.isRoot) { | ||
this.parent.next(io); | ||
} | ||
@@ -324,3 +397,9 @@ else { // RC ends response | ||
// todo: think. is it right for the RC to always end the response? | ||
/* | ||
* Think: Is it right for the RC to always end the response? | ||
* | ||
* This makes Bootstruct a closed app and | ||
* unable to pass the request/response to an external middleware. | ||
* indtead of ending the response, consider calling "next" (that must be passed on Bootstruct creation) | ||
*/ | ||
}; | ||
@@ -327,0 +406,0 @@ |
@@ -91,3 +91,3 @@ 'use strict'; | ||
folderObj.entries[entryName.toLowerCase()] = newObj; | ||
folderObj.entries[entryName] = newObj; | ||
}); | ||
@@ -94,0 +94,0 @@ |
{ | ||
"name": "bootstruct", | ||
"version": "1.2.4", | ||
"description": "Routing by structure", | ||
"version": "1.2.5", | ||
"description": "Routing by structure. A name-convention framework for Node.js.", | ||
"main": "index.js", | ||
@@ -6,0 +6,0 @@ "author": "Taitu Lizenbaum taitulism@gmail.com", |
770
README.md
@@ -1,36 +0,19 @@ | ||
Bootstruct | ||
========== | ||
>*"Routing by structure"* | ||
A name-convention framework for Node.js | ||
Table of contents | ||
----------------- | ||
>*Routing by structure.* | ||
* [Get started](#get-started) | ||
* [Overview](#overview) | ||
* [Controllers](#controllers) | ||
* [get, post, put, delete](#get-post-put-delete) | ||
* [index](#index) | ||
* [first & last](#first--last) | ||
* [verbs](#verbs) | ||
* [io](#io) | ||
* [io.params](#ioparams) | ||
* [io other props](#io-other-props) | ||
* [Summary](#summary) | ||
* [The Shorter Version](#the-shorter-version) | ||
* [Important notes](#important-notes) | ||
A name-convention framework for Node.js | ||
Get started | ||
----------- | ||
1. Start a new project: Create a folder with an extra ordinary name like: "myApp". | ||
2. Install Bootstruct: | ||
1. Install Bootstruct in a new folder: | ||
```sh | ||
$ npm install bootstruct | ||
``` | ||
If this means nothing to you, welcome to Node! | ||
3. In your project's folder, create a `server.js` file with the following content: | ||
2. Create an `index.js` file and an `app` folder. | ||
3. Copy the following to your `index.js` file: | ||
```js | ||
@@ -41,4 +24,4 @@ var http = require('http'); | ||
// create a new Bootstruct app from `myApp` folder | ||
var app = bts('myApp'); | ||
// this creates a new Bootstruct app from `app` folder | ||
var app = bts('app'); | ||
@@ -51,5 +34,4 @@ | ||
``` | ||
4. Create a folder named `app` in your project's folder. | ||
5. Inside `app`, create a file named `get.js` and make it export a single function that accepts a single argument: | ||
4. Inside `app` folder, create a `get.js` file with the following content: | ||
```js | ||
@@ -60,14 +42,9 @@ module.exports = function (io) { | ||
``` | ||
`io` is an object that holds the native request/response as properties: | ||
`io.req` | ||
`io.res` | ||
Both by reference, untouched. | ||
If you used Node before, the `io.res.end` part should be very clear now. | ||
6. Start your server up: | ||
5. Start your server up: | ||
``` | ||
$ node server.js | ||
$ node index.js | ||
``` | ||
** You're now ready for GET requests to `yourdomain.com:1001/` ** | ||
**You're now ready for GET requests to `yourdomain.com:1001/`** | ||
@@ -79,5 +56,44 @@ >NOTE: You can use `post`, `put` and `delete` (.js) as well. They are all reserved names for files and folders in Bootstruct. | ||
#Overview | ||
--------- | ||
With Bootstruct you structure your files and folders in a certain way to get a certain behavior. | ||
Table of contents | ||
----------------- | ||
* [Get started](#get-started) | ||
* [Overview](#overview) | ||
* [get, post, put, delete](#get-post-put-delete) | ||
* [A "stale" folder](#a-stale-folder) | ||
* [Scale It Up](#scale-it-up) | ||
* ['verbs' folder](#verbs-folder) | ||
* [noVerb](#noverb) | ||
* [first & last](#first--last) | ||
* [Reserved Entry Names](#reserved-entry-names) | ||
* [io](#io) | ||
* [io.params](#ioparams) | ||
* [io other props](#io-other-props) | ||
* [Bootstruct Flow](#bootstruct-flow) | ||
* [The Shorter Version](#the-shorter-version) | ||
* [Important notes](#important-notes) | ||
intro | ||
----- | ||
"Coding by convention" or "configuration over code" or whatever. They are all fine and it's all a matter of personal taste and project's needs. | ||
Bootstruct does it with names convention. "Routing by structure" if you'd like. | ||
As such, learning Bootstruct is more about understanding how it reads your folder-structure and behave based on this structure than code and syntax. | ||
Overview | ||
-------- | ||
What Bootstruct does? | ||
* Bootstruct saves you from coding your routes | ||
* Bootstruct enforces a good code seperation by design | ||
* Bootstruct gives you intuitive control over requests' flow | ||
* Bootstruct provides you with nice RESTfull URLs | ||
To support routes like: | ||
@@ -89,24 +105,16 @@ | ||
you don't need to learn any new syntax, just structure your files and folders like this: | ||
```js | ||
. | ||
├── node_modules | ||
├── app <── | ||
│ ├── index.js | ||
you don't have to write any code but the handler functions themselves. Just structure your app folder like the following and export your handlers from the `index.js` files like in the [Get started](#get-started) example above: | ||
``` | ||
├── app | ||
│ ├── index.js <── called for all requests to: '/' | ||
│ └── foo | ||
│ ├── index.js | ||
│ ├── index.js <── called for all requests to: '/foo' | ||
│ └── bar | ||
│ └── index.js | ||
│ | ||
├── index.js | ||
└── package.json | ||
│ └── index.js <── called for all requests to: '/foo/bar' | ||
``` | ||
**The `index.js` file will run only in the requested folder.** | ||
If you're familiar with express/connect, the equivalent would be: | ||
```js | ||
// NOT Bootstruct! express/connect equivalent: | ||
// NOT Bootstruct! **express/connect** equivalent: | ||
app.all('/', function () { | ||
@@ -126,246 +134,234 @@ // ... | ||
"Coding by convention" or "configuration over code" or whatever. They are all fine and it's all a matter of personal taste and project's needs. | ||
Bootstruct does it with names convention. "Routing by structure" if you'd like. | ||
>NOTE: You can use `all` instead of `index` if you'd like. | ||
As such, learning Bootstruct is more about file-names, folder-names and folder-structure than code and syntax. Understand Bootstruct's flow. | ||
As your app scales up, you can turn your files into folders. If the `get.js` file from the [Get started](#get-started) example would get bigger, you could replace it with a `get` folder containing an `index.js` file with whatever contents the original `get.js` file had. Anyway this is going to be "require"d as the folder's `get` method. | ||
>NOTE: This is why Bootstruct doesn't matter if it's a file or a folder and in these docs files and folders are referred as **"entries"**. | ||
get, post, put, delete | ||
---------------------- | ||
You probably want to be more specific about different types of request methods. Just add verb-files where you need them (e.g. `get.js`, `post.js`): | ||
You probably want to support different types of request methods individually, don't you? | ||
Well, just add verb-entries where you need them (for example a `get.js` file or a `post` folder). | ||
``` | ||
├── app | ||
│ ├── index.js <── called for ALL requests to: '/' | ||
│ ├── get.js <── called for GET requests to: '/' | ||
│ └── foo | ||
│ ├── index.js <── called for ALL requests to: '/foo' | ||
│ ├── get.js <── called for GET requests to: '/foo' | ||
│ ├── post.js <── called for POST requests to: '/foo' | ||
│ └── bar | ||
│ └── index.js <── called for ALL requests to: '/foo/bar' | ||
``` | ||
>NOTE: `index.js` is called before all verbs (when exists). | ||
You don't have to use an `index.js`: | ||
``` | ||
├── app | ||
│ ├── get.js <── called for GET requests to: '/' | ||
│ └── foo | ||
│ ├── get.js <── called for GET requests to: '/foo' | ||
│ ├── post.js <── called for POST requests to: '/foo' | ||
│ └── bar | ||
│ └── get.js <── called for GET requests to: '/foo/bar' | ||
``` | ||
Bootstruct provides you with an even greater controll on requests' flow. The `index` and the verb entries only run for the requested folder (or the target folder). On a request to `/foo/bar`, `bar` is the target folder and requests are just "passing by" its parent folders `app` and `app/foo`. | ||
If an `index.js` exists it will **always** run before the verb and they are both called only for the target folder. The target-folder is the last folder whose name found in the request pathname (e.g. `bar` on request to: `/foo/bar`). | ||
If you want a folder to do something even if it's not the target folder, create a `first` entry in it. It will run for every request this folder was called in, even if it's not the target folder. | ||
**express/connect** equivalent would be: | ||
`first` is called in a folder when a request comes in whether or not this folder is the target folder or should the request be passed on. `first` is called when a request "checks-in" at a folder and `last` is called when it "checks-out", after the target folder is done with its `index` and verb files. | ||
```js | ||
// NOT Bootstruct! express/connect equivalent: | ||
app.all('/', function () { | ||
// ... | ||
}); | ||
All of these special names we give our entries are reserved entry names in Bootstruct and play a cretain roll in your app's flow. | ||
app.get('/', function () { | ||
// ... | ||
}); | ||
###Reserved Entry Names: | ||
1. first | ||
2. index | ||
3. get | ||
4. post | ||
5. put | ||
6. delete | ||
7. last | ||
app.all('/foo', function () { | ||
// ... | ||
}); | ||
Entries with custom names (e.g. `foo`, `bar`) are parsed as Bootstruct controllers. | ||
app.get('/foo', function () { | ||
// ... | ||
}); | ||
app.post('/foo', function () { | ||
// ... | ||
}); | ||
app.all('/foo/bar', function () { | ||
// ... | ||
}); | ||
#Controllers | ||
------------ | ||
A controller is an object that is parsed out from a folder. You can say it's a kind of a representation of a folder. | ||
Folder's entries become controller's sub-controllers and methods. | ||
Bootstruct builds its controller objects when it initializes (on require). | ||
The Root-Controller (RC) is a javascript object that Bootstruct parses out from the `app` folder. | ||
Let's say that when the `app` folder is empty - RC is empty: | ||
```js | ||
RC = {} // empty object | ||
``` | ||
and when we create `index` and `get` entries inside it: | ||
```js | ||
RC = { | ||
index: fn | ||
get: fn | ||
} | ||
``` | ||
Controllers can have sub-controllers as folders can have sub-folders. | ||
Example structure: | ||
A "stale" folder | ||
---------------- | ||
If `bar`, for example, is a "stale" folder (has no sub-folders and no other methods but `index`), you can cut the overhead of a folder and turn it into a file: | ||
Before: | ||
``` | ||
. | ||
├── app | ||
├── get.js | ||
└── foo | ||
└── get.js | ||
│ ├── index.js | ||
│ └── foo | ||
│ ├── index.js | ||
│ └── bar <── folder | ||
│ └── index.js | ||
``` | ||
In a "let's-say" code: | ||
```js | ||
RC = { | ||
get: fn | ||
sub_controllers: { | ||
foo: { | ||
get: fn | ||
} | ||
} | ||
} | ||
After: | ||
``` | ||
>NOTE: `foo` will be parsed as a sub-controller of RC because `foo` is not a reserved entry name. | ||
If you'd log the current filename in both `get.js` files: | ||
```js | ||
module.exports = function (io) { | ||
console.log(__filename); | ||
io.res.end(); | ||
}; | ||
├── app | ||
│ ├── index.js | ||
│ └── foo | ||
│ ├── index.js | ||
│ └── bar.js <── file | ||
``` | ||
and run the following GET requests: | ||
1. / | ||
2. /foo | ||
3. /foo/bar | ||
You should get these logs: | ||
1. .../app/get.js | ||
2. .../app/foo/get.js | ||
3. .../app/foo/get.js | ||
When addressing the root, the RC's `get` will run. | ||
When addressing `foo`, foo's `get` will run. | ||
When addressing a `bar` (which doesn't exist), `get` will run in the last controller found (`foo`). | ||
Scale It Up | ||
----------- | ||
If any method file gets bigger, turn it into a folder: | ||
>__IMPORTANT NOTE__: The last controller found in the URL parts is the only controller that also runs its `index` and verb methods. All of its parent-controllers only run their wrapping methods, `first` and `last`. | ||
Before: | ||
``` | ||
├── app | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── post.js <── file | ||
│ ├── put.js | ||
│ └── delete.js | ||
``` | ||
After: | ||
``` | ||
├── app | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── post <── folder | ||
│ │ ├── index.js | ||
│ │ ├── dependency_1.js | ||
│ │ └── dependency_2.js | ||
│ ├── put.js | ||
│ └── delete.js | ||
``` | ||
get, post, put, delete | ||
---------------------- | ||
These 4 verb names are reserved for entries that exports functions, like in the get-started example. | ||
These are some of the methods a controller can have. | ||
For code separation you could use folders with these names as well, just make sure to export your function from an `index.js` file within. | ||
Example structure: | ||
'verbs' folder | ||
-------------- | ||
If you use all verbs, also having multiple sub-folders can hurt your eyes: | ||
``` | ||
. | ||
├── app | ||
├── get.js | ||
├── post | ||
│ ├── module.js | ||
│ └── index.js | ||
├── put.js | ||
└── delete.js | ||
│ ├── about | ||
│ ├── contact | ||
│ ├── delete.js | ||
│ ├── get.js | ||
│ ├── index.js | ||
│ ├── messages | ||
│ ├── post.js | ||
│ ├── profile | ||
│ └── put.js | ||
``` | ||
Example file: | ||
```js | ||
module.exports = function (io){ | ||
// do your thing... | ||
io.res.end(); | ||
}; | ||
For the sake of your eyes, you can use a `verbs` folder, just as a namespace to contain the verbs entries: | ||
``` | ||
├── app | ||
│ ├── about | ||
│ ├── contact | ||
│ ├── index.js | ||
│ ├── messages | ||
│ ├── profile | ||
│ └── verbs <── | ||
│ ├── get.js | ||
│ ├── post.js | ||
│ ├── put.js | ||
│ └── delete.js | ||
``` | ||
>NOTE: `index` and `all` both can exist inside a `verbs` folder. | ||
index | ||
----- | ||
`index` works the same and should also export a function. Controllers run their `index` method before any kind of verb. | ||
Example structure: | ||
In case of duplicates, the entry outside `verbs` will take place: | ||
``` | ||
. | ||
├── app | ||
├── index.js | ||
├── get.js | ||
└── post.js | ||
│ ├── all.js <── this will run | ||
│ └── verbs | ||
│ ├── all.js <── this won't | ||
│ ├── get.js | ||
│ └── post.js | ||
``` | ||
`index.js` contents: | ||
```js | ||
module.exports = function (io) { | ||
io.res.write('from index \n'); | ||
// explained later but might ring a bell | ||
io.next(); | ||
}; | ||
``` | ||
`get.js` contents: | ||
```js | ||
module.exports = function (io) { | ||
io.res.end('from get'); | ||
}; | ||
``` | ||
`post.js` contents: | ||
```js | ||
module.exports = function (io) { | ||
io.res.end('from post'); | ||
}; | ||
noVerb | ||
------ | ||
Consider: | ||
``` | ||
On a `GET` request to `'/'` the response would be: | ||
├── app | ||
│ ├── all.js | ||
│ └── get.js | ||
``` | ||
from index | ||
from get | ||
Now let's say you get a `POST` request. Naturally, Bootstruct will skip the verb and will only run the `all` method. If you want to handle unsupported verbs requests individually you can use a `noVerb` entry. Controllers run their `noVerb` method on unsupported verbs requests, but **only if they have at least one verb**. | ||
``` | ||
On a `POST` request it would be: | ||
├── app | ||
│ ├── all.js | ||
│ ├── get.js | ||
│ └── noVerb.js <── gets called for any type of requests other than `GET` | ||
``` | ||
from index | ||
from post | ||
``` | ||
├── app | ||
│ ├── all.js | ||
│ ├── * | ||
│ └── noVerb.js <── doesn't get called because no verbs at all | ||
``` | ||
This saves you some of the logic you would normally put in you `all` method, regarding `request.method`. | ||
The `index` method runs before any verb does. When you're done in `index` you use `io.next` to make Bootstruct call the next method in line: the verb method. | ||
>NOTE: You can use a `noVerb` entry inside a `verbs` folder as well. | ||
**TIP:** `405` is the server status code for "Method not allowed". | ||
first & last | ||
------------ | ||
These, as their names suggest, will be called before and after the `index` and the verb methods as intuitively expected. `first` runs before the `index`. | ||
`last` runs after the verb. | ||
Example structure: | ||
`noVerb` is a special method in Bootstruct because when exists, it's being delegated from the parent folder to all of its sub-folders recursively. | ||
``` | ||
. | ||
├── app | ||
├── first.js | ||
├── index.js | ||
├── get.js | ||
└── last.js | ||
│ ├── get.js | ||
│ ├── noVerb.js <── gets called for unsupported requests to both: '/' and '/foo' | ||
│ └── foo | ||
│ ├── get.js | ||
│ └── bar.js | ||
``` | ||
Export the same function in all files: | ||
```js | ||
module.exports = function (io) { | ||
console.log(__filename); | ||
io.next(); | ||
}; | ||
``` | ||
On a GET request to '/' you'll get the following logs, in this order: | ||
path/.../app/first.js | ||
path/.../app/index.js | ||
path/.../app/get.js | ||
path/.../app/last.js | ||
first & last | ||
------------ | ||
As we saw earlier, the `index` and the verb entries (files or folders) only run for the target-folder. | ||
verbs | ||
----- | ||
For an even better code separation, you could move all of your verbs into a `verbs` folder. | ||
When you'll have sub-controllers in the same containing folder, adding a `verbs` folder would be more easy on the eye. `verbs` is only a namespace for verb files so it should always be a folder. | ||
If you want a folder to do something even if it's not the target-folder but its name was addressed in the request (e.g. `foo` in `foo/bar`), you could use 2 other methods: `first` and `last`. Both are called when a folder is requested whether or not it's the target-folder or should the request be passed on. `first` is called before the target-folder is done with its `index` and the verb method. `last` is called after the target-folder is done. | ||
Example structure: | ||
``` | ||
. | ||
├── app | ||
├── verbs | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── post.js | ||
│ ├── put.js | ||
│ └── delete.js | ||
├── foo | ||
│ ├── ... | ||
│ └── ... | ||
├── bar | ||
│ ├── ... | ||
│ └── ... | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ └── foo | ||
│ ├── first.js <── | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── post.js | ||
│ ├── bar | ||
│ │ ├── first.js <── | ||
│ │ ├── index.js | ||
│ │ └── last.js <── | ||
│ └── last.js <── | ||
``` | ||
We'll see how these `first` and `last` methods fit in the flow in a sec. | ||
@@ -375,76 +371,82 @@ | ||
Reserved Entry Names | ||
-------------------- | ||
All of these names are all reserved names for entries (files or folders) in Bootstruct. You name your entries with these names and get a certain behavior. | ||
#methods | ||
******** | ||
Excluding `verbs`, entries with **reserved** names being translated into Bootstruct's **known methods** and Bootstruct runs them in a certain order (`first`, `last` etc.). | ||
Bootstruct treats entries with **NON-reserved** names as sub-controllers or **custom methods** of the current controller. | ||
Methods are just functions that handle the `io`. They could be a file or a folder (with an `index.js` file). Anyways, you should export that function because it's gonna be `require`d by a controller. | ||
Methods in Bootstruct are like single-method-controllers, the `index` and they also have no sub-controllers. | ||
>NOTE: if a folder contains an `index.js` file, or in other words: if a controller has an `.index` method, it will be the only method to run the `io`. Reserved entry names means nothing to Bootstruct when an `index.js` is found in the same folder. | ||
Example structure: | ||
``` | ||
. | ||
├── app | ||
├── first.js | ||
├── foo | ||
│ └── get.js | ||
├── bar.js | ||
└── last.js | ||
1. first - first thing to run in a folder | ||
2. verbs - just a namespace folder to hold your verb handlers | ||
3. index - called on all HTTP requests ─┐ | ||
4. all - called on all HTTP requests │ | ||
5. get - called on `GET` HTTP requests │ | ||
6. post - called on `POST` HTTP requests ├─ on target folder only | ||
7. put - called on `PUT` HTTP requests │ | ||
8. delete - called on `DELETE` HTTP requests │ | ||
9. noVerb - called on unsupported verbs requests ─┘ | ||
10. last - last thing to run in a folder | ||
``` | ||
`bar.js` is a non-reserved name file so it should export a function that handles an io. | ||
_Custom_ named entries (like `foo` or `bar`) become controllers which are URL namespace handlers for requests containing their name (e.g. `/foo` and `/foo/bar`). | ||
Let's say this function also logs the file's path, same as before. | ||
So on request to: | ||
/bar | ||
Reserved entry names are parsed as those controllers' different methods and they are called when needed according to their role listed above. Methods expected to export a single function that accept `io` as a single argument (see [Get started](#get-started) for an example) and they pass this `io` from one to another. | ||
you'll see these logs: | ||
An example of a **pseudo** object that describes `foo` controller with a `bar` sub-controller: | ||
```js | ||
var foo = { | ||
first: require('foo/first'), | ||
index: require('foo/index'), | ||
verbs: { | ||
get: require('foo/get'), | ||
post: require('foo/post'), | ||
}, | ||
subControllers: { | ||
bar: { | ||
first: require('foo/bar/first'), | ||
index: require('foo/bar/index'), | ||
verbs: { | ||
get: require('foo/bar/get'), | ||
post: require('foo/bar/post'), | ||
}, | ||
subControllers: null, | ||
last: require('foo/bar/last') | ||
} | ||
}, | ||
last: require('foo/last') | ||
}; | ||
``` | ||
Something very similar is generated on Bootstruct init, when your `app` folder is being parsed. | ||
.../app/first.js | ||
.../app/bar.js | ||
.../app/last.js | ||
>NOTE: You might want to use custom method files (e.g. `bar`) for simple stuff like handling a simple "about" page (`about.js`). | ||
io | ||
-- | ||
**express/connect** middleware functions accept 2-3 arguments: `request`, `response` and `next`. | ||
Bootstruct methods handles only a single argument: `io`. | ||
`io` is an object that holds the native request/response as properties and a `next` method (and more): | ||
`io.req` | ||
`io.res` | ||
Both by reference, untouched. | ||
`io.next()` is for you to call from within your methods when they are done and the `io` is ready for the next method. | ||
With **express/connect**: | ||
#io | ||
*** | ||
`io` is an object that is being created on each request and being passed through your app's routes. | ||
It holds the `request` and the `resonse` objects (`io.req` and `io.res`) and a `.next()` method to pass it to its next checkpoint. | ||
Think of a flow chart that describes your app different possible routes. `io` is the object that walks along those routes. | ||
```js | ||
// NOT Bootstruct! | ||
app.get('/foo', function(req, res, next){ | ||
res.send('hello world'); | ||
next(); | ||
}); | ||
``` | ||
(request,response) | ||
└──┬──┘ | ||
│ | ||
function root (io) { | ||
┌───┘ | ||
first(io) | ||
│ | ||
GET (io) | ||
│ | ||
last (io) | ||
} │ | ||
│ | ||
With **Bootstruct**: | ||
```js | ||
// file: /app/foo/get.js | ||
module.exports = function (io) { | ||
io.res.send('hello world'); | ||
io.next(); | ||
}; | ||
``` | ||
@@ -457,50 +459,37 @@ | ||
--------- | ||
Bootstruct refers the different URL parts as parameters so it splits the URL (pathname only, not the queryString) by slashes and stores the returned array in `io.params`. | ||
Bootstruct refers the different URL parts as parameters so it splits the URL by slashes (pathname only) and stores the returned array in `io.params`. | ||
Bootstruct also: | ||
* merges repeating slashes | ||
* trims slashes (preceding & trailing) | ||
On request to: `/foo/bar/aaa/bbb` | ||
On request to: | ||
`http://yourdomain.com/aaa/bbb/ccc?flag=1&type=normal` | ||
`io.params` starts as: `['foo', 'bar', 'aaa', 'bbb']`. | ||
it'll be equal to: `['aaa', 'bbb', 'ccc']`. | ||
Starting at your `app` folder, Bootstruct uses `io.params` to check if `app` folder is the target-folder by checking the first item for an existing sub-folder. If the first item (e.g. `foo`) is a sub-folder, `app` is not the target. Next, `foo` removes its name from `io.params` (always the first item) and checks the new first item (e.g. `bar`) for a sub-folder and so on. This way the target-folder (`bar`) is left with the params that are not controllers in your app (e.g. `['aaa', 'bbb']`). | ||
Then Bootstruct checks the first parameter and if you have a controller with that name (e.g. `aaa`), it will handle the request only after removing its name from `io.params`. At this stage `io.params = ['bbb', 'ccc']`. Then the next param is being checked against an existing sub-controller (e.g. is there a folder name `bbb` inside `aaa`). | ||
A Request to: `/foo/bar/john`: | ||
with **express/connect**: | ||
```js | ||
// NOT Bootstruct! | ||
app.get('/foo/bar/:name', function(req, res, next){ | ||
console.log(req.params.name); // --> 'john' | ||
next(); | ||
}); | ||
``` | ||
Considering Bootstruct's nature, this is how Bootstruct routes the io through your different folders/controllers structure: It always checks the next item in `io.params` for a matching controller's name. | ||
Every time an io "checks-in" at a controller (with RC as an exception), the controller removes its name from the `io.params` array. It's always the first item. | ||
On `foo` controller check-in, io.params changes: `[foo, bar, baz] ===> [bar, baz]`. | ||
Then the controller (starting with the RC) checks the first item: | ||
* If it has a sub-controller with a matching name (e.g. `foo`), it will pass the io to that sub-controller for another "check-in". | ||
* if there is no sub-controller with that name (e.g. `bar`), what's left in io.params is for you to handle as requests' parameters. | ||
Example structure (same as the last one): | ||
and with **Bootstruct**: | ||
```js | ||
// file: .../app/foo/bar/get.js | ||
module.exports = function (io) { | ||
console.log(io.params[0]); // --> 'john' | ||
io.next(); | ||
}; | ||
``` | ||
. | ||
├── app | ||
├── get.js | ||
└── foo | ||
└── get.js | ||
``` | ||
Run these 2 requests: | ||
1. /bar | ||
2. /foo/bar | ||
You should get: | ||
1. path/.../app/get.js | ||
Params: [bar] | ||
2. path/.../app/foo/get.js | ||
Params: [bar] | ||
io other props | ||
-------------- | ||
* io._ctrl - (internal) The current handler | ||
* io._ctrl - (internal) The current handling controller | ||
* io._profiles - (internal) io's state in all controllers | ||
@@ -511,25 +500,30 @@ | ||
#Summary | ||
******** | ||
Bootstruct Flow | ||
--------------- | ||
Consider a structure: | ||
``` | ||
. | ||
├── app | ||
├── first.js | ||
├── index.js | ||
├── get.js | ||
├── last.js | ||
└── foo | ||
├── first.js | ||
├── index.js | ||
├── get.js | ||
├── last.js | ||
└── bar | ||
├── first.js | ||
├── index.js | ||
├── get.js | ||
└── last.js | ||
│ ├── first.js | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── last.js | ||
│ └── foo | ||
│ ├── first.js | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ ├── last.js | ||
│ └── bar | ||
│ ├── first.js | ||
│ ├── index.js | ||
│ ├── get.js | ||
│ └── last.js | ||
``` | ||
There are 3 levels of nested folders as before: `app/foo/bar`. | ||
Each has the following methods: | ||
* `first` | ||
* `index` | ||
* a verb (`get`) | ||
* `last` | ||
>NOTE: This is a full use case. You don't have to use all of the controller's possible methods for every folder. | ||
>NOTE: This is a full use case. You don't have to use all of the possible methods for every folder. | ||
@@ -543,5 +537,8 @@ Consider all of these files contain: | ||
``` | ||
All functions logs the filename they're exported from and moves on. | ||
i.e. logs the current file path and moves on to the next method. This will make Bootstruct's flow "visible" to you. | ||
The following are examples of requested URLs (GET requests) and their expected logs given the above structure: | ||
The following are examples of different requests supported by the given structure (GET requests only) and their expected logs. | ||
>NOTE: To make it more easy on the eye, preceding `long/path/to/app` and `.js` extensions were removed from the log. | ||
``` | ||
@@ -580,38 +577,5 @@ url: / | ||
A pseudo javascript object that describes your root-controller for this case would be: | ||
```js | ||
RC = { | ||
first: fn, | ||
sub_controllers: { | ||
foo: { | ||
first: fn, | ||
sub_controllers: { | ||
bar: { | ||
first: fn, | ||
sub_controllers: {}, | ||
verbs: { | ||
index: fn, | ||
get: fn | ||
}, | ||
last : fn | ||
} | ||
} | ||
verbs: { | ||
index: fn, | ||
get: fn | ||
}, | ||
last : fn | ||
} | ||
}, | ||
verbs: { | ||
index: fn, | ||
get: fn | ||
}, | ||
last : fn | ||
} | ||
``` | ||
The Shorter Version | ||
@@ -621,8 +585,8 @@ ------------------- | ||
1. Check-in: Controllers run their `first` method. | ||
2. Controllers check the next URL part. Is there a matching sub-controller? | ||
If so, the controller passes the `io` to that sub-controller for a check-in. **Back to 1**. | ||
If not, current controller is the target-controller. It will run its `index` method and then its `verb` method. | ||
3. Check-out: Controllers run their `last` method. | ||
4. Controllers pass the `io` back to their parent controller for a check-out. **Back to 3**. | ||
1. **Check-in**: A folder run its `first` method. | ||
2. **Check: IsTarget?**: Bootstruct checks the next URL part. Is there a matching sub-folder in the current one? | ||
**If so**, the folder passes the `io` to that sub-folder for a check-in. **Back to 1**. | ||
**If not**, current folder is the target-folder. It will run its `index` method and its `verb` methods. | ||
3. **Check-out**: The folder run its `last` method. | ||
4. the folder passes the `io` back to its parent folder for a check-out. **Back to 3**. | ||
@@ -632,7 +596,6 @@ | ||
Important notes: | ||
---------------- | ||
* Bootstruct is CaSe-InSeNsItIvE when it comes to URLs and file names. | ||
* Bootstruct ignores trailing slashes in URLs. | ||
* Bootstruct ignores trailing slashes in URLs and merges repeating slashes. | ||
* Bootstruct ignores entries that their names start with an underscore and doesn't parse them (e.g. `_ignored.js`). | ||
@@ -647,5 +610,44 @@ * You can use the `io` to hold different properties through its cycle. | ||
**** | ||
*********************************************************************** | ||
Questions, suggestions, bugs, hugs, criticism or kudos are all welcome. | ||
*taitulism(at)gmail(dot)com* | ||
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
40151
682
640
11
24