Class: Otto

Inherits:
Object
  • Object
show all
Extended by:
ClassMethods
Defined in:
lib/otto.rb,
lib/otto/route.rb,
lib/otto/static.rb,
lib/otto/version.rb,
lib/otto/helpers/request.rb,
lib/otto/helpers/response.rb

Overview

Otto is a simple Rack router that allows you to define routes in a file

Defined Under Namespace

Modules: ClassMethods, RequestHelpers, ResponseHelpers, Static, VERSION Classes: Route

Constant Summary collapse

LIB_HOME =
__dir__

Class Attribute Summary collapse

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from ClassMethods

default, env?, path

Constructor Details

#initialize(path = nil, opts = {}) ⇒ Otto

Returns a new instance of Otto.



27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/otto.rb', line 27

def initialize path=nil, opts={}
  @routes_static =  { GET: {} }
  @routes =         { GET: [] }
  @routes_literal = { GET: {} }
  @route_definitions = {}
  @option = opts.merge({
    public: nil,
    locale: 'en'
  })
  Otto.logger.debug "new Otto: #{opts}" if Otto.debug
  load(path) unless path.nil?
  super()
end

Class Attribute Details

.debugObject

Returns the value of attribute debug.



207
208
209
# File 'lib/otto.rb', line 207

def debug
  @debug
end

.loggerObject

Returns the value of attribute logger.



207
208
209
# File 'lib/otto.rb', line 207

def logger
  @logger
end

Instance Attribute Details

#not_foundObject

Returns the value of attribute not_found.



25
26
27
# File 'lib/otto.rb', line 25

def not_found
  @not_found
end

#optionObject (readonly) Also known as: options

Returns the value of attribute option.



24
25
26
# File 'lib/otto.rb', line 24

def option
  @option
end

#route_definitionsObject (readonly)

Returns the value of attribute route_definitions.



23
24
25
# File 'lib/otto.rb', line 23

def route_definitions
  @route_definitions
end

#routesObject (readonly)

Returns the value of attribute routes.



23
24
25
# File 'lib/otto.rb', line 23

def routes
  @routes
end

#routes_literalObject (readonly)

Returns the value of attribute routes_literal.



23
24
25
# File 'lib/otto.rb', line 23

def routes_literal
  @routes_literal
end

#routes_staticObject (readonly)

Returns the value of attribute routes_static.



23
24
25
# File 'lib/otto.rb', line 23

def routes_static
  @routes_static
end

#server_errorObject

Returns the value of attribute server_error.



25
26
27
# File 'lib/otto.rb', line 25

def server_error
  @server_error
end

#static_routeObject (readonly)

Returns the value of attribute static_route.



24
25
26
# File 'lib/otto.rb', line 24

def static_route
  @static_route
end

Instance Method Details

#add_static_path(path) ⇒ Object



76
77
78
79
80
81
82
83
84
85
# File 'lib/otto.rb', line 76

def add_static_path path
  if safe_file?(path)
    base_path = File.split(path).first
    # Files in the root directory can refer to themselves
    base_path = path if base_path == '/'
    static_path = File.join(option[:public], base_path)
    Otto.logger.debug "new static route: #{base_path} (#{path})" if Otto.debug
    routes_static[:GET][base_path] = base_path
  end
end

#call(env) ⇒ Object



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
# File 'lib/otto.rb', line 87

def call env
  locale = determine_locale env
  env['rack.locale'] = locale
  if option[:public] && safe_dir?(option[:public])
    @static_route ||= Rack::File.new(option[:public])
  end
  path_info = Rack::Utils.unescape(env['PATH_INFO'])
  path_info = '/' if path_info.to_s.empty?
  path_info_clean = path_info.gsub /\/$/, ''
  base_path = File.split(path_info).first
  # Files in the root directory can refer to themselves
  base_path = path_info if base_path == '/'
  http_verb = env['REQUEST_METHOD'].upcase.to_sym
  literal_routes = routes_literal[http_verb] || {}
  literal_routes.merge! routes_literal[:GET] if http_verb == :HEAD
  if static_route && http_verb == :GET && routes_static[:GET].member?(base_path)
    #Otto.logger.debug " request: #{path_info} (static)"
    static_route.call(env)
  elsif literal_routes.has_key?(path_info_clean)
    route = literal_routes[path_info_clean]
    #Otto.logger.debug " request: #{http_verb} #{path_info} (literal route: #{route.verb} #{route.path})"
    route.call(env)
  elsif static_route && http_verb == :GET && safe_file?(path_info)
    static_path = File.join(option[:public], base_path)
    Otto.logger.debug " new static route: #{base_path} (#{path_info})"
    routes_static[:GET][base_path] = base_path
    static_route.call(env)
  else
    extra_params = {}
    found_route = nil
    valid_routes = routes[http_verb] || []
    valid_routes.push *routes[:GET] if http_verb == :HEAD
    valid_routes.each { |route|
      #Otto.logger.debug " request: #{http_verb} #{path_info} (trying route: #{route.verb} #{route.pattern})"
      if (match = route.pattern.match(path_info))
        values = match.captures.to_a
        # The first capture returned is the entire matched string b/c
        # we wrapped the entire regex in parens. We don't need it to
        # the full match.
        full_match = values.shift
        extra_params =
          if route.keys.any?
            route.keys.zip(values).inject({}) do |hash,(k,v)|
              if k == 'splat'
                (hash[k] ||= []) << v
              else
                hash[k] = v
              end
              hash
            end
          elsif values.any?
            {'captures' => values}
          else
            {}
          end
          found_route = route
          break
      end
    }
    found_route ||= literal_routes['/404']
    if found_route
      found_route.call env, extra_params
    else
      @not_found || Otto::Static.not_found
    end
  end
