Conflicts:

lib/http_router/version.rb
	test/common/recognize.txt
This commit is contained in:
Josh Hull 2011-07-16 13:10:14 -07:00
parent 1c4f5d88c5
commit cdc73f391a
25 changed files with 2734 additions and 305 deletions

View File

@ -1,9 +1,33 @@
# encoding: utf-8
require 'bundler'
Bundler::GemHelper.install_tasks
Rake::Task['release'].enhance([:test, :release_js])
task :release_js do
$: << 'lib'
require 'http_router/version'
File.open('js/package.json', 'w') do |f|
f << <<-EOT
{
"name": "http_router",
"description": "URL routing and generation in js",
"author": "Joshua Hull <joshbuddy@gmail.com>",
"version": "#{HttpRouter::VERSION}",
"directories": {
"lib" : "./lib/http_router"
},
"main": "lib/http_router"
}
EOT
end
sh "cd js && npm publish"
sh "git commit js/package.json -m'bumped js version'"
end
test_tasks = ['test:generation', 'test:recognition', 'test:integration', 'test:examples', 'test:rdoc_examples']
test_tasks << 'test:js' if `which coffee && which node` && $?.success?
desc "Run all tests"
task :test => ['test:generation', 'test:recognition', 'test:integration', 'test:examples', 'test:rdoc_examples']
task :test => test_tasks
require 'pp'
@ -22,6 +46,13 @@ namespace :test do
Dir['./test/**/test_*.rb'].each { |test| require test }
end
desc "Run js tests"
task :js do
sh "coffee -c js/test/test.coffee"
sh "coffee -c js/lib/http_router.coffee"
sh "node js/test/test.js"
end
desc "Run generic recognition tests"
task :recognition do
$: << 'lib'

368
js/lib/http_router.coffee Normal file
View File

@ -0,0 +1,368 @@
root.Sherpa = class Sherpa
constructor: (@callback) ->
@root = new Node()
@routes = {}
match: (httpRequest, httpResponse) ->
request = if (httpRequest.url?) then new Request(httpRequest) else new PathRequest(httpRequest)
@root.match(request)
if request.destinations.length > 0
new Response(request, httpResponse).invoke()
else if @callback?
@callback(request.underlyingRequest)
findSubparts: (part) ->
subparts = []
while match = part.match(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)
part = part.slice(match.index, part.length)
subparts.push part.slice(0, match[0].length)
part = part.slice(match[0].length, part.length)
subparts
generatePaths: (path) ->
[paths, chars, startIndex, endIndex] = [[''], path.split(''), 0, 1]
for charIndex in [0...chars.length]
c = chars[charIndex]
switch c
when '\\'
# do nothing ...
charIndex++
add = if chars[charIndex] == ')' or chars[charIndex] == '('
chars[charIndex]
else
"\\#{chars[charIndex]}"
paths[pathIndex] += add for pathIndex in [startIndex...endIndex]
when '('
# over current working set, double paths
paths.push(paths[pathIndex]) for pathIndex in [startIndex...endIndex]
# move working set to newly copied paths
startIndex = endIndex
endIndex = paths.length
when ')'
startIndex -= endIndex - startIndex
else
paths[pathIndex] += c for pathIndex in [startIndex...endIndex]
paths.reverse()
paths
url: (name, params) ->
@routes[name]?.url(params)
addComplexPart: (subparts, compiledPath, matchesWith, variableNames) ->
escapeRegexp = (str) -> str.replace(/([\.*+?^=!:${}()|[\]\/\\])/g, '\\$1')
[capturingIndicies, splittingIndicies, captures, spans] = [[], [], 0, false]
regexSubparts = for part in subparts
switch part[0]
when '\\'
compiledPath.push "'#{part[1]}'"
escapeRegexp(part[1])
when ':', '*'
spans = true if part[0] == '*'
captures += 1
name = part.slice(1, part.length)
variableNames.push(name)
if part[0] == '*'
splittingIndicies.push(captures)
compiledPath.push "params['#{name}'].join('/')"
else
capturingIndicies.push(captures)
compiledPath.push "params['#{name}']"
if spans
if matchesWith[name]? then "((?:#{matchesWith[name].source}\\/?)+)" else '(.*?)'
else
"(#{(matchesWith[name]?.source || '[^/]*?')})"
else
compiledPath.push "'#{part}'"
escapeRegexp(part)
regexp = new RegExp("#{regexSubparts.join('')}$")
if spans
new SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies)
else
new RegexMatcher(regexp, capturingIndicies, splittingIndicies)
addSimplePart: (subparts, compiledPath, matchesWith, variableNames) ->
part = subparts[0]
switch part[0]
when ':'
variableName = part.slice(1, part.length)
compiledPath.push "params['#{variableName}']"
variableNames.push(variableName)
if matchesWith[variableName]? then new SpanningRegexMatcher(matchesWith[variableName], [0], []) else new Variable()
when '*'
compiledPath.push "params['#{variableName}'].join('/')"
variableName = part.slice(1, part.length)
variableNames.push(variableName)
new Glob(matchesWith[variableName])
else
compiledPath.push "'#{part}'"
new Lookup(part)
add: (rawPath, opts) ->
matchesWith = opts?.matchesWith || {}
defaults = opts?.default || {}
routeName = opts?.name
partiallyMatch = false
route = if rawPath.exec?
new Route([@root.add(new RegexPath(@root, rawPath))])
else
if rawPath.substring(rawPath.length - 1) == '*'
rawPath = rawPath.substring(0, rawPath.length - 1)
partiallyMatch = true
pathSet = for path in @generatePaths(rawPath)
node = @root
variableNames = []
parts = path.split('/')
compiledPath = []
for part in parts
unless part == ''
compiledPath.push "'/'"
subparts = @findSubparts(part)
nextNodeFn = if subparts.length == 1 then @addSimplePart else @addComplexPart
node = node.add(nextNodeFn(subparts, compiledPath, matchesWith, variableNames))
if opts?.conditions?
node = node.add(new RequestMatcher(opts.conditions))
path = new Path(node, variableNames)
path.partial = partiallyMatch
path.compiled = if compiledPath.length == 0 then "'/'" else compiledPath.join('+')
path
new Route(pathSet, matchesWith)
route.default = defaults
route.name = routeName
@routes[routeName] = route if routeName?
route
class Response
constructor: (@request, @httpResponse, @position) ->
@position ||= 0
next: ->
if @position == @destinations.length - 1
false
else
new Response(@request, @httpResponse, @position + 1).invoke()
invoke: ->
req = if typeof(@request.underlyingRequest) == 'string' then {} else @request.underlyingRequest
req.params = @request.destinations[@position].params
req.route = @request.destinations[@position].route
req.pathInfo = @request.destinations[@position].pathInfo
@request.destinations[@position].route.destination(req, @httpResponse)
class Node
constructor: ->
@type ||= 'node'
@matchers = []
add: (n) ->
@matchers.push(n) if !@matchers[@matchers.length - 1]?.usable(n)
@matchers[@matchers.length - 1].use(n)
usable: (n) -> n.type == @type
match: (request) ->
m.match(request) for m in @matchers
superMatch: Node::match
use: (n) -> this
class Lookup extends Node
constructor: (part) ->
@part = part
@type = 'lookup'
@map = {}
super
match: (request) ->
if @map[request.path[0]]?
request = request.clone()
part = request.path.shift()
@map[part].match(request)
use: (n) ->
@map[n.part] ||= new Node()
@map[n.part]
class Variable extends Node
constructor: ->
@type ||= 'variable'
super
match: (request) ->
if request.path.length > 0
request = request.clone()
request.variables.push(request.path.shift())
super(request)
class Glob extends Variable
constructor: (@regexp) ->
@type = 'glob'
super
match: (request) ->
if request.path.length > 0
original_request = request
cloned_path = request.path.slice(0, request.path)
for i in [1..original_request.path.length]
request = original_request.clone()
match = request.path[i - 1].match(@regexp) if @regexp?
return if @regexp? and (!match? or match[0].length != request.path[i - 1].length)
request.variables.push(request.path.slice(0, i))
request.path = request.path.slice(i, request.path.length)
@superMatch(request)
class RegexMatcher extends Node
constructor: (@regexp, @capturingIndicies, @splittingIndicies) ->
@type ||= 'regex'
@varIndicies = []
@varIndicies[i] = [i, 'split'] for i in @splittingIndicies
@varIndicies[i] = [i, 'capture'] for i in @capturingIndicies
@varIndicies.sort (a, b) -> a[0] - b[0]
super
match: (request) ->
if request.path[0]? and match = request.path[0].match(@regexp)
return unless match[0].length == request.path[0].length
request = request.clone()
@addVariables(request, match)
request.path.shift()
super(request)
addVariables: (request, match) ->
for v in @varIndicies when v?
idx = v[0]
type = v[1]
switch type
when 'split' then request.variables.push match[idx].split('/')
when 'capture' then request.variables.push match[idx]
usable: (n) ->
n.type == @type && n.regexp == @regexp && n.capturingIndicies == @capturingIndicies && n.splittingIndicies == @splittingIndicies
class SpanningRegexMatcher extends RegexMatcher
constructor: (@regexp, @capturingIndicies, @splittingIndicies) ->
@type = 'spanning'
super
match: (request) ->
if request.path.length > 0
wholePath = request.wholePath()
if match = wholePath.match(@regexp)
return unless match.index == 0
request = request.clone()
@addVariables(request, match)
request.path = request.splitPath(wholePath.slice(match.index + match[0].length, wholePath.length))
@superMatch(request)
class RequestMatcher extends Node
constructor: (@conditions) ->
@type = 'request'
super
match: (request) ->
conditionCount = 0
satisfiedConditionCount = 0
for type, matcher of @conditions
val = request.underlyingRequest[type]
conditionCount++
v = if matcher instanceof Array
matching = ->
for cond in matcher
if cond.exec?
return true if matcher.exec(val)
else
return true if cond == val
false
matching()
else
if matcher.exec? then matcher.exec(val) else matcher == val
satisfiedConditionCount++ if v
if conditionCount == satisfiedConditionCount
super(request)
usable: (n) ->
n.type == @type && n.conditions == @conditions
class Path extends Node
constructor: (@parent, @variableNames) ->
@type = 'path'
@partial = false
addDestination: (request) -> request.destinations.push({route: @route, request: request, params: @constructParams(request)})
match: (request) ->
if @partial or request.path.length == 0
@addDestination(request)
if @partial
request.destinations[request.destinations.length - 1].pathInfo = "/#{request.wholePath()}"
constructParams: (request) ->
params = {}
for i in [0...@variableNames.length]
params[@variableNames[i]] = request.variables[i]
params
url: (rawParams) ->
rawParams = {} unless rawParams?
params = {}
for key in @variableNames
params[key] = if @route.default? then rawParams[key] || @route.default[key] else rawParams[key]
return undefined if !params[key]?
for name in @variableNames
if @route.matchesWith[name]?
match = params[name].match(@route.matchesWith[name])
return undefined unless match? && match[0].length == params[name].length
path = if @compiled == '' then '' else eval(@compiled)
if path?
delete rawParams[name] for name in @variableNames
path
class RegexPath extends Path
constructor: (@parent, @regexp) ->
@type = 'regexp_route'
super
match: (request) ->
request.regexpRouteMatch = @regexp.exec(request.decodedPath())
if request.regexpRouteMatch? && request.regexpRouteMatch[0].length == request.decodedPath().length
request = request.clone()
request.path = []
super(request)
constructParams: (request) -> request.regexpRouteMatch
url: (rawParams) -> throw("This route cannot be generated")
class Route
constructor: (@pathSet, @matchesWith) ->
path.route = this for path in @pathSet
to: (@destination) ->
path.parent.add(path) for path in @pathSet
generateQuery: (params, base, query) ->
query = ""
base ||= ""
if params?
if params instanceof Array
for idx in [0...(params.length)]
query += @generateQuery(params[idx], "#{base}[]")
else if params instanceof Object
for k,v of params
query += @generateQuery(v, if base == '' then k else "#{base}[#{k}]")
else
query += encodeURIComponent(base).replace(/%20/g, '+')
query += '='
query += encodeURIComponent(params).replace(/%20/g, '+')
query += '&'
query
url: (params) ->
path = undefined
for pathObj in @pathSet
path = pathObj.url(params)
break if path?
if path?
query = @generateQuery(params)
joiner = if query != '' then '?' else ''
"#{encodeURI(path)}#{joiner}#{query.substr(0, query.length - 1)}"
else
undefined
class Request
constructor: (@underlyingRequest, @callback) ->
@variables = []
@destinations = []
if @underlyingRequest?
@path = @splitPath()
toString: -> "<Request path: /#{@path.join('/') } #{@path.length}>"
wholePath: -> @path.join('/')
decodedPath: (path) ->
unless path?
path = require('url').parse(@underlyingRequest.url).pathname
decodeURI(path)
splitPath: (path) ->
decodedPath = @decodedPath(path)
splitPath = if decodedPath == '/' then [] else decodedPath.split('/')
splitPath.shift() if splitPath[0] == ''
splitPath
clone: ->
c = new Request()
c.path = @path.slice(0, @path.length)
c.variables = @variables.slice(0, @variables.length)
c.underlyingRequest = @underlyingRequest
c.callback = @callback
c.destinations = @destinations
c
class PathRequest extends Request
decodedPath: (path) ->
unless path?
path = @underlyingRequest
decodeURI(path)

