Class: Rack::JetRouter

Inherits:
Object
  • Object
show all
Defined in:
lib/rack/jet_router.rb

Overview

Jet-speed router class, derived from Keight.rb.

Example #1:

### (assume that 'xxxx_app' are certain Rack applications.)
mapping = {
    "/"                       => home_app,
    "/api" => {
        "/books" => {
            ""                => books_app,
            "/:id(.:format)"  => book_app,
            "/:book_id/comments/:comment_id" => comment_app,
        },
    },
    "/admin" => {
        "/books"              => admin_books_app,
    },
}
router = Rack::JetRouter.new(mapping)
router.lookup("/api/books/123.html")
    #=> [book_app, {"id"=>"123", "format"=>"html"}]
status, headers, body = router.call(env)

Example #2:

mapping = [
    ["/"                       , {GET: home_app}],
    ["/api", [
        ["/books", [
            [""                , {GET: book_list_app, POST: book_create_app}],
            ["/:id(.:format)"  , {GET: book_show_app, PUT: book_update_app}],
            ["/:book_id/comments/:comment_id", {POST: comment_create_app}],
        ]],
    ]],
    ["/admin", [
        ["/books"              , {ANY: admin_books_app}],
    ]],
]
router = Rack::JetRouter.new(mapping)
router.lookup("/api/books/123")
    #=> [{"GET"=>book_show_app, "PUT"=>book_update_app}, {"id"=>"123", "format"=>nil}]
status, headers, body = router.call(env)

Example #3:

mapping = {
    "/"                       => {GET: home_app},  # not {"GET"=>home_app}
    "/api" => {
        "/books" => {            # not {"GET"=>..., "POST"=>...}
            ""                => {GET: book_list_app, POST: book_create_app},
            "/:id(.:format)"  => {GET: book_show_app, PUT: book_update_app},
            "/:book_id/comments/:comment_id" => {POST: comment_create_app},
        },
    },
    "/admin" => {
        "/books"              => {ANY: admin_books_app},  # not {"ANY"=>...}
    },
}
router = Rack::JetRouter.new(mapping)
router.lookup("/api/books/123")
    #=> [{"GET"=>book_show_app, "PUT"=>book_update_app}, {"id"=>"123", "format"=>nil}]
status, headers, body = router.call(env)

Defined Under Namespace

Classes: Builder

Constant Summary collapse

RELEASE =
'$Release: 1.4.0 $'.split()[1]
REQUEST_METHODS =

; [!haggu] contains available request methods.

%w[GET POST PUT DELETE PATCH HEAD OPTIONS TRACE LINK UNLINK] \
.each_with_object({}) {|s, d| d[s] = s.intern }

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mapping, cache_size: 0, env_key: 'rack.urlpath_params', int_param: nil, urlpath_cache_size: 0, _enable_range: true) ⇒ JetRouter

Returns a new instance of JetRouter.



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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/rack/jet_router.rb', line 85

