journey-1.0.4/0000755000004100000410000000000011770361473013250 5ustar www-datawww-datajourney-1.0.4/.travis.yml0000644000004100000410000000007511770361473015363 0ustar www-datawww-datarvm: - 1.8.7 - 1.9.2 - 1.9.3 - jruby - rbx - ree journey-1.0.4/test/0000755000004100000410000000000011770361473014227 5ustar www-datawww-datajourney-1.0.4/test/route/0000755000004100000410000000000011770361473015365 5ustar www-datawww-datajourney-1.0.4/test/route/definition/0000755000004100000410000000000011770361473017515 5ustar www-datawww-datajourney-1.0.4/test/route/definition/test_parser.rb0000644000004100000410000000433011770361473022375 0ustar www-datawww-datarequire 'helper' module Journey module Definition class TestParser < MiniTest::Unit::TestCase def setup @parser = Parser.new end def test_slash assert_equal :SLASH, @parser.parse('/').type assert_round_trip '/' end def test_segment assert_round_trip '/foo' end def test_segments assert_round_trip '/foo/bar' end def test_segment_symbol assert_round_trip '/foo/:id' end def test_symbol assert_round_trip '/:foo' end def test_group assert_round_trip '(/:foo)' end def test_groups assert_round_trip '(/:foo)(/:bar)' end def test_nested_groups assert_round_trip '(/:foo(/:bar))' end def test_dot_symbol assert_round_trip('.:format') end def test_dot_literal assert_round_trip('.xml') end def test_segment_dot assert_round_trip('/foo.:bar') end def test_segment_group_dot assert_round_trip('/foo(.:bar)') end def test_segment_group assert_round_trip('/foo(/:action)') end def test_segment_groups assert_round_trip('/foo(/:action)(/:bar)') end def test_segment_nested_groups assert_round_trip('/foo(/:action(/:bar))') end def test_group_followed_by_path assert_round_trip('/foo(/:action)/:bar') end def test_star assert_round_trip('*foo') assert_round_trip('/*foo') assert_round_trip('/bar/*foo') assert_round_trip('/bar/(*foo)') end def test_or assert_round_trip('a|b') assert_round_trip('a|b|c') assert_round_trip('(a|b)|c') assert_round_trip('a|(b|c)') assert_round_trip('*a|(b|c)') assert_round_trip('*a|:b|c') end def test_arbitrary assert_round_trip('/bar/*foo#') end def test_literal_dot_paren assert_round_trip "/sprockets.js(.:format)" end def test_groups_with_dot assert_round_trip "/(:locale)(.:format)" end def assert_round_trip str assert_equal str, @parser.parse(str).to_s end end end end journey-1.0.4/test/route/definition/test_scanner.rb0000644000004100000410000000320111770361473022526 0ustar www-datawww-datarequire 'helper' module Journey module Definition class TestScanner < MiniTest::Unit::TestCase def setup @scanner = Scanner.new end # /page/:id(/:action)(.:format) def test_tokens [ ['/', [[:SLASH, '/']]], ['*omg', [[:STAR, '*'], [:LITERAL, 'omg']]], ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]], ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]], ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]], ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]], ['/(:page)', [ [:SLASH, '/'], [:LPAREN, '('], [:SYMBOL, ':page'], [:RPAREN, ')'], ]], ['(/:action)', [ [:LPAREN, '('], [:SLASH, '/'], [:SYMBOL, ':action'], [:RPAREN, ')'], ]], ['(())', [[:LPAREN, '('], [:LPAREN, '('], [:RPAREN, ')'], [:RPAREN, ')']]], ['(.:format)', [ [:LPAREN, '('], [:DOT, '.'], [:SYMBOL, ':format'], [:RPAREN, ')'], ]], ].each do |str, expected| @scanner.scan_setup str assert_tokens expected, @scanner end end def assert_tokens tokens, scanner toks = [] while tok = scanner.next_token toks << tok end assert_equal tokens, toks end end end end journey-1.0.4/test/path/0000755000004100000410000000000011770361473015163 5ustar www-datawww-datajourney-1.0.4/test/path/test_pattern.rb0000644000004100000410000002170711770361473020233 0ustar www-datawww-datarequire 'helper' module Journey module Path class TestPattern < MiniTest::Unit::TestCase x = /.+/ { '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z}, '/:controller/foo' => %r{\A/(#{x})/foo\Z}, '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)\Z}, '/:controller' => %r{\A/(#{x})\Z}, '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}, '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml\Z}, '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)\Z}, '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?\Z}, '/:controller/*foo' => %r{\A/(#{x})/(.+)\Z}, '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z}, }.each do |path, expected| define_method(:"test_to_regexp_#{path}") do strexp = Router::Strexp.new( path, { :controller => /.+/ }, ["/", ".", "?"] ) path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end { '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?}, '/:controller/foo' => %r{\A/(#{x})/foo}, '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)}, '/:controller' => %r{\A/(#{x})}, '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?}, '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml}, '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)}, '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?}, '/:controller/*foo' => %r{\A/(#{x})/(.+)}, '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar}, }.each do |path, expected| define_method(:"test_to_non_anchored_regexp_#{path}") do strexp = Router::Strexp.new( path, { :controller => /.+/ }, ["/", ".", "?"], false ) path = Pattern.new strexp assert_equal(expected, path.to_regexp) end end { '/:controller(/:action)' => %w{ controller action }, '/:controller/foo' => %w{ controller }, '/:controller/:action' => %w{ controller action }, '/:controller' => %w{ controller }, '/:controller(/:action(/:id))' => %w{ controller action id }, '/:controller/:action.xml' => %w{ controller action }, '/:controller.:format' => %w{ controller format }, '/:controller(.:format)' => %w{ controller format }, '/:controller/*foo' => %w{ controller foo }, '/:controller/*foo/bar' => %w{ controller foo }, }.each do |path, expected| define_method(:"test_names_#{path}") do strexp = Router::Strexp.new( path, { :controller => /.+/ }, ["/", ".", "?"] ) path = Pattern.new strexp assert_equal(expected, path.names) end end def test_to_regexp_with_extended_group strexp = Router::Strexp.new( '/page/:name', { :name => / #ROFL (tender|love #MAO )/x }, ["/", ".", "?"] ) path = Pattern.new strexp assert_match('/page/tender', path) assert_match('/page/love', path) refute_match('/page/loving', path) end def test_optional_names [ ['/:foo(/:bar(/:baz))', %w{ bar baz }], ['/:foo(/:bar)', %w{ bar }], ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }], ].each do |pattern, list| path = Pattern.new pattern assert_equal list.sort, path.optional_names.sort end end def test_to_regexp_match_non_optional strexp = Router::Strexp.new( '/:name', { :name => /\d+/ }, ["/", ".", "?"] ) path = Pattern.new strexp assert_match('/123', path) refute_match('/', path) end def test_to_regexp_with_group strexp = Router::Strexp.new( '/page/:name', { :name => /(tender|love)/ }, ["/", ".", "?"] ) path = Pattern.new strexp assert_match('/page/tender', path) assert_match('/page/love', path) refute_match('/page/loving', path) end def test_ast_sets_regular_expressions requirements = { :name => /(tender|love)/, :value => /./ } strexp = Router::Strexp.new( '/page/:name/:value', requirements, ["/", ".", "?"] ) assert_equal requirements, strexp.requirements path = Pattern.new strexp nodes = path.ast.grep(Nodes::Symbol) assert_equal 2, nodes.length nodes.each do |node| assert_equal requirements[node.to_sym], node.regexp end end def test_match_data_with_group strexp = Router::Strexp.new( '/page/:name', { :name => /(tender|love)/ }, ["/", ".", "?"] ) path = Pattern.new strexp match = path.match '/page/tender' assert_equal 'tender', match[1] assert_equal 2, match.length end def test_match_data_with_multi_group strexp = Router::Strexp.new( '/page/:name/:id', { :name => /t(((ender|love)))()/ }, ["/", ".", "?"] ) path = Pattern.new strexp match = path.match '/page/tender/10' assert_equal 'tender', match[1] assert_equal '10', match[2] assert_equal 3, match.length assert_equal %w{ tender 10 }, match.captures end def test_star_with_custom_re z = /\d+/ strexp = Router::Strexp.new( '/page/*foo', { :foo => z }, ["/", ".", "?"] ) path = Pattern.new strexp assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp) end def test_insensitive_regexp_with_group strexp = Router::Strexp.new( '/page/:name/aaron', { :name => /(tender|love)/i }, ["/", ".", "?"] ) path = Pattern.new strexp assert_match('/page/TENDER/aaron', path) assert_match('/page/loVE/aaron', path) refute_match('/page/loVE/AAron', path) end def test_to_regexp_with_strexp strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"]) path = Pattern.new strexp x = %r{\A/([^/.?]+)\Z} assert_equal(x.source, path.source) end def test_to_regexp_defaults path = Pattern.new '/:controller(/:action(/:id))' expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z} assert_equal expected, path.to_regexp end def test_failed_match path = Pattern.new '/:controller(/:action(/:id(.:format)))' uri = 'content' refute path =~ uri end def test_match_controller path = Pattern.new '/:controller(/:action(/:id(.:format)))' uri = '/content' match = path =~ uri assert_equal %w{ controller action id format }, match.names assert_equal 'content', match[1] assert_nil match[2] assert_nil match[3] assert_nil match[4] end def test_match_controller_action path = Pattern.new '/:controller(/:action(/:id(.:format)))' uri = '/content/list' match = path =~ uri assert_equal %w{ controller action id format }, match.names assert_equal 'content', match[1] assert_equal 'list', match[2] assert_nil match[3] assert_nil match[4] end def test_match_controller_action_id path = Pattern.new '/:controller(/:action(/:id(.:format)))' uri = '/content/list/10' match = path =~ uri assert_equal %w{ controller action id format }, match.names assert_equal 'content', match[1] assert_equal 'list', match[2] assert_equal '10', match[3] assert_nil match[4] end def test_match_literal path = Path::Pattern.new "/books(/:action(.:format))" uri = '/books' match = path =~ uri assert_equal %w{ action format }, match.names assert_nil match[1] assert_nil match[2] end def test_match_literal_with_action path = Path::Pattern.new "/books(/:action(.:format))" uri = '/books/list' match = path =~ uri assert_equal %w{ action format }, match.names assert_equal 'list', match[1] assert_nil match[2] end def test_match_literal_with_action_and_format path = Path::Pattern.new "/books(/:action(.:format))" uri = '/books/list.rss' match = path =~ uri assert_equal %w{ action format }, match.names assert_equal 'list', match[1] assert_equal 'rss', match[2] end end end end journey-1.0.4/test/nfa/0000755000004100000410000000000011770361473014773 5ustar www-datawww-datajourney-1.0.4/test/nfa/test_transition_table.rb0000644000004100000410000000344711770361473021730 0ustar www-datawww-datarequire 'helper' module Journey module NFA class TestTransitionTable < MiniTest::Unit::TestCase def setup @parser = Journey::Parser.new end def test_eclosure table = tt '/' assert_equal [0], table.eclosure(0) table = tt ':a|:b' assert_equal 3, table.eclosure(0).length table = tt '(:a|:b)' assert_equal 5, table.eclosure(0).length assert_equal 5, table.eclosure([0]).length end def test_following_states_one table = tt '/' assert_equal [1], table.following_states(0, '/') assert_equal [1], table.following_states([0], '/') end def test_following_states_group table = tt 'a|b' states = table.eclosure 0 assert_equal 1, table.following_states(states, 'a').length assert_equal 1, table.following_states(states, 'b').length end def test_following_states_multi table = tt 'a|a' states = table.eclosure 0 assert_equal 2, table.following_states(states, 'a').length assert_equal 0, table.following_states(states, 'b').length end def test_following_states_regexp table = tt 'a|:a' states = table.eclosure 0 assert_equal 1, table.following_states(states, 'a').length assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length assert_equal 0, table.following_states(states, 'b').length end def test_alphabet table = tt 'a|:a' assert_equal [/[^\.\/\?]+/, 'a'], table.alphabet table = tt 'a|a' assert_equal ['a'], table.alphabet end private def tt string ast = @parser.parse string builder = Builder.new ast builder.transition_table end end end end journey-1.0.4/test/nfa/test_simulator.rb0000644000004100000410000000460711770361473020405 0ustar www-datawww-datarequire 'helper' module Journey module NFA class TestSimulator < MiniTest::Unit::TestCase def test_simulate_simple sim = simulator_for ['/foo'] assert_match sim, '/foo' end def test_simulate_simple_no_match sim = simulator_for ['/foo'] refute_match sim, 'foo' end def test_simulate_simple_no_match_too_long sim = simulator_for ['/foo'] refute_match sim, '/foo/bar' end def test_simulate_simple_no_match_wrong_string sim = simulator_for ['/foo'] refute_match sim, '/bar' end def test_simulate_regex sim = simulator_for ['/:foo/bar'] assert_match sim, '/bar/bar' assert_match sim, '/foo/bar' end def test_simulate_or sim = simulator_for ['/foo', '/bar'] assert_match sim, '/bar' assert_match sim, '/foo' refute_match sim, '/baz' end def test_simulate_optional sim = simulator_for ['/foo(/bar)'] assert_match sim, '/foo' assert_match sim, '/foo/bar' refute_match sim, '/foo/' end def test_matchdata_has_memos paths = %w{ /foo /bar } parser = Journey::Parser.new asts = paths.map { |x| ast = parser.parse x ast.each { |n| n.memo = ast} ast } expected = asts.first builder = Builder.new Nodes::Or.new asts sim = Simulator.new builder.transition_table md = sim.match '/foo' assert_equal [expected], md.memos end def test_matchdata_memos_on_merge parser = Journey::Parser.new routes = [ '/articles(.:format)', '/articles/new(.:format)', '/articles/:id/edit(.:format)', '/articles/:id(.:format)', ].map { |path| ast = parser.parse path ast.each { |n| n.memo = ast } ast } asts = routes.dup ast = Nodes::Or.new routes nfa = Journey::NFA::Builder.new ast sim = Simulator.new nfa.transition_table md = sim.match '/articles' assert_equal [asts.first], md.memos end def simulator_for paths parser = Journey::Parser.new asts = paths.map { |x| parser.parse x } builder = Builder.new Nodes::Or.new asts Simulator.new builder.transition_table end end end end journey-1.0.4/test/nodes/0000755000004100000410000000000011770361473015337 5ustar www-datawww-datajourney-1.0.4/test/nodes/test_symbol.rb0000644000004100000410000000043011770361473020225 0ustar www-datawww-datarequire 'helper' module Journey module Nodes class TestSymbol < MiniTest::Unit::TestCase def test_default_regexp? sym = Symbol.new nil assert sym.default_regexp? sym.regexp = nil refute sym.default_regexp? end end end end journey-1.0.4/test/test_route.rb0000644000004100000410000000700011770361473016746 0ustar www-datawww-datarequire 'helper' module Journey class TestRoute < MiniTest::Unit::TestCase def test_initialize app = Object.new path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' defaults = Object.new route = Route.new("name", app, path, {}, defaults) assert_equal app, route.app assert_equal path, route.path assert_equal defaults, route.defaults end def test_route_adds_itself_as_memo app = Object.new path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' defaults = Object.new route = Route.new("name", app, path, {}, defaults) route.ast.grep(Nodes::Terminal).each do |node| assert_equal route, node.memo end end def test_ip_address path = Path::Pattern.new '/messages/:id(.:format)' route = Route.new("name", nil, path, {:ip => '192.168.1.1'}, { :controller => 'foo', :action => 'bar' }) assert_equal '192.168.1.1', route.ip end def test_default_ip path = Path::Pattern.new '/messages/:id(.:format)' route = Route.new("name", nil, path, {}, { :controller => 'foo', :action => 'bar' }) assert_equal(//, route.ip) end def test_format_empty path = Path::Pattern.new '/messages/:id(.:format)' route = Route.new("name", nil, path, {}, { :controller => 'foo', :action => 'bar' }) assert_equal '/messages', route.format({}) end def test_format_with_star path = Path::Pattern.new '/:controller/*extra' route = Route.new("name", nil, path, {}, { :controller => 'foo', :action => 'bar' }) assert_equal '/foo/himom', route.format({ :controller => 'foo', :extra => 'himom', }) end def test_connects_all_match path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))' route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' }) assert_equal '/foo/bar/10', route.format({ :controller => 'foo', :action => 'bar', :id => 10 }) end def test_extras_are_not_included_if_optional path = Path::Pattern.new '/page/:id(/:action)' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/page/10', route.format({ :id => 10 }) end def test_extras_are_not_included_if_optional_with_parameter path = Path::Pattern.new '(/sections/:section)/pages/:id' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10}) end def test_extras_are_not_included_if_optional_parameter_is_nil path = Path::Pattern.new '(/sections/:section)/pages/:id' route = Route.new("name", nil, path, { }, { :action => 'show' }) assert_equal '/pages/10', route.format({:id => 10, :section => nil}) end def test_score path = Path::Pattern.new "/page/:id(/:action)(.:format)" specific = Route.new "name", nil, path, {}, {:controller=>"pages", :action=>"show"} path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)" generic = Route.new "name", nil, path, {} knowledge = {:id=>20, :controller=>"pages", :action=>"show"} routes = [specific, generic] refute_equal specific.score(knowledge), generic.score(knowledge) found = routes.sort_by { |r| r.score(knowledge) }.last assert_equal specific, found end end end journey-1.0.4/test/helper.rb0000644000004100000410000000012311770361473016027 0ustar www-datawww-datarequire 'rubygems' require 'minitest/autorun' require 'journey' require 'stringio' journey-1.0.4/test/test_router.rb0000644000004100000410000003674311770361473017150 0ustar www-datawww-data# encoding: UTF-8 require 'helper' module Journey class TestRouter < MiniTest::Unit::TestCase attr_reader :routes def setup @routes = Routes.new @router = Router.new(@routes, {}) @formatter = Formatter.new(@routes) end def test_request_class_reader klass = Object.new router = Router.new(routes, :request_class => klass) assert_equal klass, router.request_class end class FakeRequestFeeler < Struct.new(:env, :called) def new env self.env = env self end def hello self.called = true 'world' end def path_info; env['PATH_INFO']; end def request_method; env['REQUEST_METHOD']; end def ip; env['REMOTE_ADDR']; end end def test_dashes klass = FakeRequestFeeler.new nil router = Router.new(routes, {}) exp = Router::Strexp.new '/foo-bar-baz', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo-bar-baz' called = false router.recognize(env) do |r, _, params| called = true end assert called end def test_unicode klass = FakeRequestFeeler.new nil router = Router.new(routes, {}) #match the escaped version of /ほげ exp = Router::Strexp.new '/%E3%81%BB%E3%81%92', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92' called = false router.recognize(env) do |r, _, params| called = true end assert called end def test_request_class_and_requirements_success klass = FakeRequestFeeler.new nil router = Router.new(routes, {:request_class => klass }) requirements = { :hello => /world/ } exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, requirements, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' router.recognize(env) do |r, _, params| assert_equal({:id => '10'}, params) end assert klass.called, 'hello should have been called' assert_equal env.env, klass.env end def test_request_class_and_requirements_fail klass = FakeRequestFeeler.new nil router = Router.new(routes, {:request_class => klass }) requirements = { :hello => /mom/ } exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp router.routes.add_route nil, path, requirements, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' router.recognize(env) do |r, _, params| flunk 'route should not be found' end assert klass.called, 'hello should have been called' assert_equal env.env, klass.env end class CustomPathRequest < Router::NullReq def path_info env['custom.path_info'] end end def test_request_class_overrides_path_info router = Router.new(routes, {:request_class => CustomPathRequest }) exp = Router::Strexp.new '/bar', {}, ['/.?'] path = Path::Pattern.new exp routes.add_route nil, path, {}, {}, {} env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar' recognized = false router.recognize(env) do |r, _, params| recognized = true end assert recognized, "route should have been recognized" end def test_regexp_first_precedence add_routes @router, [ Router::Strexp.new("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']), Router::Strexp.new("/whois/:id(.:format)", {}, ['/', '.', '?']) ] env = rails_env 'PATH_INFO' => '/whois/example.com' list = [] @router.recognize(env) do |r, _, params| list << r end assert_equal 2, list.length r = list.first assert_equal '/whois/:domain', r.path.spec.to_s end def test_required_parts_verified_are_anchored add_routes @router, [ Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false) ] assert_raises(Router::RoutingError) do @formatter.generate(:path_info, nil, { :id => '10' }, { }) end end def test_required_parts_are_verified_when_building add_routes @router, [ Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false) ] path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) assert_equal '/foo/10', path assert_raises(Router::RoutingError) do @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) end end def test_only_required_parts_are_verified add_routes @router, [ Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false) ] path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { }) assert_equal '/foo/10', path path, _ = @formatter.generate(:path_info, nil, { }, { }) assert_equal '/foo', path path, _ = @formatter.generate(:path_info, nil, { :id => 'aa' }, { }) assert_equal '/foo/aa', path end def test_X_Cascade add_routes @router, [ "/messages(.:format)" ] resp = @router.call({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }) assert_equal ['Not Found'], resp.last assert_equal 'pass', resp[1]['X-Cascade'] assert_equal 404, resp.first end def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes strexp = Router::Strexp.new("/", {}, ['/', '.', '?'], false) path = Path::Pattern.new strexp app = lambda { |env| [200, {}, ['success!']] } @router.routes.add_route(app, path, {}, {}, {}) env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog') resp = @router.call(env) assert_equal ['success!'], resp.last assert_equal '', env['SCRIPT_NAME'] assert_equal '/weblog', env['PATH_INFO'] end def test_defaults_merge_correctly path = Path::Pattern.new '/foo(/:id)' @router.routes.add_route nil, path, {}, {:id => nil}, {} env = rails_env 'PATH_INFO' => '/foo/10' @router.recognize(env) do |r, _, params| assert_equal({:id => '10'}, params) end env = rails_env 'PATH_INFO' => '/foo' @router.recognize(env) do |r, _, params| assert_equal({:id => nil}, params) end end def test_recognize_with_unbound_regexp add_routes @router, [ Router::Strexp.new("/foo", { }, ['/', '.', '?'], false) ] env = rails_env 'PATH_INFO' => '/foo/bar' @router.recognize(env) { |*_| } assert_equal '/foo', env.env['SCRIPT_NAME'] assert_equal '/bar', env.env['PATH_INFO'] end def test_bound_regexp_keeps_path_info add_routes @router, [ Router::Strexp.new("/foo", { }, ['/', '.', '?'], true) ] env = rails_env 'PATH_INFO' => '/foo' before = env.env['SCRIPT_NAME'] @router.recognize(env) { |*_| } assert_equal before, env.env['SCRIPT_NAME'] assert_equal '/foo', env.env['PATH_INFO'] end def test_path_not_found add_routes @router, [ "/messages(.:format)", "/messages/new(.:format)", "/messages/:id/edit(.:format)", "/messages/:id(.:format)" ] env = rails_env 'PATH_INFO' => '/messages/unknown/path' yielded = false @router.recognize(env) do |*whatever| yielded = true end refute yielded end def test_required_part_in_recall add_routes @router, [ "/messages/:a/:b" ] path, _ = @formatter.generate(:path_info, nil, { :a => 'a' }, { :b => 'b' }) assert_equal "/messages/a/b", path end def test_splat_in_recall add_routes @router, [ "/*path" ] path, _ = @formatter.generate(:path_info, nil, { }, { :path => 'b' }) assert_equal "/b", path end def test_recall_should_be_used_when_scoring add_routes @router, [ "/messages/:action(/:id(.:format))", "/messages/:id(.:format)" ] path, _ = @formatter.generate(:path_info, nil, { :id => 10 }, { :action => 'index' }) assert_equal "/messages/index/10", path end def test_nil_path_parts_are_ignored path = Path::Pattern.new "/:controller(/:action(.:format))" @router.routes.add_route nil, path, {}, {}, {} params = { :controller => "tasks", :format => nil } extras = { :action => 'lol' } path, _ = @formatter.generate(:path_info, nil, params, extras) assert_equal '/tasks', path end def test_generate_slash params = [ [:controller, "tasks"], [:action, "show"] ] str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true) path = Path::Pattern.new str @router.routes.add_route nil, path, {}, {}, {} path, _ = @formatter.generate(:path_info, nil, Hash[params], {}) assert_equal '/', path end def test_generate_calls_param_proc path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route nil, path, {}, {}, {} parameterized = [] params = [ [:controller, "tasks"], [:action, "show"] ] @formatter.generate( :path_info, nil, Hash[params], {}, lambda { |k,v| parameterized << [k,v]; v }) assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort end def test_generate_id path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route nil, path, {}, {}, {} path, params = @formatter.generate( :path_info, nil, {:id=>1, :controller=>"tasks", :action=>"show"}, {}) assert_equal '/tasks/show', path assert_equal({:id => 1}, params) end def test_generate_escapes path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route nil, path, {}, {}, {} path, _ = @formatter.generate(:path_info, nil, { :controller => "tasks", :action => "a/b c+d", }, {}) assert_equal '/tasks/a/b%20c+d', path end def test_generate_extra_params path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route nil, path, {}, {}, {} path, params = @formatter.generate(:path_info, nil, { :id => 1, :controller => "tasks", :action => "show", :relative_url_root => nil }, {}) assert_equal '/tasks/show', path assert_equal({:id => 1, :relative_url_root => nil}, params) end def test_generate_uses_recall_if_needed path = Path::Pattern.new '/:controller(/:action(/:id))' @router.routes.add_route nil, path, {}, {}, {} path, params = @formatter.generate(:path_info, nil, {:controller =>"tasks", :id => 10}, {:action =>"index"}) assert_equal '/tasks/index/10', path assert_equal({}, params) end def test_generate_with_name path = Path::Pattern.new '/:controller(/:action)' @router.routes.add_route nil, path, {}, {}, {} path, params = @formatter.generate(:path_info, "tasks", {:controller=>"tasks"}, {:controller=>"tasks", :action=>"index"}) assert_equal '/tasks', path assert_equal({}, params) end { '/content' => { :controller => 'content' }, '/content/list' => { :controller => 'content', :action => 'list' }, '/content/show/10' => { :controller => 'content', :action => 'show', :id => "10" }, }.each do |request_path, expected| define_method("test_recognize_#{expected.keys.map(&:to_s).join('_')}") do path = Path::Pattern.new "/:controller(/:action(/:id))" app = Object.new route = @router.routes.add_route(app, path, {}, {}, {}) env = rails_env 'PATH_INFO' => request_path called = false @router.recognize(env) do |r, _, params| assert_equal route, r assert_equal(expected, params) called = true end assert called end end { :segment => ['/a%2Fb%20c+d/splat', { :segment => 'a/b c+d', :splat => 'splat' }], :splat => ['/segment/a/b%20c+d', { :segment => 'segment', :splat => 'a/b c+d' }] }.each do |name, (request_path, expected)| define_method("test_recognize_#{name}") do path = Path::Pattern.new '/:segment/*splat' app = Object.new route = @router.routes.add_route(app, path, {}, {}, {}) env = rails_env 'PATH_INFO' => request_path called = false @router.recognize(env) do |r, _, params| assert_equal route, r assert_equal(expected, params) called = true end assert called end end def test_namespaced_controller strexp = Router::Strexp.new( "/:controller(/:action(/:id))", { :controller => /.+?/ }, ["/", ".", "?"] ) path = Path::Pattern.new strexp app = Object.new route = @router.routes.add_route(app, path, {}, {}, {}) env = rails_env 'PATH_INFO' => '/admin/users/show/10' called = false expected = { :controller => 'admin/users', :action => 'show', :id => '10' } @router.recognize(env) do |r, _, params| assert_equal route, r assert_equal(expected, params) called = true end assert called end def test_recognize_literal path = Path::Pattern.new "/books(/:action(.:format))" app = Object.new route = @router.routes.add_route(app, path, {}, {:controller => 'books'}) env = rails_env 'PATH_INFO' => '/books/list.rss' expected = { :controller => 'books', :action => 'list', :format => 'rss' } called = false @router.recognize(env) do |r, _, params| assert_equal route, r assert_equal(expected, params) called = true end assert called end def test_recognize_cares_about_verbs path = Path::Pattern.new "/books(/:action(.:format))" app = Object.new conditions = { :request_method => 'GET' } @router.routes.add_route(app, path, conditions, {}) conditions = conditions.dup conditions[:request_method] = 'POST' post = @router.routes.add_route(app, path, conditions, {}) env = rails_env 'PATH_INFO' => '/books/list.rss', "REQUEST_METHOD" => "POST" called = false @router.recognize(env) do |r, _, params| assert_equal post, r called = true end assert called end private def add_routes router, paths paths.each do |path| path = Path::Pattern.new path router.routes.add_route nil, path, {}, {}, {} end end RailsEnv = Struct.new(:env) def rails_env env RailsEnv.new rack_env env end def rack_env env { "rack.version" => [1, 1], "rack.input" => StringIO.new, "rack.errors" => StringIO.new, "rack.multithread" => true, "rack.multiprocess" => true, "rack.run_once" => false, "REQUEST_METHOD" => "GET", "SERVER_NAME" => "example.org", "SERVER_PORT" => "80", "QUERY_STRING" => "", "PATH_INFO" => "/content", "rack.url_scheme" => "http", "HTTPS" => "off", "SCRIPT_NAME" => "", "CONTENT_LENGTH" => "0" }.merge env end end end journey-1.0.4/test/test_routes.rb0000644000004100000410000000250111770361473017132 0ustar www-datawww-datarequire 'helper' module Journey class TestRoutes < MiniTest::Unit::TestCase def test_clear routes = Routes.new exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?'] path = Path::Pattern.new exp requirements = { :hello => /world/ } routes.add_route nil, path, requirements, {:id => nil}, {} assert_equal 1, routes.length routes.clear assert_equal 0, routes.length end def test_ast routes = Routes.new path = Path::Pattern.new '/hello' routes.add_route nil, path, {}, {}, {} ast = routes.ast routes.add_route nil, path, {}, {}, {} refute_equal ast, routes.ast end def test_simulator_changes routes = Routes.new path = Path::Pattern.new '/hello' routes.add_route nil, path, {}, {}, {} sim = routes.simulator routes.add_route nil, path, {}, {}, {} refute_equal sim, routes.simulator end # FIXME: first name *should* win, revert this after Rails 3.2 release def test_last_name_wins routes = Routes.new one = Path::Pattern.new '/hello' two = Path::Pattern.new '/aaron' routes.add_route nil, one, {}, {}, 'aaron' routes.add_route nil, two, {}, {}, 'aaron' assert_equal '/aaron', routes.named_routes['aaron'].path.spec.to_s end end end journey-1.0.4/test/router/0000755000004100000410000000000011770361473015547 5ustar www-datawww-datajourney-1.0.4/test/router/test_utils.rb0000644000004100000410000000065711770361473020303 0ustar www-datawww-datarequire 'helper' module Journey class Router class TestUtils < MiniTest::Unit::TestCase def test_path_escape assert_equal "a/b%20c+d", Utils.escape_path("a/b c+d") end def test_fragment_escape assert_equal "a/b%20c+d?e", Utils.escape_fragment("a/b c+d?e") end def test_uri_unescape assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d") end end end end journey-1.0.4/test/router/test_strexp.rb0000644000004100000410000000145411770361473020464 0ustar www-datawww-datarequire 'helper' module Journey class Router class TestStrexp < MiniTest::Unit::TestCase def test_many_names exp = Strexp.new( "/:controller(/:action(/:id(.:format)))", {:controller=>/.+?/}, ["/", ".", "?"], true) assert_equal ["controller", "action", "id", "format"], exp.names end def test_names { "/bar(.:format)" => %w{ format }, ":format" => %w{ format }, ":format-" => %w{ format }, ":format0" => %w{ format0 }, ":format1,:format2" => %w{ format1 format2 }, }.each do |string, expected| exp = Strexp.new(string, {}, ["/", ".", "?"]) assert_equal expected, exp.names end end end end end journey-1.0.4/test/gtg/0000755000004100000410000000000011770361473015010 5ustar www-datawww-datajourney-1.0.4/test/gtg/test_transition_table.rb0000644000004100000410000000532111770361473021736 0ustar www-datawww-datarequire 'helper' require 'json' module Journey module GTG class TestGeneralizedTable < MiniTest::Unit::TestCase def test_to_json table = tt %w{ /articles(.:format) /articles/new(.:format) /articles/:id/edit(.:format) /articles/:id(.:format) } json = JSON.load table.to_json assert json['regexp_states'] assert json['string_states'] assert json['accepting'] end if system("dot -V 2>/dev/null") def test_to_svg table = tt %w{ /articles(.:format) /articles/new(.:format) /articles/:id/edit(.:format) /articles/:id(.:format) } svg = table.to_svg assert svg refute_match(/DOCTYPE/, svg) end end def test_simulate_gt sim = simulator_for ['/foo', '/bar'] assert_match sim, '/foo' end def test_simulate_gt_regexp sim = simulator_for [':foo'] assert_match sim, 'foo' end def test_simulate_gt_regexp_mix sim = simulator_for ['/get', '/:method/foo'] assert_match sim, '/get' assert_match sim, '/get/foo' end def test_simulate_optional sim = simulator_for ['/foo(/bar)'] assert_match sim, '/foo' assert_match sim, '/foo/bar' refute_match sim, '/foo/' end def test_match_data path_asts = asts %w{ /get /:method/foo } paths = path_asts.dup builder = GTG::Builder.new Nodes::Or.new path_asts tt = builder.transition_table sim = GTG::Simulator.new tt match = sim.match '/get' assert_equal [paths.first], match.memos match = sim.match '/get/foo' assert_equal [paths.last], match.memos end def test_match_data_ambiguous path_asts = asts %w{ /articles(.:format) /articles/new(.:format) /articles/:id/edit(.:format) /articles/:id(.:format) } paths = path_asts.dup ast = Nodes::Or.new path_asts builder = GTG::Builder.new ast sim = GTG::Simulator.new builder.transition_table match = sim.match '/articles/new' assert_equal [paths[1], paths[3]], match.memos end private def asts paths parser = Journey::Parser.new paths.map { |x| ast = parser.parse x ast.each { |n| n.memo = ast} ast } end def tt paths x = asts paths builder = GTG::Builder.new Nodes::Or.new x builder.transition_table end def simulator_for paths GTG::Simulator.new tt(paths) end end end end journey-1.0.4/test/gtg/test_builder.rb0000644000004100000410000000340511770361473020024 0ustar www-datawww-datarequire 'helper' module Journey module GTG class TestBuilder < MiniTest::Unit::TestCase def test_following_states_multi table = tt ['a|a'] assert_equal 1, table.move([0], 'a').length end def test_following_states_multi_regexp table = tt [':a|b'] assert_equal 1, table.move([0], 'fooo').length assert_equal 2, table.move([0], 'b').length end def test_multi_path table = tt ['/:a/d', '/b/c'] [ [1, '/'], [2, 'b'], [2, '/'], [1, 'c'], ].inject([0]) { |state, (exp, sym)| new = table.move(state, sym) assert_equal exp, new.length new } end def test_match_data_ambiguous table = tt %w{ /articles(.:format) /articles/new(.:format) /articles/:id/edit(.:format) /articles/:id(.:format) } sim = NFA::Simulator.new table match = sim.match '/articles/new' assert_equal 2, match.memos.length end ## # Identical Routes may have different restrictions. def test_match_same_paths table = tt %w{ /articles/new(.:format) /articles/new(.:format) } sim = NFA::Simulator.new table match = sim.match '/articles/new' assert_equal 2, match.memos.length end private def ast strings parser = Journey::Parser.new asts = strings.map { |string| memo = Object.new ast = parser.parse string ast.each { |n| n.memo = memo } ast } Nodes::Or.new asts end def tt strings Builder.new(ast(strings)).transition_table end end end end journey-1.0.4/README.rdoc0000644000004100000410000000250311770361473015056 0ustar www-datawww-data= Journey * http://github.com/rails/journey == DESCRIPTION: Journey is a router. It routes requests. == FEATURES/PROBLEMS: * Designed for rails == SYNOPSIS: Too complex right now. :( == REQUIREMENTS: * None == INSTALL: * gem install journey == LICENSE: (The MIT License) Copyright (c) 2011 Aaron Patterson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. journey-1.0.4/Rakefile0000644000004100000410000000140511770361473014715 0ustar www-datawww-data# -*- ruby -*- require 'rubygems' require 'hoe' Hoe.plugins.delete :rubyforge Hoe.plugin :minitest Hoe.plugin :gemspec # `gem install hoe-gemspec` Hoe.plugin :git # `gem install hoe-git` Hoe.plugin :bundler # `gem install hoe-bundler` Hoe.spec 'journey' do developer('Aaron Patterson', 'aaron@tenderlovemaking.com') self.readme_file = 'README.rdoc' self.history_file = 'CHANGELOG.rdoc' self.extra_rdoc_files = FileList['*.rdoc'] self.extra_dev_deps += [ ["racc", ">= 1.4.6"], ["rdoc", "~> 3.11"], ["json"], ] end rule '.rb' => '.y' do |t| sh "racc -l -o #{t.name} #{t.source}" end task :compile => "lib/journey/parser.rb" Rake::Task[:test].prerequisites.unshift "lib/journey/parser.rb" # vim: syntax=ruby journey-1.0.4/.gemtest0000644000004100000410000000000011770361473014707 0ustar www-datawww-datajourney-1.0.4/journey.gemspec0000644000004100000410000000666511770361473016325 0ustar www-datawww-data# -*- encoding: utf-8 -*- Gem::Specification.new do |s| s.name = "journey" s.version = "1.0.4.20120614141756" s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= s.authors = ["Aaron Patterson"] s.date = "2012-06-14" s.description = "Journey is a router. It routes requests." s.email = ["aaron@tenderlovemaking.com"] s.extra_rdoc_files = ["Manifest.txt", "CHANGELOG.rdoc", "README.rdoc"] s.files = [".autotest", ".gemtest", ".travis.yml", "CHANGELOG.rdoc", "Gemfile", "Manifest.txt", "README.rdoc", "Rakefile", "journey.gemspec", "lib/journey.rb", "lib/journey/backwards.rb", "lib/journey/core-ext/hash.rb", "lib/journey/formatter.rb", "lib/journey/gtg/builder.rb", "lib/journey/gtg/simulator.rb", "lib/journey/gtg/transition_table.rb", "lib/journey/nfa/builder.rb", "lib/journey/nfa/dot.rb", "lib/journey/nfa/simulator.rb", "lib/journey/nfa/transition_table.rb", "lib/journey/nodes/node.rb", "lib/journey/parser.rb", "lib/journey/parser.y", "lib/journey/parser_extras.rb", "lib/journey/path/pattern.rb", "lib/journey/route.rb", "lib/journey/router.rb", "lib/journey/router/strexp.rb", "lib/journey/router/utils.rb", "lib/journey/routes.rb", "lib/journey/scanner.rb", "lib/journey/visitors.rb", "lib/journey/visualizer/fsm.css", "lib/journey/visualizer/fsm.js", "lib/journey/visualizer/index.html.erb", "test/gtg/test_builder.rb", "test/gtg/test_transition_table.rb", "test/helper.rb", "test/nfa/test_simulator.rb", "test/nfa/test_transition_table.rb", "test/nodes/test_symbol.rb", "test/path/test_pattern.rb", "test/route/definition/test_parser.rb", "test/route/definition/test_scanner.rb", "test/router/test_strexp.rb", "test/router/test_utils.rb", "test/test_route.rb", "test/test_router.rb", "test/test_routes.rb"] s.homepage = "http://github.com/rails/journey" s.rdoc_options = ["--main", "README.rdoc"] s.require_paths = ["lib"] s.rubyforge_project = "journey" s.rubygems_version = "1.8.22" s.summary = "Journey is a router" s.test_files = ["test/gtg/test_builder.rb", "test/gtg/test_transition_table.rb", "test/nfa/test_simulator.rb", "test/nfa/test_transition_table.rb", "test/nodes/test_symbol.rb", "test/path/test_pattern.rb", "test/route/definition/test_parser.rb", "test/route/definition/test_scanner.rb", "test/router/test_strexp.rb", "test/router/test_utils.rb", "test/test_route.rb", "test/test_router.rb", "test/test_routes.rb"] if s.respond_to? :specification_version then s.specification_version = 3 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q, ["~> 2.11"]) s.add_development_dependency(%q, [">= 1.4.6"]) s.add_development_dependency(%q, ["~> 3.11"]) s.add_development_dependency(%q, [">= 0"]) s.add_development_dependency(%q, ["~> 3.10"]) s.add_development_dependency(%q, ["~> 2.13"]) else s.add_dependency(%q, ["~> 2.11"]) s.add_dependency(%q, [">= 1.4.6"]) s.add_dependency(%q, ["~> 3.11"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, ["~> 3.10"]) s.add_dependency(%q, ["~> 2.13"]) end else s.add_dependency(%q, ["~> 2.11"]) s.add_dependency(%q, [">= 1.4.6"]) s.add_dependency(%q, ["~> 3.11"]) s.add_dependency(%q, [">= 0"]) s.add_dependency(%q, ["~> 3.10"]) s.add_dependency(%q, ["~> 2.13"]) end end journey-1.0.4/CHANGELOG.rdoc0000644000004100000410000000140011770361473015403 0ustar www-datawww-dataThu Jun 14 14:03:22 2012 Aaron Patterson * lib/journey/formatter.rb: when generating routes, skip route literals (routes that do not have replacement values like "/:controller") when matching unnamed routes. https://github.com/rails/rails/issues/6459 * test/test_router.rb: corresponding test Wed Feb 15 11:49:41 2012 Aaron Patterson * lib/journey/formatter.rb: reject non-truthy parameters parts. Fixes rails ticket #4587 Mon Jan 23 17:07:53 2012 Aaron Patterson * Added symbol? and literal? predicate methods to the ast nodes for easier AST traversal. * Made the dummy node a real class. journey-1.0.4/metadata.yml0000644000004100000410000001133711770361473015560 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: journey version: !ruby/object:Gem::Version hash: 31 prerelease: segments: - 1 - 0 - 4 version: 1.0.4 platform: ruby authors: - Aaron Patterson autorequire: bindir: bin cert_chain: [] date: 2012-06-14 00:00:00 Z dependencies: - !ruby/object:Gem::Dependency name: minitest prerelease: false requirement: &id001 !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version hash: 21 segments: - 2 - 11 version: "2.11" type: :development version_requirements: *id001 - !ruby/object:Gem::Dependency name: racc prerelease: false requirement: &id002 !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version hash: 11 segments: - 1 - 4 - 6 version: 1.4.6 type: :development version_requirements: *id002 - !ruby/object:Gem::Dependency name: rdoc prerelease: false requirement: &id003 !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version hash: 17 segments: - 3 - 11 version: "3.11" type: :development version_requirements: *id003 - !ruby/object:Gem::Dependency name: json prerelease: false requirement: &id004 !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version hash: 3 segments: - 0 version: "0" type: :development version_requirements: *id004 - !ruby/object:Gem::Dependency name: rdoc prerelease: false requirement: &id005 !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version hash: 19 segments: - 3 - 10 version: "3.10" type: :development version_requirements: *id005 - !ruby/object:Gem::Dependency name: hoe prerelease: false requirement: &id006 !ruby/object:Gem::Requirement none: false requirements: - - ~> - !ruby/object:Gem::Version hash: 25 segments: - 2 - 13 version: "2.13" type: :development version_requirements: *id006 description: Journey is a router. It routes requests. email: - aaron@tenderlovemaking.com executables: [] extensions: [] extra_rdoc_files: - Manifest.txt - CHANGELOG.rdoc - README.rdoc files: - .autotest - .gemtest - .travis.yml - CHANGELOG.rdoc - Gemfile - Manifest.txt - README.rdoc - Rakefile - journey.gemspec - lib/journey.rb - lib/journey/backwards.rb - lib/journey/core-ext/hash.rb - lib/journey/formatter.rb - lib/journey/gtg/builder.rb - lib/journey/gtg/simulator.rb - lib/journey/gtg/transition_table.rb - lib/journey/nfa/builder.rb - lib/journey/nfa/dot.rb - lib/journey/nfa/simulator.rb - lib/journey/nfa/transition_table.rb - lib/journey/nodes/node.rb - lib/journey/parser.rb - lib/journey/parser.y - lib/journey/parser_extras.rb - lib/journey/path/pattern.rb - lib/journey/route.rb - lib/journey/router.rb - lib/journey/router/strexp.rb - lib/journey/router/utils.rb - lib/journey/routes.rb - lib/journey/scanner.rb - lib/journey/visitors.rb - lib/journey/visualizer/fsm.css - lib/journey/visualizer/fsm.js - lib/journey/visualizer/index.html.erb - test/gtg/test_builder.rb - test/gtg/test_transition_table.rb - test/helper.rb - test/nfa/test_simulator.rb - test/nfa/test_transition_table.rb - test/nodes/test_symbol.rb - test/path/test_pattern.rb - test/route/definition/test_parser.rb - test/route/definition/test_scanner.rb - test/router/test_strexp.rb - test/router/test_utils.rb - test/test_route.rb - test/test_router.rb - test/test_routes.rb homepage: http://github.com/rails/journey licenses: [] post_install_message: rdoc_options: - --main - README.rdoc require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version hash: 3 segments: - 0 version: "0" required_rubygems_version: !ruby/object:Gem::Requirement none: false requirements: - - ">=" - !ruby/object:Gem::Version hash: 3 segments: - 0 version: "0" requirements: [] rubyforge_project: journey rubygems_version: 1.8.22 signing_key: specification_version: 3 summary: Journey is a router test_files: - test/gtg/test_builder.rb - test/gtg/test_transition_table.rb - test/nfa/test_simulator.rb - test/nfa/test_transition_table.rb - test/nodes/test_symbol.rb - test/path/test_pattern.rb - test/route/definition/test_parser.rb - test/route/definition/test_scanner.rb - test/router/test_strexp.rb - test/router/test_utils.rb - test/test_route.rb - test/test_router.rb - test/test_routes.rb journey-1.0.4/Gemfile0000644000004100000410000000063011770361473014542 0ustar www-datawww-data# -*- ruby -*- # DO NOT EDIT THIS FILE. Instead, edit Rakefile, and run `rake bundler:gemfile`. source :gemcutter gem "minitest", "~>2.3", :group => [:development, :test] gem "racc", ">=1.4.6", :group => [:development, :test] gem "rdoc", "~>3.11", :group => [:development, :test] gem "json", ">=0", :group => [:development, :test] gem "hoe", "~>2.12", :group => [:development, :test] # vim: syntax=ruby journey-1.0.4/Manifest.txt0000644000004100000410000000222511770361473015560 0ustar www-datawww-data.autotest .gemtest .travis.yml CHANGELOG.rdoc Gemfile Manifest.txt README.rdoc Rakefile journey.gemspec lib/journey.rb lib/journey/backwards.rb lib/journey/core-ext/hash.rb lib/journey/formatter.rb lib/journey/gtg/builder.rb lib/journey/gtg/simulator.rb lib/journey/gtg/transition_table.rb lib/journey/nfa/builder.rb lib/journey/nfa/dot.rb lib/journey/nfa/simulator.rb lib/journey/nfa/transition_table.rb lib/journey/nodes/node.rb lib/journey/parser.rb lib/journey/parser.y lib/journey/parser_extras.rb lib/journey/path/pattern.rb lib/journey/route.rb lib/journey/router.rb lib/journey/router/strexp.rb lib/journey/router/utils.rb lib/journey/routes.rb lib/journey/scanner.rb lib/journey/visitors.rb lib/journey/visualizer/fsm.css lib/journey/visualizer/fsm.js lib/journey/visualizer/index.html.erb test/gtg/test_builder.rb test/gtg/test_transition_table.rb test/helper.rb test/nfa/test_simulator.rb test/nfa/test_transition_table.rb test/nodes/test_symbol.rb test/path/test_pattern.rb test/route/definition/test_parser.rb test/route/definition/test_scanner.rb test/router/test_strexp.rb test/router/test_utils.rb test/test_route.rb test/test_router.rb test/test_routes.rb journey-1.0.4/.autotest0000644000004100000410000000026111770361473015120 0ustar www-datawww-data# -*- ruby -*- require 'autotest/restart' Autotest.add_hook :initialize do |at| at.testlib = 'minitest/autorun' at.find_directories = ARGV unless ARGV.empty? end journey-1.0.4/lib/0000755000004100000410000000000011770361473014016 5ustar www-datawww-datajourney-1.0.4/lib/journey.rb0000644000004100000410000000022511770361473016035 0ustar www-datawww-datarequire 'journey/router' require 'journey/gtg/builder' require 'journey/gtg/simulator' require 'journey/nfa/builder' require 'journey/nfa/simulator' journey-1.0.4/lib/journey/0000755000004100000410000000000011770361473015511 5ustar www-datawww-datajourney-1.0.4/lib/journey/parser_extras.rb0000644000004100000410000000050711770361473020722 0ustar www-datawww-datarequire 'journey/scanner' require 'journey/nodes/node' module Journey class Parser < Racc::Parser include Journey::Nodes def initialize @scanner = Scanner.new end def parse string @scanner.scan_setup string do_parse end def next_token @scanner.next_token end end end journey-1.0.4/lib/journey/path/0000755000004100000410000000000011770361473016445 5ustar www-datawww-datajourney-1.0.4/lib/journey/path/pattern.rb0000644000004100000410000001032511770361473020450 0ustar www-datawww-datamodule Journey module Path class Pattern attr_reader :spec, :requirements, :anchored def initialize strexp parser = Journey::Parser.new @anchored = true case strexp when String @spec = parser.parse strexp @requirements = {} @separators = "/.?" when Router::Strexp @spec = parser.parse strexp.path @requirements = strexp.requirements @separators = strexp.separators.join @anchored = strexp.anchor else raise "wtf bro: #{strexp}" end @names = nil @optional_names = nil @required_names = nil @re = nil @offsets = nil end def ast @spec.grep(Nodes::Symbol).each do |node| re = @requirements[node.to_sym] node.regexp = re if re end @spec.grep(Nodes::Star).each do |node| node = node.left node.regexp = @requirements[node.to_sym] || /(.+)/ end @spec end def names @names ||= spec.grep(Nodes::Symbol).map { |n| n.name } end def required_names @required_names ||= names - optional_names end def optional_names @optional_names ||= spec.grep(Nodes::Group).map { |group| group.grep(Nodes::Symbol) }.flatten.map { |n| n.name }.uniq end class RegexpOffsets < Journey::Visitors::Visitor # :nodoc: attr_reader :offsets def initialize matchers @matchers = matchers @capture_count = [0] end def visit node super @capture_count end def visit_SYMBOL node node = node.to_sym if @matchers.key? node re = /#{@matchers[node]}|/ @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0)) else @capture_count << (@capture_count.last || 0) end end end class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc: def initialize separator, matchers @separator = separator @matchers = matchers @separator_re = "([^#{separator}]+)" super() end def accept node %r{\A#{visit node}\Z} end def visit_CAT node [visit(node.left), visit(node.right)].join end def visit_SYMBOL node node = node.to_sym return @separator_re unless @matchers.key? node re = @matchers[node] "(#{re})" end def visit_GROUP node "(?:#{visit node.left})?" end def visit_LITERAL node Regexp.escape node.left end alias :visit_DOT :visit_LITERAL def visit_SLASH node node.left end def visit_STAR node re = @matchers[node.left.to_sym] || '.+' "(#{re})" end end class UnanchoredRegexp < AnchoredRegexp # :nodoc: def accept node %r{\A#{visit node}} end end class MatchData attr_reader :names def initialize names, offsets, match @names = names @offsets = offsets @match = match end def captures (length - 1).times.map { |i| self[i + 1] } end def [] x idx = @offsets[x - 1] + x @match[idx] end def length @offsets.length end def post_match @match.post_match end def to_s @match.to_s end end def match other return unless match = to_regexp.match(other) MatchData.new names, offsets, match end alias :=~ :match def source to_regexp.source end def to_regexp @re ||= regexp_visitor.new(@separators, @requirements).accept spec end private def regexp_visitor @anchored ? AnchoredRegexp : UnanchoredRegexp end def offsets return @offsets if @offsets viz = RegexpOffsets.new @requirements @offsets = viz.accept spec end end end end journey-1.0.4/lib/journey/nfa/0000755000004100000410000000000011770361473016255 5ustar www-datawww-datajourney-1.0.4/lib/journey/nfa/builder.rb0000644000004100000410000000260611770361473020234 0ustar www-datawww-datarequire 'journey/nfa/transition_table' require 'journey/gtg/transition_table' module Journey module NFA class Visitor < Visitors::Visitor def initialize tt @tt = tt @i = -1 end def visit_CAT node left = visit node.left right = visit node.right @tt.merge left.last, right.first [left.first, right.last] end def visit_GROUP node from = @i += 1 left = visit node.left to = @i += 1 @tt.accepting = to @tt[from, left.first] = nil @tt[left.last, to] = nil @tt[from, to] = nil [from, to] end def visit_OR node from = @i += 1 children = node.children.map { |c| visit c } to = @i += 1 children.each do |child| @tt[from, child.first] = nil @tt[child.last, to] = nil end @tt.accepting = to [from, to] end def terminal node from_i = @i += 1 # new state to_i = @i += 1 # new state @tt[from_i, to_i] = node @tt.accepting = to_i @tt.add_memo to_i, node.memo [from_i, to_i] end end class Builder def initialize ast @ast = ast end def transition_table tt = TransitionTable.new Visitor.new(tt).accept @ast tt end end end end journey-1.0.4/lib/journey/nfa/transition_table.rb0000644000004100000410000000723311770361473022150 0ustar www-datawww-datarequire 'journey/nfa/dot' module Journey module NFA class TransitionTable include Journey::NFA::Dot attr_accessor :accepting attr_reader :memos def initialize @table = Hash.new { |h,f| h[f] = {} } @memos = {} @accepting = nil @inverted = nil end def accepting? state accepting == state end def accepting_states [accepting] end def add_memo idx, memo @memos[idx] = memo end def memo idx @memos[idx] end def []= i, f, s @table[f][i] = s end def merge left, right @memos[right] = @memos.delete left @table[right] = @table.delete(left) end def states (@table.keys + @table.values.map(&:keys).flatten).uniq end ### # Returns a generalized transition graph with reduced states. The states # are reduced like a DFA, but the table must be simulated like an NFA. # # Edges of the GTG are regular expressions def generalized_table gt = GTG::TransitionTable.new marked = {} state_id = Hash.new { |h,k| h[k] = h.length } alphabet = self.alphabet stack = [eclosure(0)] until stack.empty? state = stack.pop next if marked[state] || state.empty? marked[state] = true alphabet.each do |alpha| next_state = eclosure(following_states(state, alpha)) next if next_state.empty? gt[state_id[state], state_id[next_state]] = alpha stack << next_state end end final_groups = state_id.keys.find_all { |s| s.sort.last == accepting } final_groups.each do |states| id = state_id[states] gt.add_accepting id save = states.find { |s| @memos.key?(s) && eclosure(s).sort.last == accepting } gt.add_memo id, memo(save) end gt end ### # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def following_states t, a Array(t).map { |s| inverted[s][a] }.flatten.uniq end ### # Returns set of NFA states to which there is a transition on ast symbol # +a+ from some state +s+ in +t+. def move t, a Array(t).map { |s| inverted[s].keys.compact.find_all { |sym| sym === a }.map { |sym| inverted[s][sym] } }.flatten.uniq end def alphabet inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s } end ### # Returns a set of NFA states reachable from some NFA state +s+ in set # +t+ on nil-transitions alone. def eclosure t stack = Array(t) seen = {} children = [] until stack.empty? s = stack.pop next if seen[s] seen[s] = true children << s stack.concat inverted[s][nil] end children.uniq end def transitions @table.map { |to, hash| hash.map { |from, sym| [from, sym, to] } }.flatten(1) end private def inverted return @inverted if @inverted @inverted = Hash.new { |h,from| h[from] = Hash.new { |j,s| j[s] = [] } } @table.each { |to, hash| hash.each { |from, sym| if sym sym = Nodes::Symbol === sym ? sym.regexp : sym.left end @inverted[from][sym] << to } } @inverted end end end end journey-1.0.4/lib/journey/nfa/dot.rb0000644000004100000410000000141711770361473017373 0ustar www-datawww-data# encoding: utf-8 module Journey module NFA module Dot def to_dot edges = transitions.map { |from, sym, to| " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];" } #memo_nodes = memos.values.flatten.map { |n| # label = n # if Journey::Route === n # label = "#{n.verb.source} #{n.path.spec}" # end # " #{n.object_id} [label=\"#{label}\", shape=box];" #} #memo_edges = memos.map { |k, memos| # (memos || []).map { |v| " #{k} -> #{v.object_id};" } #}.flatten.uniq <<-eodot digraph nfa { rankdir=LR; node [shape = doublecircle]; #{accepting_states.join ' '}; node [shape = circle]; #{edges.join "\n"} } eodot end end end end journey-1.0.4/lib/journey/nfa/simulator.rb0000644000004100000410000000163111770361473020622 0ustar www-datawww-datarequire 'strscan' module Journey module NFA class MatchData attr_reader :memos def initialize memos @memos = memos end end class Simulator attr_reader :tt def initialize transition_table @tt = transition_table end def simulate string input = StringScanner.new string state = tt.eclosure 0 until input.eos? sym = input.scan(/[\/\.\?]|[^\/\.\?]+/) # FIXME: tt.eclosure is not needed for the GTG state = tt.eclosure tt.move(state, sym) end acceptance_states = state.find_all { |s| tt.accepting? tt.eclosure(s).sort.last } return if acceptance_states.empty? memos = acceptance_states.map { |x| tt.memo x }.flatten.compact MatchData.new memos end alias :=~ :simulate alias :match :simulate end end end journey-1.0.4/lib/journey/route.rb0000644000004100000410000000464611770361473017206 0ustar www-datawww-datamodule Journey class Route attr_reader :app, :path, :verb, :defaults, :ip, :name attr_reader :constraints alias :conditions :constraints attr_accessor :precedence ## # +path+ is a path constraint. # +constraints+ is a hash of constraints to be applied to this route. def initialize name, app, path, constraints, defaults = {} constraints = constraints.dup @name = name @app = app @path = path @verb = constraints[:request_method] || // @ip = constraints.delete(:ip) || // @constraints = constraints @constraints.keep_if { |_,v| Regexp === v || String === v } @defaults = defaults @required_defaults = nil @required_parts = nil @parts = nil @decorated_ast = nil @precedence = 0 end def ast return @decorated_ast if @decorated_ast @decorated_ast = path.ast @decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self } @decorated_ast end def requirements # :nodoc: # needed for rails `rake routes` path.requirements.merge(@defaults).delete_if { |_,v| /.+?/ == v } end def segments @path.names end def required_keys path.required_names.map { |x| x.to_sym } + required_defaults.keys end def score constraints required_keys = path.required_names supplied_keys = constraints.map { |k,v| v && k.to_s }.compact return -1 unless (required_keys - supplied_keys).empty? score = (supplied_keys & path.names).length score + (required_defaults.length * 2) end def parts @parts ||= segments.map { |n| n.to_sym } end alias :segment_keys :parts def format path_options (defaults.keys - required_parts).each do |key| path_options.delete key if defaults[key].to_s == path_options[key].to_s end formatter = Visitors::Formatter.new(path_options) formatted_path = formatter.accept(path.spec) formatted_path.gsub(/\/\x00/, '') end def optional_parts path.optional_names.map { |n| n.to_sym } end def required_parts @required_parts ||= path.required_names.map { |n| n.to_sym } end def required_defaults @required_defaults ||= begin matches = parts @defaults.dup.delete_if { |k,_| matches.include? k } end end end end journey-1.0.4/lib/journey/routes.rb0000644000004100000410000000310711770361473017360 0ustar www-datawww-datamodule Journey ### # The Routing table. Contains all routes for a system. Routes can be # added to the table by calling Routes#add_route class Routes include Enumerable attr_reader :routes, :named_routes def initialize @routes = [] @named_routes = {} @ast = nil @partitioned_routes = nil @simulator = nil end def length @routes.length end alias :size :length def last @routes.last end def each(&block) routes.each(&block) end def clear routes.clear end def partitioned_routes @partitioned_routes ||= routes.partition { |r| r.path.anchored && r.ast.grep(Nodes::Symbol).all? { |n| n.default_regexp? } } end def ast return @ast if @ast return if partitioned_routes.first.empty? asts = partitioned_routes.first.map { |r| r.ast } @ast = Nodes::Or.new(asts) end def simulator return @simulator if @simulator gtg = GTG::Builder.new(ast).transition_table @simulator = GTG::Simulator.new gtg end ### # Add a route to the routing table. def add_route app, path, conditions, defaults, name = nil route = Route.new(name, app, path, conditions, defaults) route.precedence = routes.length routes << route named_routes[name] = route if name clear_cache! route end private def clear_cache! @ast = nil @partitioned_routes = nil @simulator = nil end end end journey-1.0.4/lib/journey/nodes/0000755000004100000410000000000011770361473016621 5ustar www-datawww-datajourney-1.0.4/lib/journey/nodes/node.rb0000644000004100000410000000372111770361473020076 0ustar www-datawww-datarequire 'journey/visitors' module Journey module Nodes class Node # :nodoc: include Enumerable attr_accessor :left, :memo def initialize left @left = left @memo = nil end def each(&block) Visitors::Each.new(block).accept(self) end def to_s Visitors::String.new.accept(self) end def to_dot Visitors::Dot.new.accept(self) end def to_sym name.to_sym end def name left.tr ':', '' end def type raise NotImplementedError end def symbol?; false; end def literal?; false; end end class Terminal < Node alias :symbol :left end class Literal < Terminal def literal?; true; end def type; :LITERAL; end end class Dummy < Literal def initialize x = Object.new super end def literal?; false; end end %w{ Symbol Slash Dot }.each do |t| class_eval %{ class #{t} < Terminal def type; :#{t.upcase}; end end } end class Symbol < Terminal attr_accessor :regexp alias :symbol :regexp DEFAULT_EXP = /[^\.\/\?]+/ def initialize left super @regexp = DEFAULT_EXP end def default_regexp? regexp == DEFAULT_EXP end def symbol?; true; end end class Unary < Node def children; [value] end end class Group < Unary def type; :GROUP; end end class Star < Unary def type; :STAR; end end class Binary < Node attr_accessor :right def initialize left, right super(left) @right = right end def children; [left, right] end end class Cat < Binary def type; :CAT; end end class Or < Node attr_reader :children def initialize children @children = children end def type; :OR; end end end end journey-1.0.4/lib/journey/backwards.rb0000644000004100000410000000020611770361473017775 0ustar www-datawww-datamodule Rack Mount = Journey::Router Mount::RouteSet = Journey::Router Mount::RegexpWithNamedGroups = Journey::Path::Pattern end journey-1.0.4/lib/journey/visualizer/0000755000004100000410000000000011770361473017706 5ustar www-datawww-datajourney-1.0.4/lib/journey/visualizer/index.html.erb0000644000004100000410000000325411770361473022456 0ustar www-datawww-data <%= title %>