rescue => ex
  Otto.logger.error "#{ex.class}: #{ex.message} #{ex.backtrace.join("\n")}"

  if found_route = literal_routes['/500']
    found_route.call env
  else
    @server_error || Otto::Static.server_error
  end
end

#determine_locale(env) ⇒ Object



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/otto.rb', line 188

def determine_locale env
  accept_langs = env['HTTP_ACCEPT_LANGUAGE']
  accept_langs = self.option[:locale] if accept_langs.to_s.empty?
  locales = []
  unless accept_langs.empty?
    locales = accept_langs.split(',').map { |l|
      l += ';q=1.0' unless l =~ /;q=\d+(?:\.\d+)?$/
      l.split(';q=')
    }.sort_by { |locale, qvalue|
      qvalue.to_f
    }.collect { |locale, qvalue|
      locale
    }.reverse
  end
  Otto.logger.debug "locale: #{locales} (#{accept_langs})" if Otto.debug
  locales.empty? ? nil : locales
end

#load(path) ⇒ Object

Raises:

  • (ArgumentError)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/otto.rb', line 42

def load path
  path = File.expand_path(path)
  raise ArgumentError, "Bad path: #{path}" unless File.exist?(path)
  raw = File.readlines(path).select { |line| line =~ /^\w/ }.collect { |line| line.strip.split(/\s+/) }
  raw.each { |entry|
    begin
      verb, path, definition = *entry
      route = Otto::Route.new verb, path, definition
      route.otto = self
      path_clean = path.gsub /\/$/, ''
      @route_definitions[route.definition] = route
      Otto.logger.debug "route: #{route.pattern}" if Otto.debug
      @routes[route.verb] ||= []
      @routes[route.verb] << route
      @routes_literal[route.verb] ||= {}
      @routes_literal[route.verb][path_clean] = route

    rescue StandardError => ex
      Otto.logger.error "Bad route in #{path}: #{entry}"
    end
  }
  self
end

#safe_dir?(path) ⇒ Boolean

Returns:

  • (Boolean)


72
73
74
# File 'lib/otto.rb', line 72

def safe_dir? path
  (File.owned?(path) || File.grpowned?(path)) && File.directory?(path)
end

#safe_file?(path) ⇒ Boolean

Returns:

  • (Boolean)


66
67
68
69
70
# File 'lib/otto.rb', line 66

def safe_file? path
  globstr = File.join(option[:public], '*')
  pathstr = File.join(option[:public], path)
  File.fnmatch?(globstr, pathstr) && (File.owned?(pathstr) || File.grpowned?(pathstr)) && File.readable?(pathstr) && !File.directory?(pathstr)
end

#uri(route_definition, params = {}) ⇒ Object

Return the URI path for the given route_definition e.g.

Otto.default.path 'YourClass.somemethod'  #=> /some/path


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/otto.rb', line 168

def uri route_definition, params={}
  #raise RuntimeError, "Not working"
  route = @route_definitions[route_definition]
  unless route.nil?
    local_params = params.clone
    local_path = route.path.clone
    if objid = local_params.delete(:id) || local_params.delete('id')
      local_path.gsub! /\*/, objid
    end
    local_params.each_pair { |k,v|
      next unless local_path.match(":#{k}")
      local_path.gsub!(":#{k}", local_params.delete(k))
    }
    uri = Addressable::URI.new
    uri.path = local_path
    uri.query_values = local_params
    uri.to_s
  end
end