def initialize(mapping, cache_size: 0, env_key: 'rack.urlpath_params',
                        int_param: nil,          # ex: /(?:\A|_)id\z/
                        urlpath_cache_size: 0,   # for backward compatibility
                        _enable_range: true)     # undocumentend keyword arg
  @env_key = env_key
  @int_param = int_param
  #; [!21mf9] 'urlpath_cache_size:' kwarg is available for backward compatibility.
  @cache_size = [cache_size, urlpath_cache_size].max()
  #; [!5tw57] cache is disabled when 'cache_size:' is zero.
  @cache_dict = @cache_size > 0 ? {} : nil
  ##
  ## Pair list of endpoint and Rack app.
  ## ex:
  ##   [
  ##     ["/api/books"      , books_app ],
  ##     ["/api/books/:id"  , book_app  ],
  ##     ["/api/orders"     , orders_app],
  ##     ["/api/orders/:id" , order_app ],
  ##   ]
  ##
  @all_endpoints = []
  ##
  ## Endpoints without any path parameters.
  ## ex:
  ##   {
  ##     "/"           => home_app,
  ##     "/api/books"  => books_app,
  ##     "/api/orders" => orders_app,
  ##   }
  ##
  @fixed_endpoints = {}
  ##
  ## Endpoints with one or more path parameters.
  ## ex:
  ##   [
  ##     [%r!\A/api/books/([^./?]+)\z! , ["id"], book_app , (11..-1)],
  ##     [%r!\A/api/orders/([^./?]+)\z!, ["id"], order_app, (12..-1)],
  ##   ]
  ##
  @variable_endpoints = []
  ##
  ## Combined regexp of variable endpoints.
  ## ex:
  ##   %r!\A/api/(?:books/[^./?]+(\z)|orders/[^./?]+(\z))\z!
  ##
  @urlpath_rexp = nil
  #
  #; [!x2l32] gathers all endpoints.
  builder = Builder.new(self, _enable_range)
  param_rexp = /[:*]\w+|\(.*?\)/
  tmplist = []
  builder.traverse_mapping(mapping) do |path, item|
    @all_endpoints << [path, item]
    #; [!l63vu] handles urlpath pattern as fixed when no urlpath params.
    if path !~ param_rexp
      @fixed_endpoints[path] = item
    #; [!ec0av] treats '/foo(.html|.json)' as three fixed urlpaths.
    #; [!ylyi0] stores '/foo' as fixed path when path pattern is '/foo(.:format)'.
    elsif path =~ /\A([^:*\(\)]*)\(([^\(\)]+)\)\z/
      @fixed_endpoints[$1] = item unless $1.empty?
      arr = []
      $2.split('|').each do |s|
        next if s.empty?
        if s.include?(':')
          arr << s
        else
          @fixed_endpoints[$1 + s] = item
        end
      end
      tmplist << ["#{$1}(#{arr.join('|')})", item] unless arr.empty?
    else
      tmplist << [path, item]
    end
  end
  #; [!saa1a] compiles compound urlpath regexp.
  tree = builder.build_tree(tmplist)
  @urlpath_rexp = builder.build_rexp(tree) do |tuple|
    #; [!f1d7s] builds variable endpoint list.
    @variable_endpoints << tuple
  end
end

Instance Attribute Details

#urlpath_rexpObject (readonly)

Returns the value of attribute urlpath_rexp.



167
168
169
# File 'lib/rack/jet_router.rb', line 167

def urlpath_rexp
  @urlpath_rexp
end

Instance Method Details

#call(env) ⇒ Object

Finds rack app according to PATH_INFO and REQUEST_METHOD and invokes it.



170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/rack/jet_router.rb', line 170

def call(env)
  #; [!fpw8x] finds mapped app according to env['PATH_INFO'].
  req_path = env['PATH_INFO']
  obj, param_values = lookup(req_path)
  #; [!wxt2g] guesses correct urlpath and redirects to it automaticaly when request path not found.
  #; [!3vsua] doesn't redict automatically when request path is '/'.
  if ! obj && should_redirect?(env)
    location = req_path.end_with?("/") ? req_path[0..-2] : req_path + "/"
    obj, param_values = lookup(location)
    if obj
      #; [!hyk62] adds QUERY_STRING to redirect location.
      qs = env['QUERY_STRING']
      location = "#{location}?#{qs}" if qs && ! qs.empty?
      return redirect_to(location)
    end
  end
  #; [!30x0k] returns 404 when request urlpath not found.
  return error_not_found(env) unless obj
  #; [!gclbs] if mapped object is a Hash...
  if obj.is_a?(Hash)
    #; [!p1fzn] invokes app mapped to request method.
    #; [!5m64a] returns 405 when request method is not allowed.
    #; [!ys1e2] uses GET method when HEAD is not mapped.
    #; [!2hx6j] try ANY method when request method is not mapped.
    dict = obj
    req_meth = env['REQUEST_METHOD']
    app = dict[req_meth] || (req_meth == 'HEAD' ? dict['GET'] : nil) || dict['ANY']
    return error_not_allowed(env) unless app
  else
    app = obj
  end
  #; [!2c32f] stores urlpath parameter values into env['rack.urlpath_params'].
  store_param_values(env, param_values)
  #; [!hse47] invokes app mapped to request urlpath.
  return app.call(env)   # make body empty when HEAD?
