Class: Otto::RouteDefinition

Inherits:
Object
  • Object
show all
Defined in:
lib/otto/route_definition.rb

Overview

Immutable data class representing a complete route definition This encapsulates all aspects of a route: path, target, and options

Constant Summary collapse

SECURITY_GATING_OPTIONS =

Options that gate access to a route. A malformed token for one of these (e.g. csrf instead of csrf=exempt, or a bare auth) must not fall back to the default behavior silently — the route would serve without its intended protection (issue #191).

%w[auth role csrf].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(verb, path, definition, pattern: nil, keys: nil) ⇒ RouteDefinition

Returns a new instance of RouteDefinition.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/otto/route_definition.rb', line 47

def initialize(verb, path, definition, pattern: nil, keys: nil)
  @verb       = verb.to_s.upcase.to_sym
  @path       = path
  @definition = definition
  @pattern    = pattern
  @keys       = keys || []

  # Parse the definition into target and options
  parsed   = parse_definition(definition)
  @target  = parsed[:target]
  @options = parsed[:options].freeze

  # Parse the target into class, method, and kind
  target_parsed = parse_target(@target)
  @klass_name   = target_parsed[:klass_name]
  @method_name  = target_parsed[:method_name]
  @kind         = target_parsed[:kind]

  # Freeze for immutability
  freeze
end

Instance Attribute Details

#definitionString (readonly)

Returns The original definition string.

Returns:

  • (String)

    The original definition string



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

def definition
  @definition
end

#keysArray<String> (readonly)

Returns The parameter keys extracted from the path.

Returns:

  • (Array<String>)

    The parameter keys extracted from the path



45
46
47
# File 'lib/otto/route_definition.rb', line 45

def keys
  @keys
end

#kindSymbol (readonly)

Returns The invocation kind (:class, :instance, or :logic).

Returns:

  • (Symbol)

    The invocation kind (:class, :instance, or :logic)



36
37
38
# File 'lib/otto/route_definition.rb', line 36

def kind
  @kind
end

#klass_nameString (readonly)

Returns The class name portion.

Returns:

  • (String)

    The class name portion



30
31
32
# File 'lib/otto/route_definition.rb', line 30

def klass_name
  @klass_name
end

#method_nameString (readonly)

Returns The method name portion.

Returns:

  • (String)

    The method name portion



33
34
35
# File 'lib/otto/route_definition.rb', line 33

def method_name
  @method_name
end

#optionsHash (readonly)

Returns The route options (auth, response, csrf, etc.).

Returns:

  • (Hash)

    The route options (auth, response, csrf, etc.)



39
40
41
# File 'lib/otto/route_definition.rb', line 39

def options
  @options
end

#pathString (readonly)

Returns The URL path pattern.

Returns:

  • (String)

    The URL path pattern



21
22
23
# File 'lib/otto/route_definition.rb', line 21

def path
  @path
end

#patternRegexp (readonly)

Returns The compiled path pattern for matching.

Returns:

  • (Regexp)

    The compiled path pattern for matching



42
43
44
# File 'lib/otto/route_definition.rb', line 42

def pattern
  @pattern
end

#targetString (readonly)

Returns The target class and method (e.g., “TestApp.index”).

Returns:

  • (String)

    The target class and method (e.g., “TestApp.index”)



27
28
29
# File 'lib/otto/route_definition.rb', line 27

def target
  @target
end

#verbString (readonly)

Returns The HTTP verb (GET, POST, etc.).

Returns:

  • (String)

    The HTTP verb (GET, POST, etc.)



18
19
20
# File 'lib/otto/route_definition.rb', line 18

def verb
  @verb
end

Class Method Details

.parse_option_token(part, context) ⇒ Array(Symbol, String)?

Parse a single whitespace-delimited key=value option token, applying the security-gating fail-fast rule shared by normal routes and the MCP RouteParser (issue #191 and its MCP/TOOL follow-up).

Parameters:

  • part (String)

    a single option token, e.g. “auth=session”

  • context (String)

    human-readable source description for the raised error message, e.g. “route definition "GET /admin …"”

Returns:

  • (Array(Symbol, String), nil)

    the [key, value] pair to store, or nil if the token is malformed and should only be warned about

Raises:



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/otto/route_definition.rb', line 133

def self.parse_option_token(part, context)
  key, value = part.split('=', 2)
  normalized_key = key&.downcase

  if SECURITY_GATING_OPTIONS.include?(normalized_key)
    if key != normalized_key || value.nil? || value.empty?
      raise Otto::RouteDefinitionError,
            "Malformed security option #{part.inspect} in #{context}: " \
            "expected #{normalized_key}=value"
    end
    [key.to_sym, value]
  elsif key && !key.empty? && value
    [key.to_sym, value]
  end
end

Instance Method Details

#auth_requirementString?

Get authentication requirement (backward compatibility - returns first requirement)

Returns:

  • (String, nil)

    The auth requirement or nil



86
87
88
# File 'lib/otto/route_definition.rb', line 86

def auth_requirement
  auth_requirements.first
end

#auth_requirementsArray<String>

Get all authentication requirements as an array Supports multiple strategies: auth=session,apikey,oauth

Returns:

  • (Array<String>)

    Array of auth requirement strings



93
94
95
96
97
98
# File 'lib/otto/route_definition.rb', line 93

def auth_requirements
  auth = option(:auth)
  return [] unless auth

  auth.split(',').map(&:strip).reject(&:empty?)
end

#csrf_exempt?Boolean

Check if CSRF is exempt for this route

Returns:

  • (Boolean)


151
152
153
# File 'lib/otto/route_definition.rb', line 151

def csrf_exempt?
  option(:csrf) == 'exempt'
end

#has_option?(key) ⇒ Boolean

Check if route has specific option

Parameters:

  • key (Symbol, String)

    Option key to check

Returns:

  • (Boolean)


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

def has_option?(key)
  @options.key?(key.to_sym)
end

#inspectString

Detailed inspection

Returns:

  • (String)


196
197
198
# File 'lib/otto/route_definition.rb', line 196

def inspect
  "#<Otto::RouteDefinition #{self} options=#{@options.inspect}>"
end

#logic_route?Boolean

Check if this is a Logic class route (no . or # in target)

Returns:

  • (Boolean)


157
158
159
# File 'lib/otto/route_definition.rb', line 157

def logic_route?
  kind == :logic
end

#option(key, default = nil) ⇒ Object

Get option value with optional default

Parameters:

  • key (Symbol, String)

    Option key

  • default (Object) (defaults to: nil)

    Default value if option not present

Returns:

  • (Object)


80
81
82
# File 'lib/otto/route_definition.rb', line 80

def option(key, default = nil)
  @options.fetch(key.to_sym, default)
end

#response_typeString

Get response type

Returns:

  • (String)

    The response type (defaults to ‘default’)



119
120
121
# File 'lib/otto/route_definition.rb', line 119

def response_type
  option(:response, 'default')
end

#role_requirementString?

Get role requirement for route-level authorization Supports single role or comma-separated roles (OR logic): role=admin,editor

Returns:

  • (String, nil)

    The role requirement or nil



103
104
105
# File 'lib/otto/route_definition.rb', line 103

def role_requirement
  option(:role)
end

#role_requirementsArray<String>

Get all role requirements as an array Supports multiple roles with OR logic: role=admin,editor

Returns:

  • (Array<String>)

    Array of role requirement strings



110
111
112
113
114
115
# File 'lib/otto/route_definition.rb', line 110

def role_requirements
  role = option(:role)
  return [] unless role

  role.split(',').map(&:strip).reject(&:empty?)
end

#to_hHash

Convert to hash representation

Returns:

  • (Hash)


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

def to_h
  {
           verb: @verb,
           path: @path,
     definition: @definition,
         target: @target,
     klass_name: @klass_name,
    method_name: @method_name,
           kind: @kind,
        options: @options,
        pattern: @pattern,
           keys: @keys,
  }
end

#to_sString

String representation for debugging

Returns:

  • (String)


190
191
192
# File 'lib/otto/route_definition.rb', line 190

def to_s
  "#{@verb} #{@path} #{@definition}"
end

#with_options(new_options) ⇒ RouteDefinition

Create a new RouteDefinition with modified options

Parameters:

  • new_options (Hash)

    Options to merge/override

Returns:



164
165
166
167
168
169
# File 'lib/otto/route_definition.rb', line 164

def with_options(new_options)
  merged_options = @options.merge(new_options)
  new_definition = [@target, *merged_options.map { |k, v| "#{k}=#{v}" }].join(' ')

  self.class.new(@verb, @path, new_definition, pattern: @pattern, keys: @keys)
end