Class: Otto::Core::MiddlewareStack
- Inherits:
-
Object
- Object
- Otto::Core::MiddlewareStack
- 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
-
#add(middleware_class, *args, **options) ⇒ Object
(also: #use, #<<)
Enhanced middleware registration with argument uniqueness and immutability check.
-
#add_with_position(middleware_class, *args, position: nil, **options) ⇒ Object
Add middleware with position hint for optimal ordering.
-
#clear! ⇒ Object
Clear all middleware.
-
#count(middleware_class) ⇒ Object
Count occurrences of a specific middleware class.
-
#each ⇒ Object
Enumerable support.
-
#empty? ⇒ Boolean
-
#includes?(middleware_class) ⇒ Boolean
Check if middleware is registered - now O(1) using Set.
-
#initialize ⇒ MiddlewareStack
constructor
A new instance of MiddlewareStack.
-
#middleware_details ⇒ Object
Detailed introspection.
-
#middleware_list ⇒ Object
Returns list of middleware classes in order.
-
#on_change(&callback) ⇒ Object
Set a callback to be invoked when the middleware stack changes.
-
#remove(middleware_class) ⇒ Object
Remove middleware.
-
#reverse_each ⇒ Object
Legacy compatibility methods for existing Otto interface.
-
#size ⇒ Object
Statistics.
-
#validate_mcp_middleware_order ⇒ Array<String>
Validate MCP middleware ordering.
-
#wrap(base_app, security_config = nil) ⇒ Object
Build Rack application with middleware chain.
Methods included from Freezable
Constructor Details
#initialize ⇒ MiddlewareStack
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
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, **) # 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] == end # Only add if no identical middleware configuration exists return if existing_entry entry = { middleware: middleware_class, args: args, 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).
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, **) 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] == end return if existing_entry entry = { middleware: middleware_class, args: args, 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
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 |
#each ⇒ Object
Enumerable support
188 189 190 |
# File 'lib/otto/core/middleware_stack.rb', line 188 def each(&) @stack.each(&) end |
#empty? ⇒ 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
171 172 173 |
# File 'lib/otto/core/middleware_stack.rb', line 171 def includes?(middleware_class) @middleware_set.include?(middleware_class) end |
#middleware_details ⇒ Object
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_list ⇒ Object
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
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
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_each ⇒ Object
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 |
#size ⇒ Object
Statistics
235 236 237 |
# File 'lib/otto/core/middleware_stack.rb', line 235 def size @stack.size end |
#validate_mcp_middleware_order ⇒ Array<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)
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] = 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, **) else middleware.new(app, *args, **) end else # Proc-based middleware middleware.call(app) end end end |