668
js/lib/http_router.js Normal file
View File

@ -0,0 +1,668 @@
(function() {
var Sherpa;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
};
root.Sherpa = Sherpa = (function() {
var Glob, Lookup, Node, Path, PathRequest, RegexMatcher, RegexPath, Request, RequestMatcher, Response, Route, SpanningRegexMatcher, Variable;
function Sherpa(callback) {
this.callback = callback;
this.root = new Node();
this.routes = {};
}
Sherpa.prototype.match = function(httpRequest, httpResponse) {
var request;
request = (httpRequest.url != null) ? new Request(httpRequest) : new PathRequest(httpRequest);
this.root.match(request);
if (request.destinations.length > 0) {
return new Response(request, httpResponse).invoke();
} else if (this.callback != null) {
return this.callback(request.underlyingRequest);
}
};
Sherpa.prototype.findSubparts = function(part) {
var match, subparts;
subparts = [];
while (match = part.match(/\\.|[:*][a-z0-9_]+|[^:*\\]+/)) {
part = part.slice(match.index, part.length);
subparts.push(part.slice(0, match[0].length));
part = part.slice(match[0].length, part.length);
}
return subparts;
};
Sherpa.prototype.generatePaths = function(path) {
var add, c, charIndex, chars, endIndex, pathIndex, paths, startIndex, _ref, _ref2;
_ref = [[''], path.split(''), 0, 1], paths = _ref[0], chars = _ref[1], startIndex = _ref[2], endIndex = _ref[3];
for (charIndex = 0, _ref2 = chars.length; 0 <= _ref2 ? charIndex < _ref2 : charIndex > _ref2; 0 <= _ref2 ? charIndex++ : charIndex--) {
c = chars[charIndex];
switch (c) {
case '\\':
charIndex++;
add = chars[charIndex] === ')' || chars[charIndex] === '(' ? chars[charIndex] : "\\" + chars[charIndex];
for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) {
paths[pathIndex] += add;
}
break;
case '(':
for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) {
paths.push(paths[pathIndex]);
}
startIndex = endIndex;
endIndex = paths.length;
break;
case ')':
startIndex -= endIndex - startIndex;
break;
default:
for (pathIndex = startIndex; startIndex <= endIndex ? pathIndex < endIndex : pathIndex > endIndex; startIndex <= endIndex ? pathIndex++ : pathIndex--) {
paths[pathIndex] += c;
}
}
}
paths.reverse();
return paths;
};
Sherpa.prototype.url = function(name, params) {
var _ref;
return (_ref = this.routes[name]) != null ? _ref.url(params) : void 0;
};
Sherpa.prototype.addComplexPart = function(subparts, compiledPath, matchesWith, variableNames) {
var captures, capturingIndicies, escapeRegexp, name, part, regexSubparts, regexp, spans, splittingIndicies, _ref;
escapeRegexp = function(str) {
return str.replace(/([\.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
_ref = [[], [], 0, false], capturingIndicies = _ref[0], splittingIndicies = _ref[1], captures = _ref[2], spans = _ref[3];
regexSubparts = (function() {
var _i, _len, _results;
_results = [];
for (_i = 0, _len = subparts.length; _i < _len; _i++) {
part = subparts[_i];
_results.push((function() {
var _ref2;
switch (part[0]) {
case '\\':
compiledPath.push("'" + part[1] + "'");
return escapeRegexp(part[1]);
case ':':
case '*':
if (part[0] === '*') {
spans = true;
}
captures += 1;
name = part.slice(1, part.length);
variableNames.push(name);
if (part[0] === '*') {
splittingIndicies.push(captures);
compiledPath.push("params['" + name + "'].join('/')");
} else {
capturingIndicies.push(captures);
compiledPath.push("params['" + name + "']");
}
if (spans) {
if (matchesWith[name] != null) {
return "((?:" + matchesWith[name].source + "\\/?)+)";
} else {
return '(.*?)';
}
} else {
return "(" + (((_ref2 = matchesWith[name]) != null ? _ref2.source : void 0) || '[^/]*?') + ")";
}
break;
default:
compiledPath.push("'" + part + "'");
return escapeRegexp(part);
}
})());
}
return _results;
})();
regexp = new RegExp("" + (regexSubparts.join('')) + "$");
if (spans) {
return new SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies);
} else {
return new RegexMatcher(regexp, capturingIndicies, splittingIndicies);
}
};
Sherpa.prototype.addSimplePart = function(subparts, compiledPath, matchesWith, variableNames) {
var part, variableName;
part = subparts[0];
switch (part[0]) {
case ':':
variableName = part.slice(1, part.length);
compiledPath.push("params['" + variableName + "']");
variableNames.push(variableName);
if (matchesWith[variableName] != null) {
return new SpanningRegexMatcher(matchesWith[variableName], [0], []);
} else {
return new Variable();
}
break;
case '*':
compiledPath.push("params['" + variableName + "'].join('/')");
variableName = part.slice(1, part.length);
variableNames.push(variableName);
return new Glob(matchesWith[variableName]);
default:
compiledPath.push("'" + part + "'");
return new Lookup(part);
}
};
Sherpa.prototype.add = function(rawPath, opts) {
var compiledPath, defaults, matchesWith, nextNodeFn, node, part, partiallyMatch, parts, path, pathSet, route, routeName, subparts, variableNames;
matchesWith = (opts != null ? opts.matchesWith : void 0) || {};
defaults = (opts != null ? opts["default"] : void 0) || {};
routeName = opts != null ? opts.name : void 0;
partiallyMatch = false;
route = rawPath.exec != null ? new Route([this.root.add(new RegexPath(this.root, rawPath))]) : (rawPath.substring(rawPath.length - 1) === '*' ? (rawPath = rawPath.substring(0, rawPath.length - 1), partiallyMatch = true) : void 0, pathSet = (function() {
var _i, _j, _len, _len2, _ref, _results;
_ref = this.generatePaths(rawPath);
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
path = _ref[_i];
node = this.root;
variableNames = [];
parts = path.split('/');
compiledPath = [];
for (_j = 0, _len2 = parts.length; _j < _len2; _j++) {
part = parts[_j];
if (part !== '') {
compiledPath.push("'/'");
subparts = this.findSubparts(part);
nextNodeFn = subparts.length === 1 ? this.addSimplePart : this.addComplexPart;
node = node.add(nextNodeFn(subparts, compiledPath, matchesWith, variableNames));
}
}
if ((opts != null ? opts.conditions : void 0) != null) {
node = node.add(new RequestMatcher(opts.conditions));
}
path = new Path(node, variableNames);
path.partial = partiallyMatch;
path.compiled = compiledPath.length === 0 ? "'/'" : compiledPath.join('+');
_results.push(path);
}
return _results;
}).call(this), new Route(pathSet, matchesWith));
route["default"] = defaults;
route.name = routeName;
if (routeName != null) {
this.routes[routeName] = route;
}
return route;
};
Response = (function() {
function Response(request, httpResponse, position) {
this.request = request;
this.httpResponse = httpResponse;
this.position = position;
this.position || (this.position = 0);
}
Response.prototype.next = function() {
if (this.position === this.destinations.length - 1) {
return false;
} else {
return new Response(this.request, this.httpResponse, this.position + 1).invoke();
}
};
Response.prototype.invoke = function() {
var req;
req = typeof this.request.underlyingRequest === 'string' ? {} : this.request.underlyingRequest;
req.params = this.request.destinations[this.position].params;
req.route = this.request.destinations[this.position].route;
req.pathInfo = this.request.destinations[this.position].pathInfo;
return this.request.destinations[this.position].route.destination(req, this.httpResponse);
};
return Response;
})();
Node = (function() {
function Node() {
this.type || (this.type = 'node');
this.matchers = [];
}
Node.prototype.add = function(n) {
var _ref;
if (!((_ref = this.matchers[this.matchers.length - 1]) != null ? _ref.usable(n) : void 0)) {
this.matchers.push(n);
}
return this.matchers[this.matchers.length - 1].use(n);
};
Node.prototype.usable = function(n) {
return n.type === this.type;
};
Node.prototype.match = function(request) {
var m, _i, _len, _ref, _results;
_ref = this.matchers;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
m = _ref[_i];
_results.push(m.match(request));
}
return _results;
};
Node.prototype.superMatch = Node.prototype.match;
Node.prototype.use = function(n) {
return this;
};
return Node;
})();
Lookup = (function() {
__extends(Lookup, Node);
function Lookup(part) {
this.part = part;
this.type = 'lookup';
this.map = {};
Lookup.__super__.constructor.apply(this, arguments);
}
Lookup.prototype.match = function(request) {
var part;
if (this.map[request.path[0]] != null) {
request = request.clone();
part = request.path.shift();
return this.map[part].match(request);
}
};
Lookup.prototype.use = function(n) {
var _base, _name;
(_base = this.map)[_name = n.part] || (_base[_name] = new Node());
return this.map[n.part];
};
return Lookup;
})();
Variable = (function() {
__extends(Variable, Node);
function Variable() {
this.type || (this.type = 'variable');
Variable.__super__.constructor.apply(this, arguments);
}
Variable.prototype.match = function(request) {
if (request.path.length > 0) {
request = request.clone();
request.variables.push(request.path.shift());
return Variable.__super__.match.call(this, request);
}
};
return Variable;
})();
Glob = (function() {
__extends(Glob, Variable);
function Glob(regexp) {
this.regexp = regexp;
this.type = 'glob';
Glob.__super__.constructor.apply(this, arguments);
}
Glob.prototype.match = function(request) {
var cloned_path, i, match, original_request, _ref, _results;
if (request.path.length > 0) {
original_request = request;
cloned_path = request.path.slice(0, request.path);
_results = [];
for (i = 1, _ref = original_request.path.length; 1 <= _ref ? i <= _ref : i >= _ref; 1 <= _ref ? i++ : i--) {
request = original_request.clone();
if (this.regexp != null) {
match = request.path[i - 1].match(this.regexp);
}
if ((this.regexp != null) && (!(match != null) || match[0].length !== request.path[i - 1].length)) {
return;
}
request.variables.push(request.path.slice(0, i));
request.path = request.path.slice(i, request.path.length);
_results.push(this.superMatch(request));
}
return _results;
}
};
return Glob;
})();
RegexMatcher = (function() {
__extends(RegexMatcher, Node);
function RegexMatcher(regexp, capturingIndicies, splittingIndicies) {
var i, _i, _j, _len, _len2, _ref, _ref2;
this.regexp = regexp;
this.capturingIndicies = capturingIndicies;
this.splittingIndicies = splittingIndicies;
this.type || (this.type = 'regex');
this.varIndicies = [];
_ref = this.splittingIndicies;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
i = _ref[_i];
this.varIndicies[i] = [i, 'split'];
}
_ref2 = this.capturingIndicies;
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
i = _ref2[_j];
this.varIndicies[i] = [i, 'capture'];
}
this.varIndicies.sort(function(a, b) {
return a[0] - b[0];
});
RegexMatcher.__super__.constructor.apply(this, arguments);
}
RegexMatcher.prototype.match = function(request) {
var match;
if ((request.path[0] != null) && (match = request.path[0].match(this.regexp))) {
if (match[0].length !== request.path[0].length) {
return;
}
request = request.clone();
this.addVariables(request, match);
request.path.shift();
return RegexMatcher.__super__.match.call(this, request);
}
};
RegexMatcher.prototype.addVariables = function(request, match) {
var idx, type, v, _i, _len, _ref, _results;
_ref = this.varIndicies;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
v = _ref[_i];
if (v != null) {
idx = v[0];
type = v[1];
_results.push((function() {
switch (type) {
case 'split':
return request.variables.push(match[idx].split('/'));
case 'capture':
return request.variables.push(match[idx]);
}
})());
}
}
return _results;
};
RegexMatcher.prototype.usable = function(n) {
return n.type === this.type && n.regexp === this.regexp && n.capturingIndicies === this.capturingIndicies && n.splittingIndicies === this.splittingIndicies;
};
return RegexMatcher;
})();
SpanningRegexMatcher = (function() {
__extends(SpanningRegexMatcher, RegexMatcher);
function SpanningRegexMatcher(regexp, capturingIndicies, splittingIndicies) {
this.regexp = regexp;
this.capturingIndicies = capturingIndicies;
this.splittingIndicies = splittingIndicies;
this.type = 'spanning';
SpanningRegexMatcher.__super__.constructor.apply(this, arguments);
}
SpanningRegexMatcher.prototype.match = function(request) {
var match, wholePath;
if (request.path.length > 0) {
wholePath = request.wholePath();
if (match = wholePath.match(this.regexp)) {
if (match.index !== 0) {
return;
}
request = request.clone();
this.addVariables(request, match);
request.path = request.splitPath(wholePath.slice(match.index + match[0].length, wholePath.length));
return this.superMatch(request);
}
}
};
return SpanningRegexMatcher;
})();
RequestMatcher = (function() {
__extends(RequestMatcher, Node);
function RequestMatcher(conditions) {
this.conditions = conditions;
this.type = 'request';
RequestMatcher.__super__.constructor.apply(this, arguments);
}
RequestMatcher.prototype.match = function(request) {
var conditionCount, matcher, matching, satisfiedConditionCount, type, v, val, _ref;
conditionCount = 0;
satisfiedConditionCount = 0;
_ref = this.conditions;
for (type in _ref) {
matcher = _ref[type];
val = request.underlyingRequest[type];
conditionCount++;
v = matcher instanceof Array ? (matching = function() {
var cond, _i, _len;
for (_i = 0, _len = matcher.length; _i < _len; _i++) {
cond = matcher[_i];
if (cond.exec != null) {
if (matcher.exec(val)) {
return true;
}
} else {
if (cond === val) {
return true;
}
}
}
return false;
}, matching()) : matcher.exec != null ? matcher.exec(val) : matcher === val;
if (v) {
satisfiedConditionCount++;
}
}
if (conditionCount === satisfiedConditionCount) {
return RequestMatcher.__super__.match.call(this, request);
}
};
RequestMatcher.prototype.usable = function(n) {
return n.type === this.type && n.conditions === this.conditions;
};
return RequestMatcher;
})();
Path = (function() {
__extends(Path, Node);
function Path(parent, variableNames) {
this.parent = parent;
this.variableNames = variableNames;
this.type = 'path';
this.partial = false;
}
Path.prototype.addDestination = function(request) {
return request.destinations.push({
route: this.route,
request: request,
params: this.constructParams(request)
});
};
Path.prototype.match = function(request) {
if (this.partial || request.path.length === 0) {
this.addDestination(request);
if (this.partial) {
return request.destinations[request.destinations.length - 1].pathInfo = "/" + (request.wholePath());
}
}
};
Path.prototype.constructParams = function(request) {
var i, params, _ref;
params = {};
for (i = 0, _ref = this.variableNames.length; 0 <= _ref ? i < _ref : i > _ref; 0 <= _ref ? i++ : i--) {
params[this.variableNames[i]] = request.variables[i];
}
return params;
};
Path.prototype.url = function(rawParams) {
var key, match, name, params, path, _i, _j, _k, _len, _len2, _len3, _ref, _ref2, _ref3;
if (rawParams == null) {
rawParams = {};
}
params = {};
_ref = this.variableNames;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
key = _ref[_i];
params[key] = this.route["default"] != null ? rawParams[key] || this.route["default"][key] : rawParams[key];
if (!(params[key] != null)) {
return;
}
}
_ref2 = this.variableNames;
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
name = _ref2[_j];
if (this.route.matchesWith[name] != null) {
match = params[name].match(this.route.matchesWith[name]);
if (!((match != null) && match[0].length === params[name].length)) {
return;
}
}
}
path = this.compiled === '' ? '' : eval(this.compiled);
if (path != null) {
_ref3 = this.variableNames;
for (_k = 0, _len3 = _ref3.length; _k < _len3; _k++) {
name = _ref3[_k];
delete rawParams[name];
}
return path;
}
};
return Path;
})();
RegexPath = (function() {
__extends(RegexPath, Path);
function RegexPath(parent, regexp) {
this.parent = parent;
this.regexp = regexp;
this.type = 'regexp_route';
RegexPath.__super__.constructor.apply(this, arguments);
}
RegexPath.prototype.match = function(request) {
request.regexpRouteMatch = this.regexp.exec(request.decodedPath());
if ((request.regexpRouteMatch != null) && request.regexpRouteMatch[0].length === request.decodedPath().length) {
request = request.clone();
request.path = [];
return RegexPath.__super__.match.call(this, request);
}
};
RegexPath.prototype.constructParams = function(request) {
return request.regexpRouteMatch;
};
RegexPath.prototype.url = function(rawParams) {
throw "This route cannot be generated";
};
return RegexPath;
})();
Route = (function() {
function Route(pathSet, matchesWith) {
var path, _i, _len, _ref;
this.pathSet = pathSet;
this.matchesWith = matchesWith;
_ref = this.pathSet;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
path = _ref[_i];
path.route = this;
}
}
Route.prototype.to = function(destination) {
var path, _i, _len, _ref, _results;
this.destination = destination;
_ref = this.pathSet;
_results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
path = _ref[_i];
_results.push(path.parent.add(path));
}
return _results;
};
Route.prototype.generateQuery = function(params, base, query) {
var idx, k, v, _ref;
query = "";
base || (base = "");
if (params != null) {
if (params instanceof Array) {
for (idx = 0, _ref = params.length; 0 <= _ref ? idx < _ref : idx > _ref; 0 <= _ref ? idx++ : idx--) {
query += this.generateQuery(params[idx], "" + base + "[]");
}
} else if (params instanceof Object) {
for (k in params) {
v = params[k];
query += this.generateQuery(v, base === '' ? k : "" + base + "[" + k + "]");
}
} else {
query += encodeURIComponent(base).replace(/%20/g, '+');
query += '=';
query += encodeURIComponent(params).replace(/%20/g, '+');
query += '&';
}
}
return query;
};
Route.prototype.url = function(params) {
var joiner, path, pathObj, query, _i, _len, _ref;
path = void 0;
_ref = this.pathSet;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
pathObj = _ref[_i];
path = pathObj.url(params);
if (path != null) {
break;
}
}
if (path != null) {
query = this.generateQuery(params);
joiner = query !== '' ? '?' : '';
return "" + (encodeURI(path)) + joiner + (query.substr(0, query.length - 1));
} else {
return;
}
};
return Route;
})();
Request = (function() {
function Request(underlyingRequest, callback) {
this.underlyingRequest = underlyingRequest;
this.callback = callback;
this.variables = [];
this.destinations = [];
if (this.underlyingRequest != null) {
this.path = this.splitPath();
}
}
Request.prototype.toString = function() {
return "<Request path: /" + (this.path.join('/')) + " " + this.path.length + ">";
};
Request.prototype.wholePath = function() {
return this.path.join('/');
};
Request.prototype.decodedPath = function(path) {
if (path == null) {
path = require('url').parse(this.underlyingRequest.url).pathname;
}
return decodeURI(path);
};
Request.prototype.splitPath = function(path) {
var decodedPath, splitPath;
decodedPath = this.decodedPath(path);
splitPath = decodedPath === '/' ? [] : decodedPath.split('/');
if (splitPath[0] === '') {
splitPath.shift();
}
return splitPath;
};
Request.prototype.clone = function() {
var c;
c = new Request();
c.path = this.path.slice(0, this.path.length);
c.variables = this.variables.slice(0, this.variables.length);
c.underlyingRequest = this.underlyingRequest;
c.callback = this.callback;
c.destinations = this.destinations;
return c;
};
return Request;
})();
PathRequest = (function() {
__extends(PathRequest, Request);
function PathRequest() {
PathRequest.__super__.constructor.apply(this, arguments);
}
PathRequest.prototype.decodedPath = function(path) {
if (path == null) {
path = this.underlyingRequest;
}
return decodeURI(path);
};
return PathRequest;
})();
return Sherpa;
})();
}).call(this);

