Module: Otto::Core::Router

Included in:
Otto
Defined in:
lib/otto/core/router.rb

Overview

Router module providing route loading and request dispatching functionality

Instance Method Summary collapse

Instance Method Details

#determine_locale(env) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/otto/core/router.rb', line 127

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

#handle_request(env) ⇒ Object



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

def handle_request(env)
  locale             = determine_locale env
  env['rack.locale'] = locale
  env['otto.locale_config'] = @locale_config.to_h if @locale_config
  @static_route    ||= Rack::Files.new(option[:public]) if option[:public] && safe_dir?(option[:public])
  path_info          = Rack::Utils.unescape(env['PATH_INFO'])
  path_info          = '/' if path_info.to_s.empty?

  begin
    path_info_clean = path_info
                      .encode(
                        'UTF-8', # Target encoding
                        invalid: :replace, # Replace invalid byte sequences
                        undef: :replace,   # Replace characters undefined in UTF-8
                        replace: '' # Use empty string for replacement
                      )
                      .gsub(%r{/$}, '') # Remove trailing slash, if present
  rescue ArgumentError => e
    # Log the error but don't expose details
    Otto.logger.error '[Otto.handle_request] Path encoding error'
    Otto.logger.debug "[Otto.handle_request] Error details: #{e.message}" if Otto.debug
    # Set a default value or use the original path_info
    path_info_clean = path_info
  end

  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.structured_log(:debug, 'Route matched',
      Otto::LoggingHelpers.request_context(env).merge(
        type: 'static_cached',
        base_path: base_path
      ))
    static_route.call(env)
  elsif literal_routes.has_key?(path_info_clean)
    route = literal_routes[path_info_clean]
    Otto.structured_log(:debug, 'Route matched',
      Otto::LoggingHelpers.request_context(env).merge(
        type: 'literal',
        handler: route.route_definition.definition,
        auth_strategy: route.route_definition.auth_requirement || 'none'
      ))
    route.call(env)
  elsif static_route && http_verb == :GET && safe_file?(path_info)
    Otto.structured_log(:debug, 'Route matched',
      Otto::LoggingHelpers.request_context(env).merge(
        type: 'static_new',
        base_path: base_path
      ))
    routes_static[:GET][base_path] = base_path
    static_route.call(env)
  else
    match_dynamic_route(env, path_info, http_verb, literal_routes)
  end
end

#load(path) ⇒ Object

Raises:

  • (ArgumentError)


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

def load(path)
  path = File.expand_path(path)
  raise ArgumentError, "Bad path: #{path}" unless File.exist?(path)

  raw = File.readlines(path).grep(/^\w/).collect(&:strip)
  raw.each do |entry|
    # Enhanced parsing: split only on first two whitespace boundaries
    # This preserves parameters in the definition part
    parts = entry.split(/\s+/, 3)
    next if parts.size < 3 # Skip malformed entries

    verb = parts[0]
    path = parts[1]
    definition = parts[2]

    # Check for MCP routes
    if Otto::MCP::RouteParser.is_mcp_route?(definition)
      handle_mcp_route(verb, path, definition)
      next
    elsif Otto::MCP::RouteParser.is_tool_route?(definition)
      handle_tool_route(verb, path, definition)
      next
    end

    route                                   = Otto::Route.new verb, path, definition
    route.otto                              = self
    path_clean                              = path.gsub(%r{/$}, '')
    @route_definitions[route.definition]    = route
    if Otto.debug
      Otto.structured_log(:debug, 'Route loaded',
        {
             pattern: route.pattern.source,
          verb: route.verb,
          definition: route.definition,
          type: 'pattern',
        })
    end
    @routes[route.verb] ||= []
    @routes[route.verb] << route
    @routes_literal[route.verb]           ||= {}
    @routes_literal[route.verb][path_clean] = route
  rescue StandardError => e
    Otto.structured_log(:error, 'Route load failed',
      {
               path: path,
        verb: verb,
        definition: definition,
        error: e.message,
        error_class: e.class.name,
      })
    Otto.logger.debug e.backtrace.join("\n") if Otto.debug
  end
  self
end