Routes FSM with NFA simulation

Type a route in to the box and click "simulate".

Some fun routes to try: <% fun_routes.each do |path| %> <%= path %> <% end %>

<%= svg %>

This is a FSM for a system that has the following routes:

    <% paths.each do |route| %>
  • <%= route %>
  • <% end %>
<% javascripts.each do |js| %> <% end %> journey-1.0.4/lib/journey/visualizer/fsm.css0000644000004100000410000000151311770361473021205 0ustar www-datawww-databody { font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif; margin: 0; } h1 { font-size: 2.0em; font-weight: bold; text-align: center; color: white; background-color: black; padding: 5px 0; margin: 0 0 20px; } h2 { text-align: center; display: none; font-size: 0.5em; } div#chart-2 { height: 350px; } .clearfix {display: inline-block; } .input { overflow: show;} .instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em} .instruction p { padding: 0 0 5px; } .instruction li { padding: 0 10px 5px; } .form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px} .form p, .form form { text-align: center } .form form {padding: 0 10px 5px; } .form .fun_routes { font-size: 0.9em;} .form .fun_routes a { margin: 0 5px 0 0; } journey-1.0.4/lib/journey/visualizer/fsm.js0000644000004100000410000000637311770361473021042 0ustar www-datawww-datafunction tokenize(input, callback) { while(input.length > 0) { callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]); input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, ''); } } var graph = d3.select("#chart-2 svg"); var svg_edges = {}; var svg_nodes = {}; graph.selectAll("g.edge").each(function() { var node = d3.select(this); var index = node.select("title").text().split("->"); var left = parseInt(index[0]); var right = parseInt(index[1]); if(!svg_edges[left]) { svg_edges[left] = {} } svg_edges[left][right] = node; }); graph.selectAll("g.node").each(function() { var node = d3.select(this); var index = parseInt(node.select("title").text()); svg_nodes[index] = node; }); function reset_graph() { for(var key in svg_edges) { for(var mkey in svg_edges[key]) { var node = svg_edges[key][mkey]; var path = node.select("path"); var arrow = node.select("polygon"); path.style("stroke", "black"); arrow.style("stroke", "black").style("fill", "black"); } } for(var key in svg_nodes) { var node = svg_nodes[key]; node.select('ellipse').style("fill", "white"); node.select('polygon').style("fill", "white"); } return false; } function highlight_edge(from, to) { var node = svg_edges[from][to]; var path = node.select("path"); var arrow = node.select("polygon"); path .transition().duration(500) .style("stroke", "green"); arrow .transition().duration(500) .style("stroke", "green").style("fill", "green"); } function highlight_state(index, color) { if(!color) { color = "green"; } svg_nodes[index].select('ellipse') .style("fill", "white") .transition().duration(500) .style("fill", color); } function highlight_finish(index) { svg_nodes[index].select('polygon') .style("fill", "while") .transition().duration(500) .style("fill", "blue"); } function match(input) { reset_graph(); var table = tt(); var states = [0]; var regexp_states = table['regexp_states']; var string_states = table['string_states']; var accepting = table['accepting']; highlight_state(0); tokenize(input, function(token) { var new_states = []; for(var key in states) { var state = states[key]; if(string_states[state] && string_states[state][token]) { var new_state = string_states[state][token]; highlight_edge(state, new_state); highlight_state(new_state); new_states.push(new_state); } if(regexp_states[state]) { for(var key in regexp_states[state]) { var re = new RegExp("^" + key + "$"); if(re.test(token)) { var new_state = regexp_states[state][key]; highlight_edge(state, new_state); highlight_state(new_state); new_states.push(new_state); } } } } if(new_states.length == 0) { return; } states = new_states; }); for(var key in states) { var state = states[key]; if(accepting[state]) { for(var mkey in svg_edges[state]) { if(!regexp_states[mkey] && !string_states[mkey]) { highlight_edge(state, mkey); highlight_finish(mkey); } } } else { highlight_state(state, "red"); } } return false; } journey-1.0.4/lib/journey/parser.rb0000644000004100000410000001011611770361473017331 0ustar www-datawww-data# # DO NOT MODIFY!!!! # This file is automatically generated by Racc 1.4.6 # from Racc grammer file "". # require 'racc/parser.rb' require 'journey/parser_extras' module Journey class Parser < Racc::Parser ##### State transition tables begin ### racc_action_table = [ 17, 22, 13, 15, 14, 7, 15, 16, 8, 19, 13, 15, 14, 7, 24, 16, 8, 19, 13, 15, 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, 16, 8, 13, 15, 14, 7, nil, 16, 8 ] racc_action_check = [ 1, 17, 1, 1, 1, 1, 8, 1, 1, 1, 20, 20, 20, 20, 20, 20, 20, 20, 7, 7, 7, 7, nil, 7, 7, 19, 19, 19, 19, nil, 19, 19, 0, 0, 0, 0, nil, 0, 0 ] racc_action_pointer = [ 30, 0, nil, nil, nil, nil, nil, 16, 3, nil, nil, nil, nil, nil, nil, nil, nil, 1, nil, 23, 8, nil, nil, nil, nil ] racc_action_default = [ -18, -18, -2, -3, -4, -5, -6, -18, -18, -10, -11, -12, -13, -14, -15, -16, -17, -18, -1, -18, -18, -9, 25, -8, -7 ] racc_goto_table = [ 18, 1, 21, nil, nil, nil, nil, nil, 20, nil, nil, nil, nil, nil, nil, nil, nil, nil, 23, 18 ] racc_goto_check = [ 2, 1, 7, nil, nil, nil, nil, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ] racc_goto_pointer = [ nil, 1, -1, nil, nil, nil, nil, -6, nil, nil, nil ] racc_goto_default = [ nil, nil, 2, 3, 4, 5, 6, 10, 9, 11, 12 ] racc_reduce_table = [ 0, 0, :racc_error, 2, 11, :_reduce_1, 1, 11, :_reduce_2, 1, 11, :_reduce_none, 1, 12, :_reduce_none, 1, 12, :_reduce_none, 1, 12, :_reduce_none, 3, 15, :_reduce_7, 3, 13, :_reduce_8, 2, 16, :_reduce_9, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 14, :_reduce_none, 1, 19, :_reduce_14, 1, 18, :_reduce_15, 1, 17, :_reduce_16, 1, 20, :_reduce_17 ] racc_reduce_n = 18 racc_shift_n = 25 racc_token_table = { false => 0, :error => 1, :SLASH => 2, :LITERAL => 3, :SYMBOL => 4, :LPAREN => 5, :RPAREN => 6, :DOT => 7, :STAR => 8, :OR => 9 } racc_nt_base = 10 racc_use_result_var = true Racc_arg = [ racc_action_table, racc_action_check, racc_action_default, racc_action_pointer, racc_goto_table, racc_goto_check, racc_goto_default, racc_goto_pointer, racc_nt_base, racc_reduce_table, racc_token_table, racc_shift_n, racc_reduce_n, racc_use_result_var ] Racc_token_to_s_table = [ "$end", "error", "SLASH", "LITERAL", "SYMBOL", "LPAREN", "RPAREN", "DOT", "STAR", "OR", "$start", "expressions", "expression", "or", "terminal", "group", "star", "literal", "symbol", "slash", "dot" ] Racc_debug_parser = false ##### State transition tables end ##### # reduce 0 omitted def _reduce_1(val, _values, result) result = Cat.new(val.first, val.last) result end def _reduce_2(val, _values, result) result = val.first result end # reduce 3 omitted # reduce 4 omitted # reduce 5 omitted # reduce 6 omitted def _reduce_7(val, _values, result) result = Group.new(val[1]) result end def _reduce_8(val, _values, result) result = Or.new([val.first, val.last]) result end def _reduce_9(val, _values, result) result = Star.new(Symbol.new(val.last.left)) result end # reduce 10 omitted # reduce 11 omitted # reduce 12 omitted # reduce 13 omitted def _reduce_14(val, _values, result) result = Slash.new('/') result end def _reduce_15(val, _values, result) result = Symbol.new(val.first) result end def _reduce_16(val, _values, result) result = Literal.new(val.first) result end def _reduce_17(val, _values, result) result = Dot.new(val.first) result end def _reduce_none(val, _values, result) val[0] end end # class Parser end # module Journey journey-1.0.4/lib/journey/scanner.rb0000644000004100000410000000174311770361473017474 0ustar www-datawww-datarequire 'strscan' module Journey class Scanner def initialize @ss = nil end def scan_setup str @ss = StringScanner.new str end def eos? @ss.eos? end def pos @ss.pos end def pre_match @ss.pre_match end def next_token return if @ss.eos? until token = scan || @ss.eos?; end token end private def scan case # / when text = @ss.scan(/\//) [:SLASH, text] when text = @ss.scan(/\*/) [:STAR, text] when text = @ss.scan(/\(/) [:LPAREN, text] when text = @ss.scan(/\)/) [:RPAREN, text] when text = @ss.scan(/\|/) [:OR, text] when text = @ss.scan(/\./) [:DOT, text] when text = @ss.scan(/:\w+/) [:SYMBOL, text] when text = @ss.scan(/[\w%\-~]+/) [:LITERAL, text] # any char when text = @ss.scan(/./) [:LITERAL, text] end end end end journey-1.0.4/lib/journey/parser.y0000644000004100000410000000165611770361473017207 0ustar www-datawww-dataclass Journey::Parser token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR rule expressions : expressions expression { result = Cat.new(val.first, val.last) } | expression { result = val.first } | or ; expression : terminal | group | star ; group : LPAREN expressions RPAREN { result = Group.new(val[1]) } ; or : expressions OR expression { result = Or.new([val.first, val.last]) } ; star : STAR literal { result = Star.new(Symbol.new(val.last.left)) } ; terminal : symbol | literal | slash | dot ; slash : SLASH { result = Slash.new('/') } ; symbol : SYMBOL { result = Symbol.new(val.first) } ; literal : LITERAL { result = Literal.new(val.first) } dot : DOT { result = Dot.new(val.first) } ; end ---- header require 'journey/parser_extras' journey-1.0.4/lib/journey/visitors.rb0000644000004100000410000000703411770361473017724 0ustar www-datawww-data# encoding: utf-8 module Journey module Visitors class Visitor # :nodoc: DISPATCH_CACHE = Hash.new { |h,k| h[k] = "visit_#{k}" } def accept node visit node end private def visit node send DISPATCH_CACHE[node.type], node end def binary node visit node.left visit node.right end def visit_CAT(n); binary(n); end def nary node node.children.each { |c| visit c } end def visit_OR(n); nary(n); end def unary node visit node.left end def visit_GROUP(n); unary(n); end def visit_STAR(n); unary(n); end def terminal node; end %w{ LITERAL SYMBOL SLASH DOT }.each do |t| class_eval %{ def visit_#{t}(n); terminal(n); end }, __FILE__, __LINE__ end end ## # Loop through the requirements AST class Each < Visitor # :nodoc: attr_reader :block def initialize block @block = block end def visit node super block.call node end end class String < Visitor private def binary node [visit(node.left), visit(node.right)].join end def nary node node.children.map { |c| visit c }.join '|' end def terminal node node.left end def visit_STAR node "*" + super end def visit_GROUP node "(#{visit node.left})" end end ### # Used for formatting urls (url_for) class Formatter < Visitor attr_reader :options, :consumed def initialize options @options = options @consumed = {} end private def visit_GROUP node if consumed == options nil else route = visit node.left route.include?("\0") ? nil : route end end def terminal node node.left end def binary node [visit(node.left), visit(node.right)].join end def nary node node.children.map { |c| visit c }.join end def visit_SYMBOL node key = node.to_sym if value = options[key] consumed[key] = value Router::Utils.escape_path(value) else "\0" end end end class Dot < Visitor def initialize @nodes = [] @edges = [] end def accept node super <<-eodot digraph parse_tree { size="8,5" node [shape = none]; edge [dir = none]; #{@nodes.join "\n"} #{@edges.join("\n")} } eodot end private def binary node node.children.each do |c| @edges << "#{node.object_id} -> #{c.object_id};" end super end def nary node node.children.each do |c| @edges << "#{node.object_id} -> #{c.object_id};" end super end def unary node @edges << "#{node.object_id} -> #{node.left.object_id};" super end def visit_GROUP node @nodes << "#{node.object_id} [label=\"()\"];" super end def visit_CAT node @nodes << "#{node.object_id} [label=\"○\"];" super end def visit_STAR node @nodes << "#{node.object_id} [label=\"*\"];" super end def visit_OR node @nodes << "#{node.object_id} [label=\"|\"];" super end def terminal node value = node.left @nodes << "#{node.object_id} [label=\"#{value}\"];" end end end end journey-1.0.4/lib/journey/core-ext/0000755000004100000410000000000011770361473017237 5ustar www-datawww-datajourney-1.0.4/lib/journey/core-ext/hash.rb0000644000004100000410000000022411770361473020505 0ustar www-datawww-data# :stopdoc: if RUBY_VERSION < '1.9' class Hash def keep_if each do |k,v| delete(k) unless yield(k,v) end end end end # :startdoc: journey-1.0.4/lib/journey/formatter.rb0000644000004100000410000000601611770361473020044 0ustar www-datawww-datamodule Journey ### # The Formatter class is used for formatting URLs. For example, parameters # passed to +url_for+ in rails will eventually call Formatter#generate class Formatter attr_reader :routes def initialize routes @routes = routes @cache = nil end def generate key, name, options, recall = {}, parameterize = nil constraints = recall.merge options match_route(name, constraints) do |route| data = constraints.dup keys_to_keep = route.parts.reverse.drop_while { |part| !options.key?(part) || (options[part] || recall[part]).nil? } | route.required_parts (data.keys - keys_to_keep).each do |bad_key| data.delete bad_key end parameterized_parts = data.dup if parameterize parameterized_parts.each do |k,v| parameterized_parts[k] = parameterize.call(k, v) end end parameterized_parts.keep_if { |_,v| v } next if !name && route.requirements.empty? && route.parts.empty? next unless verify_required_parts!(route, parameterized_parts) z = Hash[options.to_a - data.to_a - route.defaults.to_a] return [route.format(parameterized_parts), z] end raise Router::RoutingError end def clear @cache = nil end private def named_routes routes.named_routes end def match_route name, options if named_routes.key? name yield named_routes[name] else #routes = possibles(@cache, options.to_a) routes = non_recursive(cache, options.to_a) hash = routes.group_by { |_, r| r.score options } hash.keys.sort.reverse_each do |score| next if score < 0 hash[score].sort_by { |i,_| i }.each do |_,route| yield route end end end end def non_recursive cache, options routes = [] stack = [cache] while stack.any? c = stack.shift routes.concat c[:___routes] if c.key? :___routes options.each do |pair| stack << c[pair] if c.key? pair end end routes end def possibles cache, options, depth = 0 cache.fetch(:___routes) { [] } + options.find_all { |pair| cache.key? pair }.map { |pair| possibles(cache[pair], options, depth + 1) }.flatten(1) end def verify_required_parts! route, parts tests = route.path.requirements route.required_parts.all? { |key| if tests.key? key /\A#{tests[key]}\Z/ === parts[key] else parts.fetch(key) { false } end } end def build_cache kash = {} routes.each_with_index do |route, i| money = kash route.required_defaults.each do |tuple| hash = (money[tuple] ||= {}) money = hash end (money[:___routes] ||= []) << [i, route] end kash end def cache @cache ||= build_cache end end end journey-1.0.4/lib/journey/router.rb0000644000004100000410000000677111770361473017371 0ustar www-datawww-datarequire 'journey/core-ext/hash' require 'journey/router/utils' require 'journey/router/strexp' require 'journey/routes' require 'journey/formatter' before = $-w $-w = false require 'journey/parser' $-w = before require 'journey/route' require 'journey/path/pattern' module Journey class Router class RoutingError < ::StandardError end VERSION = '1.0.4' class NullReq # :nodoc: attr_reader :env def initialize env @env = env end def request_method env['REQUEST_METHOD'] end def path_info env['PATH_INFO'] end def ip env['REMOTE_ADDR'] end def [](k); env[k]; end end attr_reader :request_class, :formatter attr_accessor :routes def initialize routes, options @options = options @params_key = options[:parameters_key] @request_class = options[:request_class] || NullReq @routes = routes end def call env env['PATH_INFO'] = Utils.normalize_path env['PATH_INFO'] find_routes(env).each do |match, parameters, route| script_name, path_info, set_params = env.values_at('SCRIPT_NAME', 'PATH_INFO', @params_key) unless route.path.anchored env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/') env['PATH_INFO'] = Utils.normalize_path(match.post_match) end env[@params_key] = (set_params || {}).merge parameters status, headers, body = route.app.call(env) if 'pass' == headers['X-Cascade'] env['SCRIPT_NAME'] = script_name env['PATH_INFO'] = path_info env[@params_key] = set_params next end return [status, headers, body] end return [404, {'X-Cascade' => 'pass'}, ['Not Found']] end def recognize req find_routes(req.env).each do |match, parameters, route| unless route.path.anchored req.env['SCRIPT_NAME'] = match.to_s req.env['PATH_INFO'] = match.post_match.sub(/^([^\/])/, '/\1') end yield(route, nil, parameters) end end def visualizer tt = GTG::Builder.new(ast).transition_table groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s } asts = groups.values.map { |v| v.first } tt.visualizer asts end private def partitioned_routes routes.partitioned_routes end def ast routes.ast end def simulator routes.simulator end def custom_routes partitioned_routes.last end def filter_routes path return [] unless ast data = simulator.match(path) data ? data.memos : [] end def find_routes env req = request_class.new env routes = filter_routes(req.path_info) + custom_routes.find_all { |r| r.path.match(req.path_info) } routes.sort_by(&:precedence).find_all { |r| r.constraints.all? { |k,v| v === req.send(k) } && r.verb === req.request_method }.reject { |r| req.ip && !(r.ip === req.ip) }.map { |r| match_data = r.path.match(req.path_info) match_names = match_data.names.map { |n| n.to_sym } match_values = match_data.captures.map { |v| v && Utils.unescape_uri(v) } info = Hash[match_names.zip(match_values).find_all { |_,y| y }] [match_data, r.defaults.merge(info), r] } end end end journey-1.0.4/lib/journey/router/0000755000004100000410000000000011770361473017031 5ustar www-datawww-datajourney-1.0.4/lib/journey/router/utils.rb0000644000004100000410000000336611770361473020526 0ustar www-datawww-datarequire 'uri' module Journey class Router class Utils # Normalizes URI path. # # Strips off trailing slash and ensures there is a leading slash. # # normalize_path("/foo") # => "/foo" # normalize_path("/foo/") # => "/foo" # normalize_path("foo") # => "/foo" # normalize_path("") # => "/" def self.normalize_path(path) path = "/#{path}" path.squeeze!('/') path.sub!(%r{/+\Z}, '') path = '/' if path == '' path end # URI path and fragment escaping # http://tools.ietf.org/html/rfc3986 module UriEscape # Symbol captures can generate multiple path segments, so include /. reserved_segment = '/' reserved_fragment = '/?' reserved_pchar = ':@&=+$,;%' safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}" safe_segment = "#{safe_pchar}#{reserved_segment}" safe_fragment = "#{safe_pchar}#{reserved_fragment}" if RUBY_VERSION >= '1.9' UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze else UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false, 'N').freeze UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false, 'N').freeze end end Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI def self.escape_path(path) Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT) end def self.escape_fragment(fragment) Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT) end def self.unescape_uri(uri) Parser.unescape(uri) end end end end journey-1.0.4/lib/journey/router/strexp.rb0000644000004100000410000000073311770361473020706 0ustar www-datawww-datamodule Journey class Router class Strexp class << self alias :compile :new end attr_reader :path, :requirements, :separators, :anchor def initialize path, requirements, separators, anchor = true @path = path @requirements = requirements @separators = separators @anchor = anchor end def names @path.scan(/:\w+/).map { |s| s.tr(':', '') } end end end end journey-1.0.4/lib/journey/gtg/0000755000004100000410000000000011770361473016272 5ustar www-datawww-datajourney-1.0.4/lib/journey/gtg/builder.rb0000644000004100000410000000754011770361473020253 0ustar www-datawww-datarequire 'journey/gtg/transition_table' module Journey module GTG class Builder DUMMY = Nodes::Dummy.new # :nodoc: attr_reader :root, :ast, :endpoints def initialize root @root = root @ast = Nodes::Cat.new root, DUMMY @followpos = nil end def transition_table dtrans = TransitionTable.new marked = {} state_id = Hash.new { |h,k| h[k] = h.length } start = firstpos(root) dstates = [start] until dstates.empty? s = dstates.shift next if marked[s] marked[s] = true # mark s s.group_by { |state| symbol(state) }.each do |sym, ps| u = ps.map { |l| followpos(l) }.flatten next if u.empty? if u.uniq == [DUMMY] from = state_id[s] to = state_id[Object.new] dtrans[from, to] = sym dtrans.add_accepting to ps.each { |state| dtrans.add_memo to, state.memo } else dtrans[state_id[s], state_id[u]] = sym if u.include? DUMMY to = state_id[u] accepting = ps.find_all { |l| followpos(l).include? DUMMY } accepting.each { |accepting_state| dtrans.add_memo to, accepting_state.memo } dtrans.add_accepting state_id[u] end end dstates << u end end dtrans end def nullable? node case node when Nodes::Group true when Nodes::Star true when Nodes::Or node.children.any? { |c| nullable?(c) } when Nodes::Cat nullable?(node.left) && nullable?(node.right) when Nodes::Terminal !node.left when Nodes::Unary nullable? node.left else raise ArgumentError, 'unknown nullable: %s' % node.class.name end end def firstpos node case node when Nodes::Star firstpos(node.left) when Nodes::Cat if nullable? node.left firstpos(node.left) | firstpos(node.right) else firstpos(node.left) end when Nodes::Or node.children.map { |c| firstpos(c) }.flatten.uniq when Nodes::Unary firstpos(node.left) when Nodes::Terminal nullable?(node) ? [] : [node] else raise ArgumentError, 'unknown firstpos: %s' % node.class.name end end def lastpos node case node when Nodes::Star firstpos(node.left) when Nodes::Or node.children.map { |c| lastpos(c) }.flatten.uniq when Nodes::Cat if nullable? node.right lastpos(node.left) | lastpos(node.right) else lastpos(node.right) end when Nodes::Terminal nullable?(node) ? [] : [node] when Nodes::Unary lastpos(node.left) else raise ArgumentError, 'unknown lastpos: %s' % node.class.name end end def followpos node followpos_table[node] end private def followpos_table @followpos ||= build_followpos end def build_followpos table = Hash.new { |h,k| h[k] = [] } @ast.each do |n| case n when Nodes::Cat lastpos(n.left).each do |i| table[i] += firstpos(n.right) end when Nodes::Star lastpos(n).each do |i| table[i] += firstpos(n) end end end table end def symbol edge case edge when Journey::Nodes::Symbol edge.regexp else edge.left end end end end end journey-1.0.4/lib/journey/gtg/transition_table.rb0000644000004100000410000000721111770361473022161 0ustar www-datawww-datarequire 'journey/nfa/dot' module Journey module GTG class TransitionTable include Journey::NFA::Dot attr_reader :memos def initialize @regexp_states = Hash.new { |h,k| h[k] = {} } @string_states = Hash.new { |h,k| h[k] = {} } @accepting = {} @memos = Hash.new { |h,k| h[k] = [] } end def add_accepting state @accepting[state] = true end def accepting_states @accepting.keys end def accepting? state @accepting[state] end def add_memo idx, memo @memos[idx] << memo end def memo idx @memos[idx] end def eclosure t Array(t) end def move t, a move_string(t, a).concat move_regexp(t, a) end def to_json require 'json' simple_regexp = Hash.new { |h,k| h[k] = {} } @regexp_states.each do |from, hash| hash.each do |re, to| simple_regexp[from][re.source] = to end end JSON.dump({ :regexp_states => simple_regexp, :string_states => @string_states, :accepting => @accepting }) end def to_svg svg = IO.popen("dot -Tsvg", 'w+') { |f| f.write to_dot f.close_write f.readlines } 3.times { svg.shift } svg.join.sub(/width="[^"]*"/, '').sub(/height="[^"]*"/, '') end def visualizer paths, title = 'FSM' viz_dir = File.join File.dirname(__FILE__), '..', 'visualizer' fsm_js = File.read File.join(viz_dir, 'fsm.js') fsm_css = File.read File.join(viz_dir, 'fsm.css') erb = File.read File.join(viz_dir, 'index.html.erb') states = "function tt() { return #{to_json}; }" fun_routes = paths.shuffle.first(3).map do |ast| ast.map { |n| case n when Nodes::Symbol case n.left when ':id' then rand(100).to_s when ':format' then %w{ xml json }.shuffle.first else 'omg' end when Nodes::Terminal then n.symbol else nil end }.compact.join end stylesheets = [fsm_css] svg = to_svg javascripts = [states, fsm_js] # Annoying hack for 1.9 warnings fun_routes = fun_routes stylesheets = stylesheets svg = svg javascripts = javascripts require 'erb' template = ERB.new erb template.result(binding) end def []= from, to, sym case sym when String @string_states[from][sym] = to when Regexp @regexp_states[from][sym] = to else raise ArgumentError, 'unknown symbol: %s' % sym.class end end def states ss = @string_states.keys + @string_states.values.map(&:values).flatten rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten (ss + rs).uniq end def transitions @string_states.map { |from, hash| hash.map { |s, to| [from, s, to] } }.flatten(1) + @regexp_states.map { |from, hash| hash.map { |s, to| [from, s, to] } }.flatten(1) end private def move_regexp t, a return [] if t.empty? t.map { |s| @regexp_states[s].map { |re,v| re === a ? v : nil } }.flatten.compact.uniq end def move_string t, a return [] if t.empty? t.map { |s| @string_states[s][a] }.compact end end end end journey-1.0.4/lib/journey/gtg/simulator.rb0000644000004100000410000000143411770361473020640 0ustar www-datawww-datarequire 'strscan' module Journey module GTG class MatchData attr_reader :memos def initialize memos @memos = memos end end class Simulator attr_reader :tt def initialize transition_table @tt = transition_table end def simulate string input = StringScanner.new string state = [0] while sym = input.scan(/[\/\.\?]|[^\/\.\?]+/) state = tt.move(state, sym) end acceptance_states = state.find_all { |s| tt.accepting? s } return if acceptance_states.empty? memos = acceptance_states.map { |x| tt.memo x }.flatten.compact MatchData.new memos end alias :=~ :simulate alias :match :simulate end end end