UNPKG

6.93 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * Lightweight web framework
5 * @module Crixalis
6 */
7
8var Route = require('./route'),
9 Context = require('./context'),
10 events = require('events'),
11 plugins = ['request', 'static', 'compression', 'shortcuts', 'access', 'core'];
12
13/**
14 * @class Crixalis
15 * @constructor
16 */
17function Crixalis () {
18 if (!(this instanceof Crixalis)) {
19 return new Crixalis();
20 }
21
22 var self = this,
23 routes = [],
24 views = Object.create(null),
25 features = Object.create(null);
26
27 /**
28 * Extend Crixalis instance with property, method, view or feature
29 * @method define
30 * @param {String} name Property name
31 * @param {Any} value Property value
32 * @param {Object} descriptor Descriptor for Object.defineProperty()
33 * @chainable
34 */
35 function define (name, value, descriptor) {
36 var target = self;
37
38 descriptor = descriptor || {};
39 descriptor.value = value || {};
40 descriptor.enumerable = typeof value !== 'function';
41
42 if (/^(view|feature)::(\w+)$/.exec(name)) {
43 name = RegExp.$2;
44 target = { view: views, feature: features }[RegExp.$1];
45 }
46
47 Object.defineProperty(target, name, descriptor);
48
49 return this;
50 }
51
52 define('define', define);
53
54 /**
55 * Load plugin
56 * @method plugin
57 * @param {String} plugin Plugin name
58 * @param {Object} options Options for plugin loader
59 * @chainable
60 */
61 this.define('plugin', function (plugin, options) {
62 var loader;
63
64 if (~plugins.indexOf(plugin)) {
65 loader = require('./plugins/' + plugin);
66 } else {
67 loader = require(plugin);
68 }
69
70 if (typeof loader !== 'function') {
71 throw new Error('Plugin returned unexpected value');
72 }
73
74 loader.call(this, options);
75
76 return this;
77 });
78
79 /**
80 * Create new route and add to controller
81 * @method route
82 * @param {String|RegExp} source Route source
83 * @param {Object} [options] Route options
84 * @param {Function} callback Route destination
85 * @chainable
86 */
87 this.define('route', function (source, options, callback) {
88 if (arguments.length == 2) {
89 callback = options,
90 options = undefined;
91 }
92
93 if (typeof callback !== 'function') {
94 throw new Error('Callback must be a function');
95 }
96
97 routes.push([transformRoute(new Route(source, options)), callback]);
98
99 return this;
100 });
101
102 /**
103 * Select matching route. Method should be called in __auto__ event handler.
104 *
105 * Crixalis.on('auto', function () {
106 * console.log(this.method + ' ' + this.url);
107 *
108 * this.select();
109 * });
110 *
111 * @method select
112 */
113 this.define('select', function () {
114 var i, l;
115
116 for (i = 0, l = routes.length; i < l; i++) {
117 /* Route matches */
118 if (routes[i][0].match(this)) {
119 /* Execute route callback */
120 if (routes[i][1].call(this) === false) {
121 /* Go to next route */
122 continue;
123 }
124
125 return;
126 }
127 }
128
129 /* Nothing matched */
130 this.error(404);
131 });
132
133 /**
134 * Transform context using selected view and send data to client
135 * @method render
136 * @param {String} [view] View name
137 */
138 this.define('render', function (view) {
139 var fn = views[view || 'auto'];
140
141 if (typeof fn === 'function') {
142 if (fn.call(this) === false) {
143 /* Async view */
144 return;
145 }
146 } else {
147 throw new Error('Unsupported view ' + view);
148 }
149
150 if (this.body && this.has('compression')) {
151 /**
152 * Emitted when compression should be applied to response data
153 * @event compression
154 */
155 this.emit('compression');
156 } else {
157 this.sendResponse();
158 }
159 });
160
161 /**
162 * Create new Context with provided request and response objects
163 * @method createContext
164 * @param {Object} request
165 * @param {Object} response
166 * @return {Context}
167 */
168 this.define('createContext', function (request, response) {
169 Context.prototype = this;
170
171 return new Context(request, response);
172 });
173
174 /**
175 * Check if feature is present
176 * @method has
177 * @return {Boolean}
178 */
179 this.define('has', function (feature) {
180 return feature in features;
181 });
182
183 /**
184 * Fired when context is ready for route selection
185 * @event auto
186 */
187 this.on('auto', function () {
188 this.select()
189 });
190
191 /**
192 * Fired when error occured
193 * @event error
194 * @param {Error} error Object with error details
195 */
196 this.on('error', function (error) {
197 var types = this.types,
198 i, l;
199
200 this.logger.error(error);
201
202 for (i = 0, l = types.length; i < l; i++) {
203 switch (types[i]) {
204 case 'application/json':
205 this.stash.json = {
206 error: {
207 message: error.message
208 }
209 };
210 break;
211
212 case '*/*':
213 case 'text/*':
214 case 'text/html':
215 this.body = error.message;
216 break;
217
218 default:
219 continue;
220 }
221
222 /* Type found */
223 break;
224 }
225
226 if (i === l) {
227 /* Not acceptable, treat as html */
228 this.body = error.message;
229 }
230
231 this.code = this.code || 500;
232 this.render();
233 });
234
235 /* Remove default actions on newListener event */
236 setupDefaultListeners(this, ['auto', 'error']);
237
238 /* Load core methods */
239 this.plugin('core');
240
241 return this;
242}
243
244Crixalis.prototype = Object.create(events.EventEmitter.prototype, {
245 constructor: { value: Crixalis },
246
247 /**
248 * Crixalis version
249 * @property version
250 * @type String
251 */
252 version: { value: require('../package').version, enumerable: true },
253
254 /**
255 * Server signature, visible in X-Powered-By header
256 * @property signature
257 * @type String
258 * @default Crixalis
259 */
260 signature: { value: 'Crixalis', enumerable: true, writable: true },
261
262 /**
263 * Maximum POST body size in bytes
264 * @property postSize
265 * @type Number
266 * @default 1048576
267 */
268 postSize: { value: 1048576, enumerable: true, writable: true },
269
270 /**
271 * Default logger
272 * @property logger
273 * @type Object
274 */
275 logger: { value: console, enumerable: true, writable: true },
276
277 /**
278 * Response code
279 * @property code
280 * @type Number
281 */
282 code: { value: 0, enumerable: true, writable: true },
283
284 /**
285 * Response body
286 * @property body
287 * @type String
288 */
289 body: { value: '', enumerable: true, writable: true }
290});
291
292function setupDefaultListeners (emitter, events) {
293 events.forEach(function (defaultEvent) {
294 function handleNewListener (event) {
295 if (defaultEvent === event) {
296 emitter.removeListener(event, (emitter.listeners(event))[0]);
297 emitter.removeListener('newListener', handleNewListener);
298 }
299 }
300
301 emitter.on('newListener', handleNewListener);
302 });
303}
304
305function transformRoute (route, callback) {
306 var i = 1;
307
308 if (route.url) {
309 route.mapping = Object.create(null);
310
311 route.pattern = new RegExp('^' + route.url.split(/([:]\w+|[*])/).map(function (part) {
312 if (part[0] === ':') {
313 route.mapping['$' + i++] = part.slice(1);
314
315 return '([^/.]+)';
316 } else if (part === '*') {
317 return '(?:.+)';
318 }
319
320 return part.replace(/[\/()\[\]{}?^$+*.,]/g, function (char) { return '\\' + char });
321 }).join('') + '$', 'i');
322
323 route.unset('url');
324 }
325
326 if (!(route.pattern instanceof RegExp)) {
327 throw new Error('Nothing to route');
328 }
329
330 return route;
331}
332
333module.exports = Crixalis;