10
js/package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "http_router",
"description": "URL routing and generation in js",
"author": "Joshua Hull <joshbuddy@gmail.com>",
"version": "0.0.0",
"directories": {
"lib" : "./lib/http_router"
},
"main": "lib/http_router"
}

View File

@ -0,0 +1,100 @@
# About
Simple test framework for asynchronous testing in [Node.js](http://nodejs.org/). It's trying to be as simple and explicit as possible. No magic, no wheel reinventing. Just use minitest for building your tests and the [assert library](http://nodejs.org/api.html#assert-212) for the actual helpers for testing equality etc.
This is how the output looks like:
![Minitest.js output](http://github.com/botanicus/minitest.js/raw/master/minitest.png)
# Setup
* `require()` minitest
* Use `minitest.setupListeners()` for listening on the `uncaughtException` and `exit` events.
* Use `minitest.context(description, block)` for defining your contexts. Context will be usually a function or object name.
* Use `#<a Context>.assertion(description, block)` for defining your assertions.
* Use `#<a Test>.finished()` to mark test as finished. All the tests has to have it. Without this you won't be able to write solid asynchronous tests, because you can't ask only "is a and b the same?", but also "did the callback run?".
* Run `node foo_test.js` to get the results.
# Example
var minitest = require("minitest");
var assert = require("assert");
minitest.setupListeners();
minitest.context("Context#setup()", function () {
this.setup(function () {
this.user = {name: "Jakub"};
});
this.assertion("it should setup listeners", function (test) {
// test something via the standard assert module
assert.ok(this.user)
// mark test as finished
test.finished();
});
this.assertion("it should be able to count", function (test) {
if (2 !== 4) {
// manually fail the test
throw new Error("You can't count, can you?");
};
});
});
## Formatters
If you don't like minitest output, you can simply override following methods:
* `Context.prototype.contextHeader()`
* `Test.prototype.reportSuccess()`
* `Test.prototype.reportError(error)`
* `Test.prototype.reportNotRun()`
All this methods are supposed to return a string and all these methods have access to `this.description`.
# Common Problems in Testing Asynchronous Code
## Exceptions in Callbacks
Obviously you can't catch errors which occured in callbacks. Consider following:
try {
db.get("botanicus", function (user) {
throw new Error("You can't catch me!");
});
} catch(error) {
// you'll never get in here
};
## Testing Exceptions
this.assertion("should throw an error", function (test) {
assert.throws(function () {
throw new Error("Error occured!");
test.finished();
});
});
This obviously can't work, because exception interrupts the anonymous function we are passing as an argument for `assert.throws()`.
this.assertion("should throw an error", function (test) {
assert.throws(function () {
throw new Error("Error occured!");
});
test.finished();
});
This is better, it will at least work, but what if there will be an error in the `assert.throws()` function and it doesn't call the anonymous function?
this.assertion("should throw an error", function (test) {
assert.throws(function () {
test.finished();
throw new Error("Error occured!");
});
});
OK, this is better, `test.finished()` doesn't jump out of the test, so in case that the assertion will fail, we will get the proper result. However it's not perfect, because I can change `test.finished()` in future to actually jump out of the function (I probably won't do that but you can't know) plus if there would be a bug, so `test.finished()` would cause an exception, it would satisfy `assert.throws()` without actually testing the code. Well, you'd probably noticed in other tests, but still.
Fortunatelly you can specify error class and expected message for `assert.throws()` in this order: `assert.throws(block, error, message)`.

View File

@ -0,0 +1,9 @@
- Fix the unicode issues with Node.js 0.1.90. It does work with Node.js 0.1.33, but in 0.1.90 it just prints first two lines (context description and first successful message) and then don't output anything (but the code is still running and we are even able to inspect these messages which should be outputted via sys.p()).
Fix this:
context("foo", function () {
var test = true;
this.assertion("bar", function (test) {
sys.p(test); // undefined, which is because of the magic with call
});
});

View File

@ -0,0 +1,35 @@
/*
Simple module for coloured output for POSIX shells.
@example
colours.bold.green + "OK: " + colours.reset + description
*/
colours = {
reset: "\x1B[0m",
grey: "\x1B[0;30m",
red: "\x1B[0;31m",
green: "\x1B[0;32m",
yellow: "\x1B[0;33m",
blue: "\x1B[0;34m",
magenta: "\x1B[0;35m",
cyan: "\x1B[0;36m",
white: "\x1B[0;37m",
bold: {
grey: "\x1B[1;30m",
red: "\x1B[1;31m",
green: "\x1B[1;32m",
yellow: "\x1B[1;33m",
blue: "\x1B[1;34m",
magenta: "\x1B[1;35m",
cyan: "\x1B[1;36m",
white: "\x1B[1;37m",
}
};
// exports
for (colour in colours) {
exports[colour] = colours[colour];
};

View File

@ -0,0 +1,24 @@
#!/usr/bin/env node
var assert = require("assert");
var minitest = require("./minitest");
// setup listeners
minitest.setupListeners();
// tests
minitest.context("Minitest.js", function () {
this.assertion("it should succeed", function (test) {
assert.ok(true);
test.finished();
});
this.assertion("it should fail", function (test) {
assert.ok(null);
test.finished();
});
this.assertion("it should not be finished", function (test) {
assert.ok(true);
});
});

View File

@ -0,0 +1,158 @@
var sys = require("sys");
var colours = require("./colours");
/* suite */
function Suite () {
this.contexts = [];
};
Suite.prototype.report = function () {
var suite = this;
this.contexts.forEach(function(context, index) {
sys.puts(context.contextHeader());
context.report();
if (suite.contexts.length === index) {
sys.puts("");
};
});
};
Suite.prototype.register = function (context) {
this.contexts.push(context);
};
// there is only one suite instance
var suite = exports.suite = new Suite();
/* context */
function Context (description, block) {
this.tests = [];
this.block = block;
this.description = description;
};
Context.prototype.run = function () {
this.block.call(this);
};
Context.prototype.register = function (test) {
this.tests.push(test);
};
Context.prototype.report = function () {
this.tests.forEach(function (test) {
test.report();
});
};
/* test */
function Test (description, block, setupBlock) {
this.description = description;
this.block = block;
this.setupBlock = setupBlock;
};
Test.prototype.run = function () {
try {
if (this.setupBlock) {
this.setupBlock.call(this);
};
this.block.call(this, this);
} catch(error) {
this.failed(error);
};
};
Test.prototype.finished = function () {
this.result = this.reportSuccess();
};
Test.prototype.failed = function (error) {
this.result = this.reportError(error);
};
Test.prototype.report = function () {
if (this.result) {
sys.puts(this.result);
} else {
sys.puts(this.reportNotFinished());
};
};
/* output formatters */
Context.prototype.contextHeader = function () {
return colours.bold.yellow + "[= " + this.description + " =]" + colours.reset;
};
Test.prototype.reportSuccess = function () {
// return colours.bold.green + " ✔ OK: " + colours.reset + this.description;
return colours.bold.green + " OK: " + colours.reset + this.description;
};
Test.prototype.reportError = function (error) {
var stack = error.stack.replace(/^/, " ");
// return colours.bold.red + " ✖ Error: " + colours.reset + this.description + "\n" + stack;
return colours.bold.red + " Error: " + colours.reset + this.description + "\n" + stack;
};
Test.prototype.reportNotFinished = function () {
// return colours.bold.magenta + " ✖ Didn't finished: " + colours.reset + this.description;
return colours.bold.magenta + " Didn't finished: " + colours.reset + this.description;
};
/* DSL */
function context (description, block) {
var context = new Context(description, block);
suite.register(context);
context.run();
};
/*
Run an example and print if it was successful or not.
@example
minitest.context("setup()", function () {
this.assertion("Default value should be 0", function (test) {
assert.equal(value, 0);
test.finished();
});
});
*/
Context.prototype.assertion = function (description, block) {
var test = new Test(description, block, this.setupBlock);
this.register(test);
test.run();
};
Context.prototype.setup = function (block) {
this.setupBlock = block;
};
function runAtExit () {
process.addListener("exit", function () {
suite.report();
});
};
function setupUncaughtExceptionListener () {
// TODO: is there any way how to get the test instance,
// so we could just set test.result, so everything would be
// reported properly on the correct place, not in the middle of tests
process.addListener("uncaughtException", function (error) {
sys.puts(Test.prototype.reportError(error));
});
};
function setupListeners () {
setupUncaughtExceptionListener();
runAtExit();
};
/* exports */
exports.Context = Context;
exports.Test = Test;
exports.context = context;
exports.runAtExit = runAtExit;
exports.setupUncaughtExceptionListener = setupUncaughtExceptionListener;
exports.setupListeners = setupListeners;

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

View File

@ -0,0 +1,31 @@
#!/usr/bin/env node
var assert = require("assert");
var minitest = require("./minitest");
// setup listeners
minitest.setupListeners();
// tests
minitest.context("Namespacing", function () {
this.setup(function () {
this.user = {name: "Jakub"};
});
this.assertion("instance variable from setup() should exist in assertion", function () {
assert.ok(this.user);
this.finished();
});
var foobar = true;
this.assertion("local variable from context of the current context should exist in assertion", function () {
assert.ok(foobar);
this.finished();
});
this.foobaz = true;
this.assertion("instance variable from context of the current context should not exist in assertion", function () {
assert.equal(undefined, this.foobaz);
this.finished();
});
});

5
js/spec/run.sh Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
coffee -c spec/spec_generate.coffee && node spec/spec_generate.js
coffee -c spec/spec_recognize.coffee && node spec/spec_recognize.js

View File

@ -0,0 +1,59 @@
util = require 'util'
require '../lib/sherpa'
minitest = require('./minitest.js/minitest')
minitest.setupListeners()
assert = require('assert')
minitest.context "Sherpa#generate()", ->
@setup ->
@router = new Sherpa()
@assertion "should generate a simple route", (test) ->
@router.add('/test', name: 'simple')
assert.equal('/test', @router.url('simple'))
test.finished()
@assertion "should generate a route with a variable in it", (test) ->
@router.add('/:test', name:'with_variable')
assert.equal('/var', @router.url('with_variable', {test: 'var'}))
test.finished()
@assertion "should generate a route with a regex variable in it", (test) ->
@router.add('/:test', matchesWith: {test: /asd|qwe|\d+/}, name: 'with_variable')
assert.equal(undefined, @router.url('with_variable', {test: 'variable'}))
assert.equal(undefined, @router.url('with_variable', {test: '123qwe'}))
assert.equal('/123', @router.url('with_variable', {test: '123'}))
assert.equal('/qwe', @router.url('with_variable', {test: 'qwe'}))
assert.equal('/asd', @router.url('with_variable', {test: 'asd'}))
test.finished()
@assertion "should generate a route with a optionals in it", (test) ->
@router.add('/(:test)', name:'with_optional')
assert.equal('/', @router.url('with_optional'))
assert.equal('/hello', @router.url('with_optional', test: 'hello'))
test.finished()
@assertion "should generate a route with nested optionals in it", (test) ->
@router.add('/(:test(/:test2))', name: 'with_optional')
assert.equal('/', @router.url('with_optional'))
assert.equal('/hello', @router.url('with_optional', {test: 'hello'}))
assert.equal('/hello/world', @router.url('with_optional', {test: 'hello', test2: 'world'}))
assert.equal('/?test2=hello', @router.url('with_optional', {test2: 'hello'}))
test.finished();
@assertion "should generate extra params as a query string after", (test) ->
@router.add('/:test', matchesWith: {test: /asd|qwe|\d+/},name:'with_variable')
assert.equal('/123?foo=bar', @router.url('with_variable', {test: '123', foo: 'bar'}))
test.finished();
@assertion "should escape values in the URI", (test) ->
@router.add('/:test', name: 'with_variable')
assert.equal('/%5B%20%5D+=-', @router.url('with_variable', {test: '[ ]+=-'}))
test.finished()
@assertion "should escape values in the query string", (test) ->
@router.add('/', name:'simple')
assert.equal('/?test+and+more=%5B+%5D%2B%3D-', @router.url('simple', {"test and more": '[ ]+=-'}))
test.finished()

111
js/spec/spec_generate.js Normal file
View File

@ -0,0 +1,111 @@
(function() {
var assert, minitest, util;
util = require('util');
require('../lib/sherpa');
minitest = require('./minitest.js/minitest');
minitest.setupListeners();
assert = require('assert');
minitest.context("Sherpa#generate()", function() {
this.setup(function() {
return this.router = new Sherpa();
});
this.assertion("should generate a simple route", function(test) {
this.router.add('/test', {
name: 'simple'
});
assert.equal('/test', this.router.url('simple'));
return test.finished();
});
this.assertion("should generate a route with a variable in it", function(test) {
this.router.add('/:test', {
name: 'with_variable'
});
assert.equal('/var', this.router.url('with_variable', {
test: 'var'
}));
return test.finished();
});
this.assertion("should generate a route with a regex variable in it", function(test) {
this.router.add('/:test', {
matchesWith: {
test: /asd|qwe|\d+/
},
name: 'with_variable'
});
assert.equal(void 0, this.router.url('with_variable', {
test: 'variable'
}));
assert.equal(void 0, this.router.url('with_variable', {
test: '123qwe'
}));
assert.equal('/123', this.router.url('with_variable', {
test: '123'
}));
assert.equal('/qwe', this.router.url('with_variable', {
test: 'qwe'
}));
assert.equal('/asd', this.router.url('with_variable', {
test: 'asd'
}));
return test.finished();
});
this.assertion("should generate a route with a optionals in it", function(test) {
this.router.add('/(:test)', {
name: 'with_optional'
});
assert.equal('/', this.router.url('with_optional'));
assert.equal('/hello', this.router.url('with_optional', {
test: 'hello'
}));
return test.finished();
});
this.assertion("should generate a route with nested optionals in it", function(test) {
this.router.add('/(:test(/:test2))', {
name: 'with_optional'
});
assert.equal('/', this.router.url('with_optional'));
assert.equal('/hello', this.router.url('with_optional', {
test: 'hello'
}));
assert.equal('/hello/world', this.router.url('with_optional', {
test: 'hello',
test2: 'world'
}));
assert.equal('/?test2=hello', this.router.url('with_optional', {
test2: 'hello'
}));
return test.finished();
});
this.assertion("should generate extra params as a query string after", function(test) {
this.router.add('/:test', {
matchesWith: {
test: /asd|qwe|\d+/
},
name: 'with_variable'
});
assert.equal('/123?foo=bar', this.router.url('with_variable', {
test: '123',
foo: 'bar'
}));
return test.finished();
});
this.assertion("should escape values in the URI", function(test) {
this.router.add('/:test', {
name: 'with_variable'
});
assert.equal('/%5B%20%5D+=-', this.router.url('with_variable', {
test: '[ ]+=-'
}));
return test.finished();
});
return this.assertion("should escape values in the query string", function(test) {
this.router.add('/', {
name: 'simple'
});
assert.equal('/?test+and+more=%5B+%5D%2B%3D-', this.router.url('simple', {
"test and more": '[ ]+=-'
}));
return test.finished();
});
});
}).call(this);

View File

@ -0,0 +1,163 @@
util = require 'util'
require '../lib/sherpa'
minitest = require('./minitest.js/minitest')
minitest.setupListeners()
assert = require('assert')
minitest.context "Sherpa#recognize()", ->
@setup ->
@router = new Sherpa()
@assertion "should recognize a simple route", (test) ->
@router.add('/').to (req, params) ->
assert.deepEqual {}, params
test.finished()
@router.match url: '/'
@assertion "should recognize another simple route ", (test) ->
@router.add('/test').to (req, params) ->
assert.deepEqual {}, params
test.finished()
@router.match url: '/test'
@assertion "should recognize a route with a variable", (test) ->
@router.add('/:test').to (req, params) ->
assert.deepEqual test: 'variable', params
test.finished()
@router.match url: '/variable'
@assertion "should recognize a route with an interstitial variable", (test) ->
@router.add('/test-:test-test').to (req, params) ->
assert.deepEqual {test: 'variable'}, params
test.finished()
@router.match url: '/test-variable-test'
@assertion "should recognize a route with a variable at the end of the path", (test) ->
@router.add('/test/:test').to (req, params) ->
assert.deepEqual {test: 'variable'}, params
test.finished()
@router.match url: '/test/variable'
@assertion "should recognize a simple route with optionals", (test) ->
count = 0
@router.add('/(test)').to (req, params) ->
assert.deepEqual {}, params
test.finished() if count == 1
count++
@router.match url: '/'
@router.match url: '/test'
@assertion "should recognize a route based on a request method", (test) ->
routes = []
@router.add('/test', conditions: {method: 'GET'}).to (req, params) ->
assert.deepEqual {}, params
routes.push 'get'
@router.add('/test', conditions: {method: 'POST'}).to (req, params) ->
assert.deepEqual {}, params
routes.push 'post'
@router.add('/test').to (req, params) ->
assert.deepEqual {}, params
assert.deepEqual ['get', 'post'], routes
routes.push 'post'
test.finished();
@router.match url: '/test', method: 'GET'
@router.match url: '/test', method: 'POST'
@router.match url: '/test', method: 'PUT'
@assertion "should recognize a simple route with nested optionals", (test) ->
urls = []
@router.callback = ->
assert.deepEqual ['/test', '/test/test2', '/test/test2/test3'], urls
test.finished()
@router.add('/test(/test2(/test3))').to (req, params) ->
assert.deepEqual {}, params
urls.push req.url
@router.match url:'/test'
@router.match url:'/test/test2'
@router.match url:'/test/test2/test3'
@router.match url:'/test/test3'
@assertion "should recognize a route based on multiple request keys", (test) ->
routes = []
@router.add('/test', conditions: {method: 'GET', scheme: 'http' }).to -> routes.push('http-get')
@router.add('/test', conditions: {method: 'POST', scheme: 'http' }).to -> routes.push('http-post')
@router.add('/test', conditions: {method: 'POST', scheme: 'https'}).to -> routes.push('https-post')
@router.add('/test', conditions: {method: 'GET', scheme: 'https'}).to -> routes.push('https-get')
@router.add('/test', conditions: { scheme: 'http' }).to -> routes.push('http-any')
@router.add('/test', conditions: { scheme: 'https'}).to -> routes.push('https-any')
@router.callback = ->
assert.deepEqual ['http-post', 'http-get', 'http-any', 'https-get', 'https-post', 'https-any'], routes
test.finished()
@router.match url: '/test', method: 'POST', scheme: 'http'
@router.match url: '/test', method: 'GET', scheme: 'http'
@router.match url: '/test', method: 'PUT', scheme: 'http'
@router.match url: '/test', method: 'GET', scheme: 'https'
@router.match url: '/test', method: 'POST', scheme: 'https'
@router.match url: '/test', method: 'PUT', scheme: 'https'
@router.match url: '/'
@assertion "should recognize a partial route", (test) ->
@router.add('/test', partial: true).to -> test.finished()
@router.match(url:'/test/testing')
@assertion "should recognize a route with a regex variable in it", (test) ->
vars = ['123', 'qwe', 'asd']
missedCount = 0
@router.callback = -> missedCount++
@router.add('/:test', matchesWith: {test: /asd|qwe|\d+/}).to (req, params) ->
assert.equal 2, missedCount
assert.deepEqual {test: vars.shift()}, params
test.finished() if vars.length == 0
@router.match(url:'/variable')
@router.match(url:'/123qwe')
@router.match(url:'/123')
@router.match(url:'/qwe')
@router.match(url:'/asd')
@assertion "should distinguish between identical routes where one has a matchesWith", (test) ->
params = []
nonParams = []
@router.add('/:test', matchesWith: {test: /^(asd|qwe|\d+)$/}).to (req, p)->
params.push p
@router.add('/:test').to (req, p) ->
nonParams.push p
if params.length == 3 and nonParams.length == 2
assert.deepEqual [{test: '123'}, {test:'qwe'}, {test: 'asd'}], params
assert.deepEqual [{test: 'poipio'}, {test:'123asd'}], nonParams
test.finished()
@router.match url:'/123'
@router.match url:'/qwe'
@router.match url:'/asd'
@router.match url:'/poipio'
@router.match url:'/123asd'
@assertion "should recognize a route based on a request method", (test) ->
routes = []
@router.add('/test', conditions:{method: 'GET'}).to -> routes.push('get')
@router.add('/test', conditions:{method: 'POST'}).to -> routes.push('post')
@router.add('/test').to ->
assert.deepEqual(['get', 'post'], routes)
test.finished()
@router.match(url:'/test', method: 'GET')
@router.match(url:'/test', method: 'POST')
@router.match(url:'/test', method: 'PUT')
@assertion "should recognize a route based on a request method regex", (test) ->
routes = []
@router.add('/test', conditions:{method: 'DELETE'}).to -> routes.push('delete')
@router.add('/test', conditions:{method: /GET|POST/}).to -> routes.push('get-post')
@router.add('/test').to ->
assert.deepEqual ['get-post', 'get-post', 'delete'], routes
test.finished()
@router.match(url:'/test', method: 'GET')
@router.match(url:'/test', method: 'POST')
@router.match(url:'/test', method: 'DELETE')
@router.match(url:'/test', method: 'PUT')

397
js/spec/spec_recognize.js Normal file
View File

@ -0,0 +1,397 @@
(function() {
var assert, minitest, util;
util = require('util');
require('../lib/sherpa');
minitest = require('./minitest.js/minitest');
minitest.setupListeners();
assert = require('assert');
minitest.context("Sherpa#recognize()", function() {
this.setup(function() {
return this.router = new Sherpa();
});
this.assertion("should recognize a simple route", function(test) {
this.router.add('/').to(function(req, params) {
assert.deepEqual({}, params);
return test.finished();
});
return this.router.match({
url: '/'
});
});
this.assertion("should recognize another simple route ", function(test) {
this.router.add('/test').to(function(req, params) {
assert.deepEqual({}, params);
return test.finished();
});
return this.router.match({
url: '/test'
});
});
this.assertion("should recognize a route with a variable", function(test) {
this.router.add('/:test').to(function(req, params) {
assert.deepEqual({
test: 'variable'
}, params);
return test.finished();
});
return this.router.match({
url: '/variable'
});
});
this.assertion("should recognize a route with an interstitial variable", function(test) {
this.router.add('/test-:test-test').to(function(req, params) {
assert.deepEqual({
test: 'variable'
}, params);
return test.finished();
});
return this.router.match({
url: '/test-variable-test'
});
});
this.assertion("should recognize a route with a variable at the end of the path", function(test) {
this.router.add('/test/:test').to(function(req, params) {
assert.deepEqual({
test: 'variable'
}, params);
return test.finished();
});
return this.router.match({
url: '/test/variable'
});
});
this.assertion("should recognize a simple route with optionals", function(test) {
var count;
count = 0;
this.router.add('/(test)').to(function(req, params) {
assert.deepEqual({}, params);
if (count === 1) {
test.finished();
}
return count++;
});
this.router.match({
url: '/'
});
return this.router.match({
url: '/test'
});
});
this.assertion("should recognize a route based on a request method", function(test) {
var routes;
routes = [];
this.router.add('/test', {
conditions: {
method: 'GET'
}
}).to(function(req, params) {
assert.deepEqual({}, params);
return routes.push('get');
});
this.router.add('/test', {
conditions: {
method: 'POST'
}
}).to(function(req, params) {
assert.deepEqual({}, params);
return routes.push('post');
});
this.router.add('/test').to(function(req, params) {
assert.deepEqual({}, params);
assert.deepEqual(['get', 'post'], routes);
routes.push('post');
return test.finished();
});
this.router.match({
url: '/test',
method: 'GET'
});
this.router.match({
url: '/test',
method: 'POST'
});
return this.router.match({
url: '/test',
method: 'PUT'
});
});
this.assertion("should recognize a simple route with nested optionals", function(test) {
var urls;
urls = [];
this.router.callback = function() {
assert.deepEqual(['/test', '/test/test2', '/test/test2/test3'], urls);
return test.finished();
};
this.router.add('/test(/test2(/test3))').to(function(req, params) {
assert.deepEqual({}, params);
return urls.push(req.url);
});
this.router.match({
url: '/test'
});
this.router.match({
url: '/test/test2'
});
this.router.match({
url: '/test/test2/test3'
});
return this.router.match({
url: '/test/test3'
});
});
this.assertion("should recognize a route based on multiple request keys", function(test) {
var routes;
routes = [];
this.router.add('/test', {
conditions: {
method: 'GET',
scheme: 'http'
}
}).to(function() {
return routes.push('http-get');
});
this.router.add('/test', {
conditions: {
method: 'POST',
scheme: 'http'
}
}).to(function() {
return routes.push('http-post');
});
this.router.add('/test', {
conditions: {
method: 'POST',
scheme: 'https'
}
}).to(function() {
return routes.push('https-post');
});
this.router.add('/test', {
conditions: {
method: 'GET',
scheme: 'https'
}
}).to(function() {
return routes.push('https-get');
});
this.router.add('/test', {
conditions: {
scheme: 'http'
}
}).to(function() {
return routes.push('http-any');
});
this.router.add('/test', {
conditions: {
scheme: 'https'
}
}).to(function() {
return routes.push('https-any');
});
this.router.callback = function() {
assert.deepEqual(['http-post', 'http-get', 'http-any', 'https-get', 'https-post', 'https-any'], routes);
return test.finished();
};
this.router.match({
url: '/test',
method: 'POST',
scheme: 'http'
});
this.router.match({
url: '/test',
method: 'GET',
scheme: 'http'
});
this.router.match({
url: '/test',
method: 'PUT',
scheme: 'http'
});
this.router.match({
url: '/test',
method: 'GET',
scheme: 'https'
});
this.router.match({
url: '/test',
method: 'POST',
scheme: 'https'
});
this.router.match({
url: '/test',
method: 'PUT',
scheme: 'https'
});
return this.router.match({
url: '/'
});
});
this.assertion("should recognize a partial route", function(test) {
this.router.add('/test', {
partial: true
}).to(function() {
return test.finished();
});
return this.router.match({
url: '/test/testing'
});
});
this.assertion("should recognize a route with a regex variable in it", function(test) {
var missedCount, vars;
vars = ['123', 'qwe', 'asd'];
missedCount = 0;
this.router.callback = function() {
return missedCount++;
};
this.router.add('/:test', {
matchesWith: {
test: /asd|qwe|\d+/
}
}).to(function(req, params) {
assert.equal(2, missedCount);
assert.deepEqual({
test: vars.shift()
}, params);
if (vars.length === 0) {
return test.finished();
}
});
this.router.match({
url: '/variable'
});
this.router.match({
url: '/123qwe'
});
this.router.match({
url: '/123'
});
this.router.match({
url: '/qwe'
});
return this.router.match({
url: '/asd'
});
});
this.assertion("should distinguish between identical routes where one has a matchesWith", function(test) {
var nonParams, params;
params = [];
nonParams = [];
this.router.add('/:test', {
matchesWith: {
test: /^(asd|qwe|\d+)$/
}
}).to(function(req, p) {
return params.push(p);
});
this.router.add('/:test').to(function(req, p) {
nonParams.push(p);
if (params.length === 3 && nonParams.length === 2) {
assert.deepEqual([
{
test: '123'
}, {
test: 'qwe'
}, {
test: 'asd'
}
], params);
assert.deepEqual([
{
test: 'poipio'
}, {
test: '123asd'
}
], nonParams);
return test.finished();
}
});
this.router.match({
url: '/123'
});
this.router.match({
url: '/qwe'
});
this.router.match({
url: '/asd'
});
this.router.match({
url: '/poipio'
});
return this.router.match({
url: '/123asd'
});
});
this.assertion("should recognize a route based on a request method", function(test) {
var routes;
routes = [];
this.router.add('/test', {
conditions: {
method: 'GET'
}
}).to(function() {
return routes.push('get');
});
this.router.add('/test', {
conditions: {
method: 'POST'
}
}).to(function() {
return routes.push('post');
});
this.router.add('/test').to(function() {
assert.deepEqual(['get', 'post'], routes);
return test.finished();
});
this.router.match({
url: '/test',
method: 'GET'
});
this.router.match({
url: '/test',
method: 'POST'
});
return this.router.match({
url: '/test',
method: 'PUT'
});
});
return this.assertion("should recognize a route based on a request method regex", function(test) {
var routes;
routes = [];
this.router.add('/test', {
conditions: {
method: 'DELETE'
}
}).to(function() {
return routes.push('delete');
});
this.router.add('/test', {
conditions: {
method: /GET|POST/
}
}).to(function() {
return routes.push('get-post');
});
this.router.add('/test').to(function() {
assert.deepEqual(['get-post', 'get-post', 'delete'], routes);
return test.finished();
});
this.router.match({
url: '/test',
method: 'GET'
});
this.router.match({
url: '/test',
method: 'POST'
});
this.router.match({
url: '/test',
method: 'DELETE'
});
return this.router.match({
url: '/test',
method: 'PUT'
});
});
});
}).call(this);

136
js/test/test.coffee Normal file
View File

@ -0,0 +1,136 @@
require.paths.push("#{__dirname}/../lib")
fs = require('fs')
util = require('util')
sys = require('sys')
http_router = require('http_router')
assert = require('assert')
class Example
constructor: (@routes, @tests) ->
class Test
constructor: (file)->
@examples = []
contents = fs.readFileSync(file, 'utf8')
lines = contents.split(/\n/m)
currentTest = null
routes = []
tests = []
for line in lines
if line.match(/^#/)
# this is a comment, skip
else if line.match(/^\s*$/)
# empty line, skip
else if line.match(/^( |\t)/)
# this is a test
tests.push(JSON.parse(line))
else
# this is a route
if tests.length != 0
@examples.push new Example(routes, tests)
routes = []
tests = []
parsedRoutes = JSON.parse(line)
if parsedRoutes instanceof Array
for r in parsedRoutes
routes.push(r)
else
routes.push(parsedRoutes)
@examples.push new Example(routes, tests)
interpretValue: (v) ->
if v.regex? then new RegExp(v.regex) else v
invoke: -> throw("need to implement")
constructRouter: (example) ->
router = new Sherpa()
for route in example.routes
for name, vals of route
path = null
opts = {name: name}
if vals.path?
path = @interpretValue(vals.path)
delete vals.path
if vals.conditions?
conditions = {}
for k, v of vals.conditions
switch k
when 'request_method' then conditions.method = @interpretValue(v)
else conditions[k] = @interpretValue(v)
opts.conditions = conditions
delete vals.conditions
if vals.default?
opts.default = vals.default
delete vals.default
matchesWith = {}
for k, v of vals
matchesWith[k] = @interpretValue(v)
delete vals.k
opts.matchesWith = matchesWith
else
path = @interpretValue(vals)
name = "" + name
router.add(path, opts).to (req, response) ->
response.params = req.params
response.end(req.route.name)
router
class GenerationTest extends Test
constructor: -> super
invoke: ->
console.log("Running #{@examples.length} generation tests")
for example in @examples
process.stdout.write "*"
router = @constructRouter(example)
for test in example.tests
process.stdout.write "."
[expectedResult, name, params] = test
continue if params? && params instanceof Array
continue if name instanceof Object
actualResult = router.url(name, params)
assert.equal(expectedResult, actualResult)
console.log("\nDone!")
class RecognitionTest extends Test
constructor: -> super
invoke: ->
console.log("Running #{@examples.length} recognition tests")
for example in @examples
process.stdout.write "*"
router = @constructRouter(example)
for test in example.tests
mockResponse = end: (part) -> @val = part
process.stdout.write "."
[expectedRouteName, requestingPath, expectedParams] = test
mockRequest = {}
complex = false
if requestingPath.path?
complex = true
mockRequest.url = requestingPath.path
delete requestingPath.path
for k, v of requestingPath
mockRequest[k] = v
else
mockRequest.url = requestingPath
mockRequest.url = "http://host#{mockRequest.url}" unless mockRequest.url.match(/^http/)
router.match(mockRequest, mockResponse)
assert.equal(expectedRouteName, mockResponse.val)
expectedParams ||= {}
mockResponse.params ||= {}
pathInfoExcpectation = null
if expectedParams.PATH_INFO?
pathInfoExcpectation = expectedParams.PATH_INFO
delete expectedParams.PATH_INFO
assert.equal(pathInfoExcpectation, mockRequest.pathInfo) if pathInfoExcpectation
assert.deepEqual(expectedParams, mockResponse.params)
unless complex
mockResponse = end: (part) -> @val = part
router.match(requestingPath, mockResponse)
assert.equal(expectedRouteName, mockResponse.val)
expectedParams ||= {}
mockResponse.params ||= {}
assert.equal(pathInfoExcpectation, mockRequest.pathInfo) if pathInfoExcpectation
assert.deepEqual(expectedParams, mockResponse.params)
console.log("\nDone!")
new GenerationTest("#{__dirname}/../../test/common/generate.txt").invoke()
new RecognitionTest("#{__dirname}/../../test/common/recognize.txt").invoke()

229
js/test/test.js Normal file
View File

@ -0,0 +1,229 @@
(function() {
var Example, GenerationTest, RecognitionTest, Test, assert, fs, http_router, sys, util;
var __hasProp = Object.prototype.hasOwnProperty, __extends = function(child, parent) {
for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; }
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
};
require.paths.push("" + __dirname + "/../lib");
fs = require('fs');
util = require('util');
sys = require('sys');
http_router = require('http_router');
assert = require('assert');
Example = (function() {
function Example(routes, tests) {
this.routes = routes;
this.tests = tests;
}
return Example;
})();
Test = (function() {
function Test(file) {
var contents, currentTest, line, lines, parsedRoutes, r, routes, tests, _i, _j, _len, _len2;
this.examples = [];
contents = fs.readFileSync(file, 'utf8');
lines = contents.split(/\n/m);
currentTest = null;
routes = [];
tests = [];
for (_i = 0, _len = lines.length; _i < _len; _i++) {
line = lines[_i];
if (line.match(/^#/)) {} else if (line.match(/^\s*$/)) {} else if (line.match(/^( |\t)/)) {
tests.push(JSON.parse(line));
} else {
if (tests.length !== 0) {
this.examples.push(new Example(routes, tests));
routes = [];
tests = [];
}
parsedRoutes = JSON.parse(line);
if (parsedRoutes instanceof Array) {
for (_j = 0, _len2 = parsedRoutes.length; _j < _len2; _j++) {
r = parsedRoutes[_j];
routes.push(r);
}
} else {
routes.push(parsedRoutes);
}
}
}
this.examples.push(new Example(routes, tests));
}
Test.prototype.interpretValue = function(v) {
if (v.regex != null) {
return new RegExp(v.regex);
} else {
return v;
}
};
Test.prototype.invoke = function() {
throw "need to implement";
};
Test.prototype.constructRouter = function(example) {
var conditions, k, matchesWith, name, opts, path, route, router, v, vals, _i, _len, _ref, _ref2;
router = new Sherpa();
_ref = example.routes;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
route = _ref[_i];
for (name in route) {
vals = route[name];
path = null;
opts = {
name: name
};
if (vals.path != null) {
path = this.interpretValue(vals.path);
delete vals.path;
if (vals.conditions != null) {
conditions = {};
_ref2 = vals.conditions;
for (k in _ref2) {
v = _ref2[k];
switch (k) {
case 'request_method':
conditions.method = this.interpretValue(v);
break;
default:
conditions[k] = this.interpretValue(v);
}
}
opts.conditions = conditions;
delete vals.conditions;
}
if (vals["default"] != null) {
opts["default"] = vals["default"];
delete vals["default"];
}
matchesWith = {};
for (k in vals) {
v = vals[k];
matchesWith[k] = this.interpretValue(v);
delete vals.k;
}
opts.matchesWith = matchesWith;
} else {
path = this.interpretValue(vals);
}
name = "" + name;
router.add(path, opts).to(function(req, response) {
response.params = req.params;
return response.end(req.route.name);
});
}
}
return router;
};
return Test;
})();
GenerationTest = (function() {
__extends(GenerationTest, Test);
function GenerationTest() {
GenerationTest.__super__.constructor.apply(this, arguments);
}
GenerationTest.prototype.invoke = function() {
var actualResult, example, expectedResult, name, params, router, test, _i, _j, _len, _len2, _ref, _ref2;
console.log("Running " + this.examples.length + " generation tests");
_ref = this.examples;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
example = _ref[_i];
process.stdout.write("*");
router = this.constructRouter(example);
_ref2 = example.tests;
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
test = _ref2[_j];
process.stdout.write(".");
expectedResult = test[0], name = test[1], params = test[2];
if ((params != null) && params instanceof Array) {
continue;
}
if (name instanceof Object) {
continue;
}
actualResult = router.url(name, params);
assert.equal(expectedResult, actualResult);
}
}
return console.log("\nDone!");
};
return GenerationTest;
})();
RecognitionTest = (function() {
__extends(RecognitionTest, Test);
function RecognitionTest() {
RecognitionTest.__super__.constructor.apply(this, arguments);
}
RecognitionTest.prototype.invoke = function() {
var complex, example, expectedParams, expectedRouteName, k, mockRequest, mockResponse, pathInfoExcpectation, requestingPath, router, test, v, _i, _j, _len, _len2, _ref, _ref2;
console.log("Running " + this.examples.length + " recognition tests");
_ref = this.examples;
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
example = _ref[_i];
process.stdout.write("*");
router = this.constructRouter(example);
_ref2 = example.tests;
for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
test = _ref2[_j];
mockResponse = {
end: function(part) {
return this.val = part;
}
};
process.stdout.write(".");
expectedRouteName = test[0], requestingPath = test[1], expectedParams = test[2];
mockRequest = {};
complex = false;
if (requestingPath.path != null) {
complex = true;
mockRequest.url = requestingPath.path;
delete requestingPath.path;
for (k in requestingPath) {
v = requestingPath[k];
mockRequest[k] = v;
}
} else {
mockRequest.url = requestingPath;
}
if (!mockRequest.url.match(/^http/)) {
mockRequest.url = "http://host" + mockRequest.url;
}
router.match(mockRequest, mockResponse);
assert.equal(expectedRouteName, mockResponse.val);
expectedParams || (expectedParams = {});
mockResponse.params || (mockResponse.params = {});
pathInfoExcpectation = null;
if (expectedParams.PATH_INFO != null) {
pathInfoExcpectation = expectedParams.PATH_INFO;
delete expectedParams.PATH_INFO;
}
if (pathInfoExcpectation) {
assert.equal(pathInfoExcpectation, mockRequest.pathInfo);
}
assert.deepEqual(expectedParams, mockResponse.params);
if (!complex) {
mockResponse = {
end: function(part) {
return this.val = part;
}
};
router.match(requestingPath, mockResponse);
assert.equal(expectedRouteName, mockResponse.val);
expectedParams || (expectedParams = {});
mockResponse.params || (mockResponse.params = {});
if (pathInfoExcpectation) {
assert.equal(pathInfoExcpectation, mockRequest.pathInfo);
}
assert.deepEqual(expectedParams, mockResponse.params);
}
}
}
return console.log("\nDone!");
};
return RecognitionTest;
})();
new GenerationTest("" + __dirname + "/../../test/common/generate.txt").invoke();
new RecognitionTest("" + __dirname + "/../../test/common/recognize.txt").invoke();
}).call(this);

