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

def initialize
  @stack = []
  @middleware_set = 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)


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

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

Parameters:

  • middleware_class (Class)

    Middleware class

  • args (Array)

    Middleware arguments

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

    Position hint (:first, :last, or nil for append)

  • options (Hash)

    a customizable set of options

Options Hash (**options):

  • :position (Symbol)

    Position hint (:first or :last)

Raises:

  • (FrozenError)


56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/otto/core/middleware_stack.rb', line 56

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 :last
    @stack << entry
  else
    @stack << entry # Default append
  end

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

#clear!Object

Clear all middleware

Raises:

  • (FrozenError)


161
162
163
164
165
166
167
168
169
# File 'lib/otto/core/middleware_stack.rb', line 161

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

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

#count(middleware_class) ⇒ Object

Count occurrences of a specific middleware class



223
224
225
# File 'lib/otto/core/middleware_stack.rb', line 223

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

#eachObject

Enumerable support



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

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

#empty?Boolean

Returns:

  • (Boolean)


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

def empty?
  @stack.empty?
end

#includes?(middleware_class) ⇒ Boolean

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

Returns:

  • (Boolean)


156
157
158
# File 'lib/otto/core/middleware_stack.rb', line 156

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

#middleware_detailsObject

Detailed introspection



203
204
205
206
207
208
209
210
211
# File 'lib/otto/core/middleware_stack.rb', line 203

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



198
199
200
# File 'lib/otto/core/middleware_stack.rb', line 198

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)



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

def on_change(&callback)
  @on_change_callback = callback
end

#remove(middleware_class) ⇒ Object

Remove middleware

Raises:

  • (FrozenError)


140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/otto/core/middleware_stack.rb', line 140

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] })
  # Notify of change
  @on_change_callback&.call
end

#reverse_eachObject

Legacy compatibility methods for existing Otto interface



230
231
232
# File 'lib/otto/core/middleware_stack.rb', line 230

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

#sizeObject

Statistics



214
215
216
# File 'lib/otto/core/middleware_stack.rb', line 214

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



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

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



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/otto/core/middleware_stack.rb', line 177

def wrap(base_app, security_config = nil)
  @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