end

#each(&block) ⇒ Object

Yields pair of urlpath pattern and app.



251
252
253
254
# File 'lib/rack/jet_router.rb', line 251

def each(&block)
  #; [!ep0pw] yields pair of urlpath pattern and app.
  @all_endpoints.each(&block)
end

#lookup(req_path) ⇒ Object Also known as: find

Finds app or Hash mapped to request path.

ex:

lookup('/api/books/123')   #=> [BookApp, {"id"=>"123"}]


211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/rack/jet_router.rb', line 211

def lookup(req_path)
  #; [!24khb] finds in fixed urlpaths at first.
  #; [!iwyzd] urlpath param value is nil when found in fixed urlpaths.
  obj = @fixed_endpoints[req_path]
  return obj, nil if obj
  #; [!upacd] finds in variable urlpath cache if it is enabled.
  #; [!1zx7t] variable urlpath cache is based on LRU.
  cache = @cache_dict
  if cache && (pair = cache.delete(req_path))
    cache[req_path] = pair
    return pair
  end
  #; [!vpdzn] returns nil when urlpath not found.
  m = @urlpath_rexp.match(req_path)
  return nil unless m
  index = m.captures.index('')
  return nil unless index
  #; [!ijqws] returns mapped object and urlpath parameter values when urlpath found.
  full_urlpath_rexp, param_names, obj, range, sep = @variable_endpoints[index]
  if range
    ## "/books/123"[7..-1] is faster than /\A\/books\/(\d+)\z/.match("/books/123")[1]
    str = req_path[range]
    ## `"/a/1/b/2"[3..-1].split('/b/')` is faster than `%r!\A/a/(\d+)/b/(\d+)\z!.match("/a/1/b/2").captures`
    values = sep ? str.split(sep) : [str]
  else
    m = full_urlpath_rexp.match(req_path)
    values = m.captures
  end
  param_values = build_param_values(param_names, values)
  #; [!84inr] caches result when variable urlpath cache enabled.
  if cache
    cache.shift() if cache.length >= @cache_size
    cache[req_path] = [obj, param_values]
  end
  return obj, param_values
end

#normalize_method_mapping(dict) ⇒ Object

called from Builder class



315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/rack/jet_router.rb', line 315

def normalize_method_mapping(dict)   # called from Builder class
  #; [!r7cmk] converts keys into string.
  #; [!z9kww] allows 'ANY' as request method.
  #; [!k7sme] raises error when unknown request method specified.
  #; [!itfsd] returns new Hash object.
  #; [!gd08f] if arg is an instance of Hash subclass, returns new instance of it.
  request_methods = REQUEST_METHODS
  #newdict = {}
  newdict = dict.class.new
  dict.each do |meth_sym, app|
    meth_str = meth_sym.to_s
    request_methods[meth_str] || meth_str == 'ANY'  or
      raise ArgumentError.new("#{meth_sym}: unknown request method.")
    newdict[meth_str] = app
  end
  return newdict
end

#param2rexp(param) ⇒ Object

Returns regexp string of path parameter. Override if necessary.



334
335
336
337
338
# File 'lib/rack/jet_router.rb', line 334

def param2rexp(param)   # called from Builder class
  #; [!6sd9b] returns regexp string according to param name.
  #; [!rfvk2] returns '\d+' if param name matched to int param regexp.
  return (rexp = @int_param) && rexp.match?(param) ? '\d+' : '[^./?]+'
end