View File

@ -1,4 +1,4 @@
# encoding: utf-8
class HttpRouter #:nodoc
VERSION = '0.8.11'
VERSION = '0.9.0'
end

View File

@ -72,7 +72,8 @@
["/1/123", "a", {"entry": "123"}]
{"a": "/:var"}
["/%C3%A4", "a", "ä"]
["/%C3%A4", "a", ["ä"]]
["/%C3%A4", "a", {"var": "ä"}]
{"a": {"path": ":var", "var": {"regex": "\\d+"}}}
[null, "a", "asd"]

View File

@ -0,0 +1,56 @@
[{"router":{"path":"/test", "conditions": {"request_method": "POST"}}}]
["router", {"path": "/test", "method": "POST"}]
[[405, {"Allow": "POST"}], {"path": "/test", "method": "GET"}]
{"router": {"path": "/test", "conditions": {"request_method": ["POST", "GET"]}}}
["router", {"path": "/test", "method": "POST"}]
["router", {"path": "/test", "method": "GET"}]
[[405, {"Allow": "GET, POST"}], {"path": "/test", "method": "PUT"}]
{"get": {"path": "/test(.:format)", "conditions": {"request_method": "GET"}}}
{"post": {"path": "/test(.:format)", "conditions": {"request_method": "POST"}}}
{"delete": {"path": "/test(.:format)", "conditions": {"request_method": "DELETE"}}}
["get", {"path": "/test", "method": "GET"}]
["post", {"path": "/test", "method": "POST"}]
["delete", {"path": "/test", "method": "DELETE"}]
["get", {"path": "/test.html", "method": "GET"}, {"format": "html"}]
["post", {"path": "/test.html", "method": "POST"}, {"format": "html"}]
["delete", {"path": "/test.html", "method": "DELETE"}, {"format": "html"}]
[[405, {"Allow": "DELETE, GET, POST"}], {"path": "/test", "method": "PUT"}]
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
{"post_2": {"path": "/test/post", "conditions": {"request_method": "POST"}}}
{"get": {"path": "/test", "conditions": {"request_method": "GET"}}}
{"get_2": {"path": "/test/post", "conditions": {"request_method": "GET"}}}
{"any_2": "/test/post"}
{"any": "/test"}
["post", {"path": "/test", "method": "POST"}]
["get", {"path": "/test", "method": "GET"}]
["any", {"path": "/test", "method": "PUT"}]
["post_2", {"path": "/test/post", "method": "POST"}]
["get_2", {"path": "/test/post", "method": "GET"}]
["any_2", {"path": "/test/post", "method": "PUT"}]
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
{"any": "/test"}
["post", {"path": "/test", "method": "POST"}]
["any", {"path": "/test", "method": "PUT"}]
{"host2_post": {"path": "/test", "conditions": {"request_method": "POST", "host": "host2"}}}
{"host2_get": {"path": "/test", "conditions": {"request_method": "GET", "host": "host2"}}}
{"host2": {"path": "/test", "conditions": {"host": "host2"}}}
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
["host2", {"path": "http://host2/test", "method": "PUT"}]
["post", {"path": "http://host1/test", "method": "POST"}]
["host2_get", {"path": "http://host2/test", "method": "GET"}]
["host2_post", {"path": "http://host2/test", "method": "POST"}]
{"with": {"path": "/test", "conditions": {"request_method": "GET", "host": {"regex": "host1"}}}}
{"without": {"path": "/test", "conditions": {"request_method": "GET"}}}
["without", "http://host2/test"]
["with", "http://host2.host1.com/test"]
{"http": {"path": "/test", "conditions": {"scheme": "http"}}}
{"https": {"path": "/test", "conditions": {"scheme": "https"}}}
["http", {"path": "/test", "scheme": "http"}]
["https", {"path": "/test", "scheme": "https"}]

