Class: Otto::Core::MiddlewareStack

Inherits:
Object
  • Object
show all
Includes:
Enumerable, Freezable
Defined in:
lib/otto/core/middleware_stack.rb

Overview

Enhanced middleware stack management for Otto framework. Provides better middleware registration, introspection capabilities, and improved execution chain management.

Instance Method Summary collapse

Methods included from Freezable

#deep_freeze!

Constructor Details

#initializeMiddlewareStack

Returns a new instance of MiddlewareStack.



16
17
18
19
20
21
22
23
# File 'lib/otto/core/middleware_stack.rb', line 16

def initialize
  @stack = []
  @middleware_set = Set.new
  # Classes pinned to run OUTERMOST regardless of insertion order (see
  # the :outermost position in #add_with_position and #ordered_stack).
  @outermost = Set.new
  @on_change_callback = nil
end

Instance Method Details

#add(middleware_class, *args, **options) ⇒ Object Also known as: use, <<

Enhanced middleware registration with argument uniqueness and immutability check

Raises:

  • (FrozenError)


32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# File 'lib/otto/core/middleware_stack.rb', line 32

def add(middleware_class, *args, **options)
  # Prevent modifications to frozen configurations
  raise FrozenError, 'Cannot modify frozen middleware stack' if frozen?

  # Check if an identical middleware configuration already exists
  existing_entry = @stack.find do |entry|
    entry[:middleware] == middleware_class &&
      entry[:args] == args &&
      entry[:options] == options
  end

  # Only add if no identical middleware configuration exists
  return if existing_entry

  entry = { middleware: middleware_class, args: args, options: options }
  @stack << entry
  @middleware_set.add(middleware_class)
  # Notify of change
  @on_change_callback&.call
end

#add_with_position(middleware_class, *args, position: nil, **options) ⇒ Object

Add middleware with position hint for optimal ordering

Positions: - :first — innermost (runs last, closest to the app) - :last/nil — append (outermost among currently-registered middleware, but a later append displaces it) - :outermost — pin to run OUTERMOST (first to see the request) and STAY there even if more middleware is appended afterward. Unlike :last, this is order-independent: honored in #ordered_stack at build time. Use for middleware that must short-circuit ahead of everything else (e.g. the CSP report receiver, which must intercept before CSRF).

Parameters:

  • middleware_class (Class)

    Middleware class

  • args (Array)

    Middleware arguments

  • position (Symbol, nil) (defaults to: nil)

    Position hint (:first, :last, :outermost, or nil)

Raises:

  • (FrozenError)


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

def add_with_position(middleware_class, *args, position: nil, **options)
  raise FrozenError, 'Cannot modify frozen middleware stack' if frozen?

  # Check for identical configuration
  existing_entry = @stack.find do |entry|
    entry[:middleware] == middleware_class &&
      entry[:args] == args &&
      entry[:options] == options
  end

  return if existing_entry

  entry = { middleware: middleware_class, args: args, options: options }

  case position
  when :first
    @stack.unshift(entry)
  when :outermost
    @stack << entry
    @outermost.add(middleware_class)
  else
    @stack << entry # :last / nil — default append
  end

  @middleware_set.add(middleware_class)
  # Notify of change
  @on_change_callback&.call
end

#clear!Object

Clear all middleware

Raises:

  • (FrozenError)


176
177
178
179
180
181
182
183
184
185
# File 'lib/otto/core/middleware_stack.rb', line 176

def clear!
  # Prevent modifications to frozen configurations
  raise FrozenError, 'Cannot modify frozen middleware stack' if frozen?

  @stack.clear
  @middleware_set.clear
  @outermost.clear
  # Notify of change
  @on_change_callback&.call
end

#count(middleware_class) ⇒ Object

Count occurrences of a specific middleware class



244
245
246
# File 'lib/otto/core/middleware_stack.rb', line 244

def count(middleware_class)
  @stack.count { |entry| entry[:middleware] == middleware_class }
end

#eachObject

Enumerable support



188
189
190
# File 'lib/otto/core/middleware_stack.rb', line 188

def each(&)
  @stack.each(&)
end

#empty?Boolean

Returns:

  • (Boolean)


239
240
241
# File 'lib/otto/core/middleware_stack.rb', line 239

def empty?
  @stack.empty?
end

#includes?(middleware_class) ⇒ Boolean

Check if middleware is registered - now O(1) using Set

Returns:

  • (Boolean)


171
172
173
# File 'lib/otto/core/middleware_stack.rb', line 171

def includes?(middleware_class)
  @middleware_set.include?(middleware_class)
end

#middleware_detailsObject

Detailed introspection



224
225
226
227
228
229
230
231
232
# File 'lib/otto/core/middleware_stack.rb', line 224

