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.
Constant Summary collapse
- PIN_TIERS =
Pin tiers honored by #ordered_stack, outward-ascending. #wrap folds the stack with reduce, so a LATER array position is a FURTHER OUT wrapper; sorting by tier therefore sorts by how early the middleware sees the request. Unpinned entries are tier 0 and keep their insertion order.
A tier is recorded on the ENTRY (as entry[:pin_tier]), not on the middleware class. Entries are identified by (class, args, options), so the same class can legitimately be registered more than once with different configuration; a class-wide pin would drag those other registrations into the pinned tier along with it.
{ outermost: 1, entrypoint: 2, }.freeze
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
-
#execution_order ⇒ Array<Class>
Returns middleware classes in EXECUTION order: the first entry is the outermost wrapper #wrap builds, i.e.
-
#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 REGISTRATION order — the order they were added, which is the reverse of execution order and ignores pin tiers.
-
#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.
31 32 33 34 35 |
# File 'lib/otto/core/middleware_stack.rb', line 31 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
44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/otto/core/middleware_stack.rb', line 44 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 name a place in the ARRAY; #wrap folds the array with reduce, so array order is the REVERSE of execution order — the last entry is the outermost wrapper and therefore the first to see a request.
- :first/:innermost — innermost: the LAST middleware to see the request,
closest to the app. Note the trap in the older
:firstspelling: it is first-in-array, hence last-to-execute.:innermostsays the same thing in execution terms and is the preferred spelling. - :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).
- :entrypoint — pin OUTSIDE even the :outermost tier: the very first middleware to touch a request. Reserved for middleware that must normalize the request before anything else can observe it. Otto pins IPPrivacyMiddleware here so every other middleware — its own, an :outermost pin, and anything the app adds via Otto#use — reads a masked REMOTE_ADDR and the canonical env[‘otto.client_ip’].
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 |
# File 'lib/otto/core/middleware_stack.rb', line 96 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, :innermost @stack.unshift(entry) when *PIN_TIERS.keys @stack << entry.merge(pin_tier: PIN_TIERS.fetch(position)) 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
202 203 204 205 206 207 208 209 210 |
# File 'lib/otto/core/middleware_stack.rb', line 202 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
287 288 289 |
# File 'lib/otto/core/middleware_stack.rb', line 287 def count(middleware_class) @stack.count { |entry| entry[:middleware] == middleware_class } end |
#each ⇒ Object
Enumerable support
213 214 215 |
# File 'lib/otto/core/middleware_stack.rb', line 213 def each(&) @stack.each(&) end |
#empty? ⇒ Boolean
282 283 284 |
# File 'lib/otto/core/middleware_stack.rb', line 282 def empty? @stack.empty? end |
#execution_order ⇒ Array<Class>
Returns middleware classes in EXECUTION order: the first entry is the outermost wrapper #wrap builds, i.e. the first to see a request. This is #middleware_list resolved through the pin tiers and reversed, so it answers “what does this stack actually do?” without building the app.
262 263 264 |
# File 'lib/otto/core/middleware_stack.rb', line 262 def execution_order ordered_stack.reverse.map { |entry| entry[:middleware] } end |
#includes?(middleware_class) ⇒ Boolean
Check if middleware is registered - now O(1) using Set
197 198 199 |
# File 'lib/otto/core/middleware_stack.rb', line 197 def includes?(middleware_class) @middleware_set.include?(middleware_class) end |
#middleware_details ⇒ Object
Detailed introspection
267 268 269 270 271 272 273 274 275 |
# File 'lib/otto/core/middleware_stack.rb', line 267 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 REGISTRATION order — the order they were added, which is the reverse of execution order and ignores pin tiers. Use #execution_order to see what actually runs first.
252 253 254 |
# File 'lib/otto/core/middleware_stack.rb', line 252 def middleware_list @stack.map { |entry| entry[:middleware] } end |
#on_change(&callback) ⇒ Object
Set a callback to be invoked when the middleware stack changes
39 40 41 |
# File 'lib/otto/core/middleware_stack.rb', line 39 def on_change(&callback) @on_change_callback = callback end |
#remove(middleware_class) ⇒ Object
Remove middleware
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 |
# File 'lib/otto/core/middleware_stack.rb', line 180 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. Pins need no cleanup: # each removed entry took its own tier with it. @middleware_set = Set.new(@stack.map { |entry| entry[:middleware] }) # Notify of change @on_change_callback&.call end |
#reverse_each ⇒ Object
Legacy compatibility methods for existing Otto interface
294 295 296 |
# File 'lib/otto/core/middleware_stack.rb', line 294 def reverse_each(&) @stack.reverse_each(&) end |
#size ⇒ Object
Statistics
278 279 280 |
# File 'lib/otto/core/middleware_stack.rb', line 278 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)
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/otto/core/middleware_stack.rb', line 132 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 pinned middleware (:outermost, :entrypoint) to the end so it stays outermost regardless of the order middleware was registered in.
NOT a request-path method. Its only caller is Otto’s build_app!, which runs at construction and again whenever the stack changes; requests are served by the chain it returns. So the ordering work here (and in #ordered_stack) is per-BUILD, not per-request — don’t add caching machinery on the assumption that it is hot.
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/otto/core/middleware_stack.rb', line 229 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 |