View File

@ -38,6 +38,9 @@
["route", "/test.html", {"format": "html"}]
["route", "/test"]
{"route": "/"}
["route", "/"]
[{"route": "(.:format)"}]
["route", "/.html", {"format": "html"}]
["route", "/"]
@ -49,6 +52,7 @@
["route", "/foo", {"test": "foo"}]
["route", "/foo.bar", {"format": "bar", "test": "foo"}]
[{"route": {"path": "/:test(.:format)", "format": {"regex": "[^\\.]+"}}}]
["route", "/asd@asd.com.json", {"test": "asd@asd.com", "format": "json"}]
@ -176,16 +180,16 @@
["test", "/test/optional/", {"PATH_INFO": "/optional/"}]
["root", "/testing/optional", {"PATH_INFO": "/testing/optional"}]
[{"route": "/one-:variable-time"}]
["route", "one-value-time", {"variable": "value"}]
{"route": "/one-:variable-time"}
["route", "/one-value-time", {"variable": "value"}]
[{"route": {"path": "/one-:variable-time", "variable": {"regex": "\\d+"}}}]
["route", "one-123-time", {"variable": "123"}]
[null, "one-value-time"]
["route", "/one-123-time", {"variable": "123"}]
[null, "/one-value-time"]
[{"route": {"path": "/one-:variable-time", "variable": {"regex": "\\d+"}}}]
["route", "one-123-time", {"variable": "123"}]
[null, "one-value-time"]
["route", "/one-123-time", {"variable": "123"}]
[null, "/one-value-time"]
[{"route": "hey.:greed.html"}]
["route", "/hey.greedybody.html", {"greed": "greedybody"}]
@ -202,62 +206,3 @@
{"without_regex": "/:common_variable.:unmatched"}
["with_regex", "/common.123", {"common_variable": "common", "matched": "123"}]
["without_regex", "/common.other", {"common_variable": "common", "unmatched": "other"}]
{"nothing":{"path":"/", "conditions": {"request_method": "GET"}}}
{"post":{"path":"/test", "conditions": {"request_method": "POST"}}}
{"put":{"path":"/test", "conditions": {"request_method": "PUT"}}}
["post", {"path": "/test", "method": "POST"}]
[[405, {"Allow": "POST, PUT"}], {"path": "/test", "method": "GET"}]
{"router": {"path": "/test", "conditions": {"request_method": ["POST", "GET"]}}}
["router", {"path": "/test", "method": "POST"}]
["router", {"path": "/test", "method": "GET"}]
[[405, {"Allow": "GET, POST"}], {"path": "/test", "method": "PUT"}]
{"get": {"path": "/test(.:format)", "conditions": {"request_method": "GET"}}}
{"post": {"path": "/test(.:format)", "conditions": {"request_method": "POST"}}}
{"delete": {"path": "/test(.:format)", "conditions": {"request_method": "DELETE"}}}
["get", {"path": "/test", "method": "GET"}]
["post", {"path": "/test", "method": "POST"}]
["delete", {"path": "/test", "method": "DELETE"}]
["get", {"path": "/test.html", "method": "GET"}, {"format": "html"}]
["post", {"path": "/test.html", "method": "POST"}, {"format": "html"}]
["delete", {"path": "/test.html", "method": "DELETE"}, {"format": "html"}]
[[405, {"Allow": "DELETE, GET, POST"}], {"path": "/test", "method": "PUT"}]
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
{"post_2": {"path": "/test/post", "conditions": {"request_method": "POST"}}}
{"get": {"path": "/test", "conditions": {"request_method": "GET"}}}
{"get_2": {"path": "/test/post", "conditions": {"request_method": "GET"}}}
{"any_2": "/test/post"}
{"any": "/test"}
["post", {"path": "/test", "method": "POST"}]
["get", {"path": "/test", "method": "GET"}]
["any", {"path": "/test", "method": "PUT"}]
["post_2", {"path": "/test/post", "method": "POST"}]
["get_2", {"path": "/test/post", "method": "GET"}]
["any_2", {"path": "/test/post", "method": "PUT"}]
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
{"any": "/test"}
["post", {"path": "/test", "method": "POST"}]
["any", {"path": "/test", "method": "PUT"}]
{"host2_post": {"path": "/test", "conditions": {"request_method": "POST", "host": "host2"}}}
{"host2_get": {"path": "/test", "conditions": {"request_method": "GET", "host": "host2"}}}
{"host2": {"path": "/test", "conditions": {"host": "host2"}}}
{"post": {"path": "/test", "conditions": {"request_method": "POST"}}}
["host2", {"path": "http://host2/test", "method": "PUT"}]
["post", {"path": "http://host1/test", "method": "POST"}]
["host2_get", {"path": "http://host2/test", "method": "GET"}]
["host2_post", {"path": "http://host2/test", "method": "POST"}]
{"with": {"path": "/test", "conditions": {"request_method": "GET", "host": {"regex": "host1"}}}}
{"without": {"path": "/test", "conditions": {"request_method": "GET"}}}
["without", "http://host2/test"]
["with", "http://host2.host1.com/test"]
{"http": {"path": "/test", "conditions": {"scheme": "http"}}}
{"https": {"path": "/test", "conditions": {"scheme": "https"}}}
["http", {"path": "/test", "scheme": "http"}]
["https", {"path": "/test", "scheme": "https"}]

