File: router.rb

package info (click to toggle)
ruby-journey 1.0.4-2.1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye, sid, trixie
  • size: 288 kB
  • sloc: ruby: 2,830; javascript: 113; yacc: 42; makefile: 2
file content (145 lines) | stat: -rw-r--r-- 3,577 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
require '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