-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmvc.js
executable file
·272 lines (213 loc) · 10.1 KB
/
mvc.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
/*
| mvc.js
| ----
| the mvc component of the crane module
|
| Mike MacMillan
| mikejmacmillan@gmail.com
*/
var util = require('util'),
_ = require('lodash'),
fs = require('fs'),
path = require('path'),
handlebars = require('handlebars'),
q = require('q'),
crane = require('crane-js'),
ioc = crane.ioc;
var _app = null,
_init = null,
_errorHandler = null,
_templates = {
layout: {}
},
_controllers = [],
_opt = {
defaultController: 'index',
indexView: 'index',
routePrefix: '/',
};
//** Component Interface
//** ----
var mvc = (module.exports = {
initialize: function(app, opt, cb) {
if(_init) return;
//** overlay the defaults
_.defaults(_opt, opt);
//** set the global reference to the express app (express only for now)
_app = app;
_errorHandler = opt.errorHandler;
//** if handlebars is being used, do some extra parsing
if(_app.settings['view engine'] == 'hndl') { //** move this to config or an "enum"
var layouts = app.settings.views + 'layout/',
partials = app.settings.views + 'partials/',
formatName = function(n) { return n.replace(path.extname(n), '') };
//** find any layouts defined within the views folder; compile them as a handlebars template
//*** dont read and cache these during dev, use a config to toggle this...
fs.existsSync(layouts) && fs.readdirSync(layouts).forEach(function(layout) {
_templates.layout[formatName(layout)] = handlebars.compile(fs.readFileSync(layouts+layout, 'utf8'));
});
//** find any partials defined within the views folder; register them with handlebars
fs.existsSync(partials) && fs.readdirSync(partials).forEach(function(partial) {
handlebars.registerPartial(formatName(partial), fs.readFileSync(partials+partial, 'utf8'));
});
}
_controllers.forEach(function(name) { ioc.instance(name) });
_init = true;
cb && cb.call(mvc);
},
//** sets the ioc container we use for resolving objects
container: function(cont) { if(cont) ioc = cont; return mvc },
controller: function(name, deps, impl) {
//** set the controller implementation as the exports object for the given module
if(!name || !impl) return;
name = name.toLowerCase(); //** normalize the dependency name; case is ignored
//** if we've seen this controller before, return
if(ioc.contains(name)) return ioc(name);
_controllers.push(name);
ioc.register(name, deps, impl, {
lifetime: ioc.lifetime.singleton,
create: function(name, inst) {
name = (name||'').replace('controller', '');
//** wire up the individual controller methods
parseMethods(inst, name);
//** if this is the default controller, wire it up sans controller name (let its methods be served off root)
if(name == _opt.defaultController)
parseMethods(inst, '');
//** extend the default controller implementation onto the object
_.defaults(inst, {
//** a simple method that wraps the pattern of rendering a view with options
view: function(res, vname, opt, p) {
p = p || _q.defer(), opt = opt||{};
//** render the view, return a promise, resolving/rejecting it when the view is rendered
res.render(vname, opt, function(err, html) {
if(err) return p.reject(err); //**** MAKE THIS RETURN AN HTTP ERROR/500
p.resolve(html);
});
return p.promise;
}
});
//** if the controller implements an initialize method, call it now
_.isFunction(inst.initialize) && inst.initialize.call(inst);
return inst;
}.bind(impl, name)
});
//** return the ioc so that if this is being included in a ioc.path() registration, it doesn't immediately resolve
return ioc;
},
repository: function(name, model, deps, impl) {
if(!name || !model || !impl) return;
name = name.toLowerCase(); //** normalize the dependency name; case is ignored
//** if we've seen this repo before, return
if(ioc.contains(name)) return ioc(name);
//** resolve the model if given a name, 'Models.Customer'
if(typeof(model) === 'string')
model = ioc.instance(model);
ioc.register(name, deps, impl, {
lifetime: ioc.lifetime.singleton,
create: function(inst) {
var mongoose = crane.mongoose(),
repo = _.extend(mongoose.Model.compile(model.modelName, model.schema, model.collection.name, mongoose.connection, mongoose), inst, {
_name: name, //** _name used internally to avoid naming conflicts, refactor this to use iocKey
model: model
});
if(inst.initialize && _.isFunction(inst.initialize))
inst.initialize.call(inst, repo);
return repo;
}
});
//** return the ioc so that if this is being included in a ioc.path() registration, it doesn't immediately resolve
return ioc;
}
});
//** Routing
//** ----
function route(handler, req, res, next) { //** simple route handler
function error(err) { _errorHandler && _errorHandler.call(this, err, req ,res) }
var p = q.defer();
//** helper to "send a response", optionally specifying the content type and status code
p.response = function(obj, status, opt) {
var type = typeof(opt) === 'string' && opt || null;
//** set the status code and content type if specified
if(status) res.statusCode = status;
if(type) res.set('content-type', type);
//** resolve the promise with the object
p.resolve(obj);
}
//** wrap a call to the response method, wrapping the message in an envelope, with a 500 error
p.error = function(message) { p.response({ message: message }, 500) }
//** used for callbacks to promises for fail state, etc
p.errorCallback = function(err) { p.error(err && err.message) }
//** call the handler for this route
util.log('[http] routing: '+ req.url);
handler.call(this, p, req, res);
//** handle the callback
p.promise
.then(function(result) {
result = result || {};
//** 1) if the result object is a string, assume its markup and send text/html to the client
if(typeof(result) == 'string') {
!res.get('content-type') && res.set('content-type', 'text/html');
res.end(result.toString());
//** 2) if the result object is an object, assume its an object literaly that needs to be serialized to json
} else {
var status = "ok";
//** set a custom status based on the status code
if(res.statusCode == 500) status = "error";
!res.get('content-type') && res.set('content-type', 'application/javascript');
res.end(JSON.stringify({ status: status, response: result }));
}
}, error)
.catch(function(a, b) {
console.log('caught');
});
}
//** parses route handlers from the given object
function parseMethods(obj, p, ctx) {
ctx = ctx||obj;
if(!p && p != '') p = obj._name||''; //** p = path
for(var m in obj) {
//** convention: anything starting with underscore is "private", dont wire up the initialize method as a route handler
if(/^_.*/.test(m) || m == 'initialize') continue;
//** if the property is an object, parse it, nesting the path; this allows /controller/nested/object/endpoint routes easily
if(typeof(obj[m]) == 'object') {
parseMethods(obj[m], (p=='' ? p : p + '/') + m, ctx);
continue;
}
if(typeof(obj[m]) !== 'function') continue;
//** if this is the "index" method, wire it up sans named endpoint
if(m == _opt.indexView) {
var frag = p != '' && _.last(p) != '/' ? p + '/?' : p;
_app.all(_opt.routePrefix + frag, _errorHandler, route.bind(obj, obj[m])); //** for now...
} else {
//**** add translation table here to translate method names before we create endpoints
//**** ie, translate obj['SomeMethod'] to obj['SomeOtherMethod'] and dont wire up 'SomeMethod'
//** create a callback for every "public" method
var frag = (p == '' ? p : p + '/') + (_.last(m) != '/' ? m + '/?' : m);
_app.all(_opt.routePrefix + frag, _errorHandler, route.bind(ctx, obj[m])); //** for now as well...
}
}
}
//** handlebars helpers
//** ----
handlebars.registerHelper('layout', function(name, opt) {
!opt && (opt = name) && (name = null);
//** get the layout template and build a context
var layout = _templates.layout[name||'default'],
ctx = layout && _.extend(this, opt, { content: opt.fn(this) });
//** either render the layout with the block text, or just render the block text
return layout && layout.call(this, ctx) || opt.fn(this);
});
handlebars.registerHelper('template', function(p, opt) {
!opt && (opt = p) && (p = null);
//** load and cache the template if we haven't seen it before
var template = _templates[p];
if(!template) {
var temp = fs.readFileSync(_app.settings.views + p + (p.indexOf('.') == -1 ? '.hndl' : ''), 'utf8'); //**** weak test for extension, fix this
//temp && (_templates[path] = template = handlebars.compile(temp)); //**** disable caching until we use config to toggle it
temp && (template = handlebars.compile(temp));
}
//** render the template with the block content
return template
? template.call(this, _.extend(this, opt, { content: opt.fn(this) }))
: opt.fn(this);
});