View File

@ -1,77 +1,7 @@
require 'json'
generation_file = "#{File.dirname(__FILE__)}/common/generate.txt"
generation = File.read(generation_file)
class GenerationTest
Info = Struct.new(:case, :original_line, :num)
attr_reader :routes, :tests
def initialize(file)
@tests = []
@routes = Info.new([], "", 0)
end
def error(msg)
raise("Error in case: #{@routes.original_line.strip}:#{@routes.num + 1}\n#{msg}")
end
def add_test(line, num)
@tests << Info.new(JSON.parse(line), line, num)
end
def add_routes(line, num)
info = Info.new(JSON.parse(line), line, num)
error("Routes have already been defined without tests") if info.case.is_a?(Array) && !@routes.case.empty?
if info.case.is_a?(Array)
@routes = info
elsif @routes.case.empty?
info.case = [info.case]
@routes = info
else
@routes.case << info.case
end
end
def interpret_val(val)
case val
when nil
error("Unable to interpret #{val.inspect}")
when Hash
val['regex'] ? Regexp.new(val['regex']) : error("Okay serious, no idea #{val.inspect}")
else
val
end
end
def invoke
error("invoke called with no tests or routes") if @tests.empty? || @routes.nil?
router = HttpRouter.new
@routes.case.each do |route_definition|
error("Too many keys! #{route_definition.keys.inspect}") unless route_definition.keys.size == 1
route_name, route_properties = route_definition.keys.first, route_definition.values.first
route = case route_properties
when String
router.add(route_properties)
when Hash
opts = {}
route_path = interpret_val(route_properties.delete("path"))
if route_properties.key?("conditions")
opts[:conditions] = Hash[route_properties.delete("conditions").map{|k, v| [k.to_sym, interpret_val(v)]}]
end
if route_properties.key?("default")
opts[:default_values] = Hash[route_properties.delete("default").map{|k, v| [k.to_sym, interpret_val(v)]}]
end
route_properties.each do |key, val|
opts[key.to_sym] = interpret_val(val)
end
router.add(route_path, opts)
else
error("Route isn't a String or hash")
end
route.name(route_name.to_sym)
route.to{|env| [200, {"env-to-test" => env.dup}, [route_name]]}
end
require "#{File.dirname(__FILE__)}/generic"
class GenerationTest < AbstractTest
def run_tests
@tests.map(&:case).each do |(expected_result, name, args)|
args = [args] unless args.is_a?(Array)
args.compact!
@ -89,31 +19,4 @@ class GenerationTest
end
end
tests = []
test = nil
num = 0
generation.each_line do |line|
begin
case line
when /^#/, /^\s*$/
# skip
when /^( |\t)/
test.add_test(line, num)
else
if test.nil? || !test.tests.empty?
tests << test if test
test = GenerationTest.new(generation_file)
end
test.add_routes(line, num)
end
rescue
warn "There was a problem with #{num}:#{line}"
raise
end
num += 1
end
tests << test
puts "Running generation tests (Routes: #{tests.size}, Tests: #{tests.inject(0){|s, t| s+=t.tests.size}})..."
tests.each(&:invoke)
puts "\ndone!"
GenerationTest.run("#{File.dirname(__FILE__)}/common/generate.txt")

