waffle
Version:
シンプルなWEBアプリケーションフレームワークです。(ALL YOUR NODE ARE BELONG TO US)
539 lines (492 loc) • 17.5 kB
JavaScript
/*
* Copyright 2012 Katsunori Koyanagi
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
/**
* @overview アプリケーションの動作設定に関する機能を提供します。
*/
"use strict";
var lang = require("../utils/Lang");
var app = require("../Waffle");
/**
* レンダラに関する情報の管理を行います。
* <p>
* レンダラはrenderに関数を持つオブジェクトでなければなりません。
* レンダラ関数には引数に、コンテキスト、パラメータ、データ、レンダリング処理の完了を通知するためのコールバック関数が渡されます。
* レンダリング処理が完了した時に、必ずコールバック関数を引数なしで呼び出してください。
* </p>
* <p>
* フレームワークへ組み込み済みのレンダラは、Renderersの要素に含まれます。
* </p>
*
* @see Renderers
* @see Context
* @class レンダラに関する情報の管理を行います。
* @constructor
*/
var RendererConfig = function() {
this.__renderers__ = {};
};
//
// レンダリング設定クラスのプロトタイプ設定
//
/**
* レンダラの設定を登録します。
* <p>
* prefixはejsやjsonなどの文字列を指定します。この文字列は、レンダラが要求された時に、スキームもしくはパスの拡張子に該当します。
* 大文字・小文字の区別はされません。nullもしくは空文字列が指定された場合は、デフォルトのレンダラとして登録されます。
* </p>
* <p>
* rendererには、Waffle.renderersに属するメンバを指定することができます。rendererにnullが指定された場合は何も行われません。
* </p>
*
* @example <code>
* config.renderer.register("ejs", waffle.renderers.ejs);
* config.renderer.register(waffle.renderers.json);
* </code>
*
* @param {String}
* prefix レンダラを示す文字列(省略可)
* @param {Function}
* renderer レンダラ
*/
RendererConfig.prototype.register = function(prefix, renderer) {
if (arguments.length === 1) {
renderer = prefix;
prefix = "";
}
if (!renderer) {
return;
}
if (prefix) {
prefix = prefix.toLowerCase();
} else {
prefix = "";
}
this.__renderers__[prefix] = renderer;
};
/**
* 指定のレンダラを取得して返します。
* <p>
* 該当するレンダラが存在しない場合は、デフォルト指定のレンダラもしくはJSONのレンダラが返されます。
* このメソッドは通常、フレームワーク内部で利用されます。アプリケーションが使用する必要はありません。
* </p>
*
* @param {String}
* prefix レンダラを示す文字列
* @return {Function} レンダラ
*/
RendererConfig.prototype.getRenderer = function(prefix) {
if (prefix) {
prefix = prefix.toLowerCase();
} else {
prefix = "";
}
return this.__renderers__[prefix] || this.__renderers__[""]
|| app.renderers.json;
};
/**
* フィルタに関する情報の管理を行います。
* <p>
* フィルタはpreとpostにそれぞれ関数を持つオブジェクトでなければなりません。
* preはコントローラへ処理を移譲する前、postはコントローラへ処理を移譲した後に呼び出される関数です。
* それぞれが欠落している場合は、そのフェーズの処理はスキップされます。
* </p>
* <p>
* フィルタ関数は、引数にコンテキストとフィルタ処理の完了を通知するためのコールバック関数が渡されます。
* フィルタ処理が完了した時に、必ずコールバック関数を引数なしで呼び出してください。
* </p>
* <p>
* フレームワークへ組み込み済みのレンダラは、{@link Filters}の要素に含まれます。
* </p>
*
* @see Filters
* @see Context
* @class フィルタに関する情報の管理を行います。
* @constructor
*/
var FilterConfig = function() {
this.__rules__ = [];
};
//
// フィルタ設定クラスのプロトタイプ設定
//
(function() {
function add(condition, before, filter) {
if (!condition) {
return;
}
var rule = {
before : before,
filter : filter
};
if (lang.isFunction(condition)) {
rule.regexp = false;
} else if (!lang.isRegExp(condition)) {
rule.regexp = true;
condition = new RegExp("^" + condition + "$", "g");
}
rule.condition = condition;
this.__rules__.push(rule);
}
/**
* リクエストのフィルタをフィルタリストの先頭に追加します。
*
* @param {String|RegExp|Function}
* condition
* マッチングの条件、関数を指定する場合は、引数にパスを受け取り、戻り値がBooleanもしくはフィルタを返す関数である必要があります。
* @param {Function}
* filter フィルタ、マッチングの条件にフィルタを直接返す関数を指定した場合は省略可
*/
FilterConfig.prototype.insertFirst = function(condition, filter) {
add.call(this, condition, true, filter);
};
/**
* リクエストのフィルタをフィルタリストの後ろに追加します。
*
* @param {String|RegExp|Function}
* condition
* マッチングの条件、関数を指定する場合は、引数にパスを受け取り、戻り値がBooleanもしくはフィルタを返す関数である必要があります。
* @param {Function}
* filter フィルタ、マッチングの条件にフィルタを直接返す関数を指定した場合は省略可
*/
FilterConfig.prototype.add = function(condition, filter) {
add.call(this, condition, false, filter);
};
/**
* 指定のパスに該当するフィルタを検索して返します。
* <p>
* パスに対する条件のマッチングは以下のルールで行われます。
* </p>
* <table>
* <tr>
* <th>条件が文字列もしくは正規表現の場合</th>
* <td>正規表現でパスがマッチする場合、指定されたフィルタを使用</td>
* </tr>
* <tr>
* <th>条件がBooleanを戻り値とする関数の場合</th>
* <td>関数にパスを渡した結果の戻り値がtrueの場合、指定されたフィルタを使用</td>
* </tr>
* <tr>
* <th>条件がフィルタを戻り値とする関数の場合</th>
* <td>関数にパスを渡した結果がnullでなければ、そのフィルタを使用</td>
* </tr>
* </table>
*
* @param {String}
* path リクエストパス
*
* @param {Array}
* filters フィルタ関数の配列
*
* @return {Array} フィルタ関数の配列。filtersにnullが指定され、フィルタが見つかった場合は新しい配列が返され、
* 見つからない場合はnullが返されます。 その他の場合は、filtersに指定された配列のインスタンスがそのまま返されます。
*
*/
FilterConfig.prototype.findFilters = function(path, filters) {
for ( var i = 0, j = this.__rules__.length; i < j; i++) {
var rule = this.__rules__[i];
var condition = rule.condition;
var before = rule.before;
var filter = rule.filter;
var regexp = rule.regexp;
var match;
if (!regexp) {
match = condition(path);
if (match === false) {
continue;
} else if (lang.isFunction(match)) {
filter = match;
}
if (filter != null) {
if (filters == null) {
filters = [];
}
before ? filters.unshift(filter) : filters.push(filter);
}
continue;
}
condition.lastIndex = 0;
if (filter == null || !condition.test(path)) {
continue;
}
if (filters == null) {
filters = [];
}
before ? filters.unshift(filter) : filters.push(filter);
}
return filters;
};
})();
/**
* ルーティングに関する情報の管理を行います。
* <p>
* ルーティング機能によって静的もしくは動的に、コントローラとリクエストパスやエラーコードが対応付けられます。
* コントローラは引数にコンテキストを受け取る関数でなければなりません。 ビジネスロジックを実行し、コンテキストにレスポンスに関する指示を送る必要があります。
* </p>
* <p>
* リクエストをコミットするために、コンテキストの呼びださなれけばならない関数は以下のとおりです。
* </p>
* <table>
* <tr>
* <th>{@link Context#redirect()}</th>
* <td>レスポンスにLocationヘッダを出力して、別のページへリダイレクトします。</td>
* </tr>
* <tr>
* <th>{@link Context#dispatch()}</th>
* <td>別のコントローラへ移譲します。</td>
* </tr>
* <tr>
* <th>{@link Context#render()}</th>
* <td>HTMLやXMLなどのレンダリング(レスポンスの出力)を行います。</td>
* </tr>
* <tr>
* <th>{@link Context#error()}</th>
* <td>エラーコントローラへの移譲を行います。</td>
* </tr>
* </table>
*
* @see Context
* @class ルーティングに関する情報の管理を行います。
* @constructor
*/
var RouterConfig = function() {
this.__rules__ = [];
};
//
// ルーティング設定クラスのプロトタイプ設定
//
/**
* ルーティングのルールを追加します。
* <p>
* ルーティングの条件には以下の内容を設定することができます。
* </p>
* <table>
* <tr>
* <th>Number</th>
* <td>エラーコードが一致する場合、指定のコントローラが使用される</td>
* </tr>
* <tr>
* <th>String</th>
* <td>正規表現にマッチした場合、、指定のコントローラが使用される</td>
* </tr>
* <tr>
* <th>RegExp</th>
* <td>正規表現にマッチした場合、、指定のコントローラが使用される</td>
* </tr>
* <tr>
* <th>Function(Booleanを返す関数)</th>
* <td>渡されたリクエストパスもしくはエラーコードを評価して、trueが返された場合に、指定のコントローラが使用される</td>
* </tr>
* <tr>
* <th>Function(コントローラを返す関数)</th>
* <td>渡されたリクエストパスもしくはエラーコードを評価して、StringもしくはFunctionが返されれた時に、その値をコントローラとして使用する</td>
* </tr>
* </table>
* <p>
* コントローラはStringもしくはFunction型である必要があります。ただし、コントローラを返す関数をルーティングの条件とした場合は省略可能です。
* コントローラをStringで指定した場合の動作は以下のとおりになります。
* </p>
* <table>
* <tr>
* <th>パスのみの場合(/controller/foo/bar)</th>
* <td>アプリケーションルート以下からパスの内容に基づき、{@link Waffle#require()}によってコントローラの関数を取得します。</td>
* </tr>
* <tr>
* <th>パスにメソッドの指定が付与される場合(/controller/foo/bar#buzz)</th>
* <td>上記と同じ方法でコントローラの取得が行われますが、取得されるのは関数ではなくオブジェクトであり、
* オブジェクトから#以降の文字列で関数が検索されます。見つからない場合はindexという名前にフォールバックされます。 </td>
* </tr>
* </table>
* <p>
* 正規表現によるマッチが行われ、またコントローラが文字列の場合、 コントローラに指定された$1などの文字列は、マッチした内容に置き換えられます。
* マッチ結果は{@link Context#matches}に保存されます。以下は例になります。
* </p>
* <table>
* <tr>
* <th>condition</th>
* <th>controller</th>
* <th>RegExp</th>
* <th>リクエストパス</th>
* <th>結果</th>
* </tr>
* <tr>
* <td>"/foo/bar/([a-z]+)"</td>
* <td>"/controller/foo/bar#$1"</td>
* <td>/^\/foo\/bar\/([a-z]+)$/g</td>
* <td>/foo/bar/buzz</td>
* <td>"/controller/foo/bar#buzz"</td>
* </tr>
* </table>
*
* @param {String|RegExp|Function|Number}
* condition ルーティングの条件
* @param {String|Function}
* controller コントローラを指し示す文字列、もしくはコントローラ関数
*/
RouterConfig.prototype.on = function(condition, controller) {
this.__rules__.push(new RouterRule(condition, controller));
};
/**
* コントローラを取得して返します。
* <p>
* エラーコードに該当するエラーページのコントローラを取得する場合は、引数のパラメータにエラーコードを指定してください。
* その他は、リクエストパスを指定してください。
* </p>
*
* @param {Waffle}
* app アプリケーション
* @param {Context}
* context コンテキスト
* @param {Number|String}
* parameter パラメータ
* @return {Function} コントローラ
*/
RouterConfig.prototype.getController = function(app, context, parameter) {
for ( var i = 0, j = this.__rules__.length; i < j; i++) {
var rule = this.__rules__[i];
var controller = rule.condition(parameter, context);
if (controller) {
if (lang.isFunction(controller)) {
return controller;
}
controller = controller.toString();
var method = null;
var idx = controller.lastIndexOf("#");
if (idx > -1) {
method = controller.substring(idx + 1);
controller = controller.substring(0, idx);
}
var c, c2;
try {
c = app.require(controller);
if (method) {
c2 = c[method];
if (lang.isFunction(c2)) {
return c2;
}
c2 = c[app.descriptor.defaultControllerMethod || "index"];
if (lang.isFunction(c2)) {
return c2;
}
} else {
if (lang.isFunction(c)) {
return c;
}
}
} catch (e) {
}
}
}
return null;
};
/**
* ルータのルールクラス
*
* @private
*/
function RouterRule(condition, controller) {
var cf = lang.isFunction(controller);
if (lang.isNumber(condition)) {
if (!controller) {
return;
}
if (!cf) {
controller = controller.toString();
}
/** @private */
this.condition = function(parameter, context) {
if (!lang.isNumber(parameter)) {
return null;
}
if (parameter !== condition) {
return null;
}
return controller;
};
return;
}
if (lang.isFunction(condition)) {
/** @private */
this.condition = function(parameter, context) {
var result = condition(parameter, context);
if (result === true) {
return controller;
} else if (result === false) {
return null;
}
return result;
};
return;
}
if (condition) {
if (!controller) {
return;
}
if (!cf) {
controller = controller.toString();
}
if (!lang.isRegExp(condition)) {
condition = new RegExp("^" + condition.toString() + "$", "g");
}
/** @private */
this.condition = function(parameter, context) {
if (lang.isNumber(parameter)) {
return null;
}
condition.lastIndex = 0;
if (!condition.test(parameter)) {
return null;
}
condition.lastIndex = 0;
context.matches = condition.exec(parameter);
if (cf) {
return controller;
}
return parameter.replace(condition, controller);
};
}
}
/**
* @private
*/
RouterRule.prototype.condition = function() {
return null;
};
/**
* フレームワークの基本動作の定義を管理します。
*
* @class フレームワークの基本動作の定義を管理します。
* @constructor
* @property {Waffle} app アプリケーションのインスタンス
* @property {RendererConfig} renderer レンダリング設定のインスタンス
* @property {FilterConfig} filter レンダリング設定のインスタンス
* @property {RouterConfig} router ルーティング設定のインスタンス
* @param {Waffle}
* app アプリケーションのインスタンス
*/
var Config = function(app) {
this.app = app;
this.filter = new FilterConfig();
this.renderer = new RendererConfig();
this.router = new RouterConfig();
var Waffle = app.constructor;
for(var name in Waffle.renderers){
this.renderer.register(name, Waffle.renderers[name]);
}
};
//
// expose
//
module.exports = Config;