def middleware_details
  @stack.map do |entry|
    {
      middleware: entry[:middleware],
            args: entry[:args],
         options: entry[:options],
    }
  end
end

#middleware_listObject

Returns list of middleware classes in order



219
220
221
# File 'lib/otto/core/middleware_stack.rb', line 219

def middleware_list
  @stack.map { |entry| entry[:middleware] }
end

#on_change(&callback) ⇒ Object

Set a callback to be invoked when the middleware stack changes

Parameters:

  • callback (Proc)

    A callable object (e.g., method or lambda)



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

def on_change(&callback)
  @on_change_callback = callback
end

#remove(middleware_class) ⇒ Object

Remove middleware

Raises:

  • (FrozenError)


154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/otto/core/middleware_stack.rb', line 154

def remove(middleware_class)
  # Prevent modifications to frozen configurations
  raise FrozenError, 'Cannot modify frozen middleware stack' if frozen?

  matches = @stack.reject! { |entry| entry[:middleware] == middleware_class }

  # Update middleware set if any matching entries were found
  return unless matches

  # Rebuild the set of unique middleware classes
  @middleware_set = Set.new(@stack.map { |entry| entry[:middleware] })
  @outermost.delete(middleware_class)
  # Notify of change
  @on_change_callback&.call
end

#reverse_eachObject

Legacy compatibility methods for existing Otto interface



251
252
253
# File 'lib/otto/core/middleware_stack.rb', line 251

def reverse_each(&)
  @stack.reverse_each(&)
end

#sizeObject

Statistics



235
236
237
# File 'lib/otto/core/middleware_stack.rb', line 235

def size
  @stack.size
end

#validate_mcp_middleware_orderArray<String>

Validate MCP middleware ordering

MCP middleware must be in security-optimal order: 1. RateLimitMiddleware (reject excessive requests early) 2. Auth middleware (validate credentials before parsing) 3. SchemaValidationMiddleware (expensive JSON schema validation last)

Returns:

  • (Array<String>)

    Warning messages if order is suboptimal



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

def validate_mcp_middleware_order
  warnings = []

  # PERFORMANCE NOTE: This implementation intentionally uses select + find_index
  # rather than a single-pass approach. The filtered mcp_middlewares array is
  # typically 0-3 items, making the performance difference unmeasurable.
  # The current approach prioritizes readability over micro-optimization.
  # Single-pass alternatives were considered but rejected as premature optimization.
  mcp_middlewares = @stack.select do |entry|
    [
      Otto::MCP::RateLimitMiddleware,
      Otto::MCP::Auth::TokenMiddleware,
      Otto::MCP::SchemaValidationMiddleware,
    ].include?(entry[:middleware])
  end

  return warnings if mcp_middlewares.size < 2

  # Find positions
  rate_limit_pos = mcp_middlewares.find_index { |e| e[:middleware] == Otto::MCP::RateLimitMiddleware }
  auth_pos = mcp_middlewares.find_index { |e| e[:middleware] == Otto::MCP::Auth::TokenMiddleware }
  validation_pos = mcp_middlewares.find_index { |e| e[:middleware] == Otto::MCP::SchemaValidationMiddleware }

  # Check optimal order: rate_limit < auth < validation
  if rate_limit_pos && auth_pos && rate_limit_pos > auth_pos
    warnings << <<~MSG.chomp
      [MCP Middleware] RateLimitMiddleware should come before TokenMiddleware
    MSG
  end

  if auth_pos && validation_pos && auth_pos > validation_pos
    warnings << <<~MSG.chomp
      [MCP Middleware] TokenMiddleware should come before SchemaValidationMiddleware
    MSG
  end

  if rate_limit_pos && validation_pos && rate_limit_pos > validation_pos
    warnings << <<~MSG.chomp
      [MCP Middleware] RateLimitMiddleware should come before SchemaValidationMiddleware
    MSG
  end

  warnings
end

#wrap(base_app, security_config = nil) ⇒ Object

Build Rack application with middleware chain

The stack folds via reduce, so the LAST entry becomes the OUTERMOST wrapper (first to see the request). #ordered_stack moves any :outermost- pinned middleware to the end so it stays outermost regardless of the order middleware was registered in.



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/otto/core/middleware_stack.rb', line 198

def wrap(base_app, security_config = nil)
  ordered_stack.reduce(base_app) do |app, entry|
    middleware = entry[:middleware]
    args = entry[:args]
    options = entry[:options]

    if middleware.respond_to?(:new)
      # Inject security_config for security middleware, placing it before custom args
      if security_config && middleware_needs_config?(middleware)
        middleware.new(app, security_config, *args, **options)
      else
        middleware.new(app, *args, **options)
      end
    else
      # Proc-based middleware
      middleware.call(app)
    end
  end
end