110
test/generic.rb Normal file
View File

@ -0,0 +1,110 @@
require 'json'
class AbstractTest
def self.run(file)
contents = File.read(file)
tests = []
test = nil
num = 0
contents.each_line do |line|
begin
case line
when /^#/, /^\s*$/
# skip
when /^( |\t)/
test.add_test(line, num)
else
if test.nil? || !test.tests.empty?
tests << test if test
test = new(file)
end
test.add_routes(line, num)
end
rescue
warn "There was a problem with #{num}:#{line}"
raise
end
num += 1
end
tests << test
puts "Running tests (#{name}) (Routes: #{tests.size}, Tests: #{tests.inject(0){|s, t| s+=t.tests.size}})..."
tests.each(&:invoke)
puts "\ndone!"
end
Info = Struct.new(:case, :original_line, :num)
attr_reader :routes, :tests
def initialize(file)
@tests = []
@routes = Info.new([], "", 0)
end
def error(msg)
raise("Error in case: #{@routes.original_line.strip}:#{@routes.num + 1}\n#{msg}")
end
def add_test(line, num)
@tests << Info.new(JSON.parse(line), line, num)
end
def add_routes(line, num)
info = Info.new(JSON.parse(line), line, num)
error("Routes have already been defined without tests") if info.case.is_a?(Array) && !@routes.case.empty?
if info.case.is_a?(Array)
@routes = info
elsif @routes.case.empty?
info.case = [info.case]
@routes = info
else
@routes.case << info.case
end
end
def interpret_val(val)
case val
when nil
error("Unable to interpret #{val.inspect}")
when Hash
val['regex'] ? Regexp.new(val['regex']) : error("Okay serious, no idea #{val.inspect}")
else
val
end
end
def run_tests
raise
end
def invoke
error("invoke called with no tests or routes") if @tests.empty? || @routes.nil?
router = HttpRouter.new
@routes.case.each do |route_definition|
error("Too many keys! #{route_definition.keys.inspect}") unless route_definition.keys.size == 1
route_name, route_properties = route_definition.keys.first, route_definition.values.first
route = case route_properties
when String
router.add(route_properties)
when Hash
opts = {}
route_path = interpret_val(route_properties.delete("path"))
if route_properties.key?("conditions")
opts[:conditions] = Hash[route_properties.delete("conditions").map{|k, v| [k.to_sym, interpret_val(v)]}]
end
if route_properties.key?("default")
opts[:default_values] = Hash[route_properties.delete("default").map{|k, v| [k.to_sym, interpret_val(v)]}]
end
route_properties.each do |key, val|
opts[key.to_sym] = interpret_val(val)
end
router.add(route_path, opts)
else
error("Route isn't a String or hash")
end
route.name(route_name.to_sym)
route.to{|env| [200, {"env-to-test" => env.dup}, [route_name]]}
end
print '.'
end
end

View File

@ -1,142 +1,22 @@
require 'json'
recognition_file = "#{File.dirname(__FILE__)}/common/recognize.txt"
recognition = File.read(recognition_file)
class RecognitionTest
Info = Struct.new(:case, :original_line, :num)
attr_reader :routes, :tests
def initialize(file)
@tests = []
@routes = Info.new([], "", 0)
end
def error(msg)
raise("Error in case: #{@routes.original_line.strip}:#{@routes.num + 1}\n#{msg}")
end
def add_test(line, num)
@tests << Info.new(JSON.parse(line), line, num)
end
def add_routes(line, num)
info = Info.new(JSON.parse(line), line, num)
error("Routes have already been defined without tests") if info.case.is_a?(Array) && !@routes.case.empty?
if info.case.is_a?(Array)
@routes = info
elsif @routes.case.empty?
info.case = [info.case]
@routes = info
else
@routes.case << info.case
end
end
def interpret_val(val)
case val
when nil
error("Unable to interpret #{val.inspect}")
when Hash
val['regex'] ? Regexp.new(val['regex']) : error("Okay serious, no idea #{val.inspect}")
else
val
end
end
def invoke
error("invoke called with no tests or routes") if @tests.empty? || @routes.nil?
router = HttpRouter.new
@routes.case.each do |route_definition|
error("Too many keys! #{route_definition.keys.inspect}") unless route_definition.keys.size == 1
route_name, route_properties = route_definition.keys.first, route_definition.values.first
route = case route_properties
when String
router.add(route_properties)
when Hash
opts = {}
route_path = interpret_val(route_properties.delete("path"))
if route_properties.key?("conditions")
opts[:conditions] = Hash[route_properties.delete("conditions").map{|k, v| [k.to_sym, interpret_val(v)]}]
end
route_properties.each do |key, val|
opts[key.to_sym] = interpret_val(val)
end
router.add(route_path, opts)
else
error("Route isn't a String or hash")
require "#{File.dirname(__FILE__)}/generic"
class RecognitionTest < AbstractTest
def run_tests
@tests.map(&:case).each do |(expected_result, name, args)|
args = [args] unless args.is_a?(Array)
args.compact!
args.map!{|a| a.is_a?(Hash) ? Hash[a.map{|k,v| [k.to_sym, v]}] : a }
result = begin
router.url(name.to_sym, *args)
rescue HttpRouter::InvalidRouteException
nil
rescue HttpRouter::MissingParameterException
nil
end
route.name(route_name.to_sym)
route.to{|env| [200, {"env-to-test" => env.dup}, [route_name]]}
end
@tests.map(&:case).each do |(name, req, params)|
env = case req
when String
Rack::MockRequest.env_for(req)
when Hash
e = Rack::MockRequest.env_for(req['path'])
e['REQUEST_METHOD'] = req['method'] if req.key?('method')
e['rack.url_scheme'] = req['scheme'] if req.key?('scheme')
e
end
response = router.call(env)
case name
when nil
error("Expected no response") unless response.first == 404
when Array
name.each_with_index do |part, i|
case part
when Hash then part.keys.all? or error("#{part.inspect} didn't match #{response[i].inspect}")
else part == response[i] or error("#{part.inspect} didn't match #{response[i].inspect}")
end
end
else
error("Expected #{name} for #{req.inspect} got #{response.inspect}") unless response.last == [name]
end
env['router.params'] ||= {}
params ||= {}
if params['PATH_INFO']
path_info = params.delete("PATH_INFO")
error("path_info #{env['PATH_INFO'].inspect} is not #{path_info.inspect}") unless path_info == env['PATH_INFO']
end
env['router.params'].keys.each do |k|
p_v = params.delete(k.to_s)
v = env['router.params'].delete(k.to_sym)
error("I got #{p_v.inspect} but expected #{v.inspect}") unless p_v == v
end
error("Left over expectations: #{params.inspect}") unless params.empty?
error("Left over matched params: #{env['router.params'].inspect}") unless env['router.params'].empty?
error("Result #{result.inspect} did not match expectation #{expected_result.inspect}") unless result == expected_result
end
print '.'
end
end
tests = []
test = nil
num = 0
recognition.each_line do |line|
begin
case line
when /^#/, /^\s*$/
# skip
when /^( |\t)/
test.add_test(line, num)
else
if test.nil? || !test.tests.empty?
tests << test if test
test = RecognitionTest.new(recognition_file)
end
test.add_routes(line, num)
end
rescue
warn "There was a problem with #{num}:#{line}"
raise
end
num += 1
end
tests << test
puts "Running recognition tests (Routes: #{tests.size}, Tests: #{tests.inject(0){|s, t| s+=t.tests.size}})..."
tests.each(&:invoke)
puts "\ndone!"
RecognitionTest.run("#{File.dirname(__FILE__)}/common/recognize.txt")
RecognitionTest.run("#{File.dirname(__FILE__)}/common/http_recognize.txt")