Class: Philiprehberger::Middleware::Stack
- Inherits:
-
Object
- Object
- Philiprehberger::Middleware::Stack
- Defined in:
- lib/philiprehberger/middleware/stack.rb
Overview
A composable middleware stack for processing pipelines.
Middleware can be a lambda ‘(env, next_mw)` or an object responding to `#call(env, next_mw)`.
Defined Under Namespace
Classes: Entry
Instance Method Summary collapse
-
#[](target_name) ⇒ #call?
Look up an entry by name.
-
#after(middleware_name) {|env| ... } ⇒ self
Attach an after hook to a named middleware.
-
#around(middleware_name) {|env, call| ... } ⇒ self
Attach an around hook to a named middleware.
-
#before(middleware_name) {|env| ... } ⇒ self
Attach a before hook to a named middleware.
-
#call(env) ⇒ Object
Execute the middleware stack with the given environment.
-
#clear ⇒ self
Remove all middleware entries, groups, and hooks.
-
#describe ⇒ String
Return a human-readable stack summary.
-
#disable_group(group_name) ⇒ self
Disable a middleware group.
-
#dry_run(env) ⇒ Array<Symbol, String, nil>
Simulate the middleware stack without executing middleware bodies.
-
#empty? ⇒ Boolean
Whether the stack has zero middleware entries.
-
#enable_group(group_name) ⇒ self
Enable a middleware group.
-
#frozen_copy ⇒ FrozenStack
Return an immutable copy of the stack.
-
#group(group_name, middleware_names) ⇒ self
Define a named group of middleware.
-
#group_enabled?(group_name) ⇒ Boolean
Check if a middleware group is enabled.
-
#has?(target_name) ⇒ Boolean
Whether the stack contains an entry with the given name.
-
#index_of(target_name) ⇒ Integer?
Position of a named entry in the stack, or
nilwhen absent. -
#initialize ⇒ Stack
constructor
Create a new empty middleware stack.
-
#insert_after(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Insert a middleware after the named entry.
-
#insert_before(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Insert a middleware before the named entry.
-
#merge(other) ⇒ self
Merge another stack’s entries onto the end of this stack.
-
#metrics ⇒ Hash
Return whole-stack invocation metrics aggregated across every #call since construction.
-
#profile(env) ⇒ Hash
Execute the middleware stack and return profiling data.
-
#remove(target_name) ⇒ self
Remove a middleware by name.
-
#replace(target_name, middleware, name: nil) ⇒ self
Replace a named middleware with a new one.
-
#size ⇒ Integer
(also: #length)
Number of middleware entries in the stack.
-
#stats ⇒ Hash
Return metadata hash about the stack.
-
#swap(name1, name2) ⇒ self
Swap positions of two named entries.
-
#to_a ⇒ Array<String, Symbol, nil>
Return the list of middleware names/entries.
-
#use(middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Append a middleware to the end of the stack.
Constructor Details
#initialize ⇒ Stack
Create a new empty middleware stack.
13 14 15 16 17 18 19 20 21 22 23 24 |
# File 'lib/philiprehberger/middleware/stack.rb', line 13 def initialize @entries = [] @groups = {} @disabled_groups = Set.new @before_hooks = Hash.new { |h, k| h[k] = [] } @after_hooks = Hash.new { |h, k| h[k] = [] } @around_hooks = Hash.new { |h, k| h[k] = [] } @metrics_mutex = Mutex.new @metrics_invocations = 0 @metrics_total_time = 0.0 @metrics_last_called_at = nil end |
Instance Method Details
#[](target_name) ⇒ #call?
Look up an entry by name.
135 136 137 138 |
# File 'lib/philiprehberger/middleware/stack.rb', line 135 def [](target_name) entry = @entries.find { |e| e.name == target_name } entry&.middleware end |
#after(middleware_name) {|env| ... } ⇒ self
Attach an after hook to a named middleware.
211 212 213 214 |
# File 'lib/philiprehberger/middleware/stack.rb', line 211 def after(middleware_name, &block) @after_hooks[middleware_name] << block self end |
#around(middleware_name) {|env, call| ... } ⇒ self
Attach an around hook to a named middleware.
The block wraps the middleware execution (including before/after hooks). It receives the env and a callable; invoke the callable to run the middleware. Multiple around hooks nest in registration order (first registered = outermost).
225 226 227 228 |
# File 'lib/philiprehberger/middleware/stack.rb', line 225 def around(middleware_name, &block) @around_hooks[middleware_name] << block self end |
#before(middleware_name) {|env| ... } ⇒ self
Attach a before hook to a named middleware.
201 202 203 204 |
# File 'lib/philiprehberger/middleware/stack.rb', line 201 def before(middleware_name, &block) @before_hooks[middleware_name] << block self end |
#call(env) ⇒ Object
Execute the middleware stack with the given environment.
234 235 236 237 238 239 240 241 242 |
# File 'lib/philiprehberger/middleware/stack.rb', line 234 def call(env) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) chain = build_chain chain.call(env) rescue Halt env ensure record_invocation(Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) end |
#clear ⇒ self
Remove all middleware entries, groups, and hooks.
300 301 302 303 304 305 306 307 308 |
# File 'lib/philiprehberger/middleware/stack.rb', line 300 def clear @entries.clear @groups.clear @disabled_groups.clear @before_hooks.clear @after_hooks.clear @around_hooks.clear self end |
#describe ⇒ String
Return a human-readable stack summary.
360 361 362 363 364 365 366 367 368 369 370 |
# File 'lib/philiprehberger/middleware/stack.rb', line 360 def describe lines = @entries.map.with_index do |entry, idx| parts = ["#{idx}: #{entry.name || '(unnamed)'}"] parts << 'if-guarded' if entry.if_guard parts << 'unless-guarded' if entry.unless_guard parts << "timeout=#{entry.timeout}s" if entry.timeout parts << 'on_error' if entry.on_error parts.join(' | ') end lines.join("\n") end |
#disable_group(group_name) ⇒ self
Disable a middleware group.
183 184 185 186 |
# File 'lib/philiprehberger/middleware/stack.rb', line 183 def disable_group(group_name) @disabled_groups.add(group_name) self end |
#dry_run(env) ⇒ Array<Symbol, String, nil>
Simulate the middleware stack without executing middleware bodies.
Returns the ordered list of middleware names that would be executed, respecting disabled groups, conditional guards, and the halt mechanism. Useful for debugging and testing pipeline configuration.
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/philiprehberger/middleware/stack.rb', line 265 def dry_run(env) disabled = disabled_middleware_names names = [] @entries.each do |entry| # Skip disabled group members next if entry.name && disabled.include?(entry.name) # Check halt condition break if env.is_a?(Hash) && env[:halt] # Evaluate conditional guards next if entry.if_guard && !entry.if_guard.call next if entry.unless_guard&.call names << entry.name end names end |
#empty? ⇒ Boolean
Whether the stack has zero middleware entries.
399 400 401 |
# File 'lib/philiprehberger/middleware/stack.rb', line 399 def empty? @entries.empty? end |
#enable_group(group_name) ⇒ self
Enable a middleware group.
174 175 176 177 |
# File 'lib/philiprehberger/middleware/stack.rb', line 174 def enable_group(group_name) @disabled_groups.delete(group_name) self end |
#frozen_copy ⇒ FrozenStack
Return an immutable copy of the stack.
375 376 377 378 |
# File 'lib/philiprehberger/middleware/stack.rb', line 375 def frozen_copy FrozenStack.new(@entries.map(&:dup), @groups.dup, @disabled_groups.dup, @before_hooks.dup, @after_hooks.dup, @around_hooks.dup) end |
#group(group_name, middleware_names) ⇒ self
Define a named group of middleware.
165 166 167 168 |
# File 'lib/philiprehberger/middleware/stack.rb', line 165 def group(group_name, middleware_names) @groups[group_name] = middleware_names.dup self end |
#group_enabled?(group_name) ⇒ Boolean
Check if a middleware group is enabled.
192 193 194 |
# File 'lib/philiprehberger/middleware/stack.rb', line 192 def group_enabled?(group_name) !@disabled_groups.include?(group_name) end |
#has?(target_name) ⇒ Boolean
Whether the stack contains an entry with the given name.
Unlike #[], this does not return the middleware itself — useful when callers need an unambiguous yes/no without conflating an absent entry with one stored as nil.
148 149 150 |
# File 'lib/philiprehberger/middleware/stack.rb', line 148 def has?(target_name) @entries.any? { |e| e.name == target_name } end |
#index_of(target_name) ⇒ Integer?
Position of a named entry in the stack, or nil when absent.
156 157 158 |
# File 'lib/philiprehberger/middleware/stack.rb', line 156 def index_of(target_name) @entries.index { |e| e.name == target_name } end |
#insert_after(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Insert a middleware after the named entry.
84 85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/philiprehberger/middleware/stack.rb', line 84 def insert_after(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) validate_middleware!(middleware) index = find_index!(target_name) @entries.insert(index + 1, Entry.new( middleware: middleware, name: name, if_guard: binding.local_variable_get(:if), unless_guard: binding.local_variable_get(:unless), on_error: on_error, timeout: timeout )) self end |
#insert_before(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Insert a middleware before the named entry.
59 60 61 62 63 64 65 66 67 68 69 70 71 |
# File 'lib/philiprehberger/middleware/stack.rb', line 59 def insert_before(target_name, middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) validate_middleware!(middleware) index = find_index!(target_name) @entries.insert(index, Entry.new( middleware: middleware, name: name, if_guard: binding.local_variable_get(:if), unless_guard: binding.local_variable_get(:unless), on_error: on_error, timeout: timeout )) self end |
#merge(other) ⇒ self
Merge another stack’s entries onto the end of this stack.
290 291 292 293 294 295 |
# File 'lib/philiprehberger/middleware/stack.rb', line 290 def merge(other) raise Error, 'argument must be a Stack' unless other.is_a?(Stack) other.each_entry { |entry| @entries << entry.dup } self end |
#metrics ⇒ Hash
Return whole-stack invocation metrics aggregated across every #call since construction.
Unlike #stats (structural metadata) and #profile (per-middleware timings), this hash summarizes how often the stack has been executed and the total wall-clock time spent.
329 330 331 332 333 334 335 336 337 338 339 |
# File 'lib/philiprehberger/middleware/stack.rb', line 329 def metrics @metrics_mutex.synchronize do avg = @metrics_invocations.zero? ? 0.0 : @metrics_total_time / @metrics_invocations { invocations: @metrics_invocations, total_time: @metrics_total_time, avg_time: avg, last_called_at: @metrics_last_called_at } end end |
#profile(env) ⇒ Hash
Execute the middleware stack and return profiling data.
248 249 250 251 252 253 254 255 |
# File 'lib/philiprehberger/middleware/stack.rb', line 248 def profile(env) timings = [] chain = build_chain(timings: timings) result = chain.call(env) { result: result, timings: timings } rescue Halt { result: env, timings: timings } end |
#remove(target_name) ⇒ self
Remove a middleware by name.
103 104 105 106 107 |
# File 'lib/philiprehberger/middleware/stack.rb', line 103 def remove(target_name) index = find_index!(target_name) @entries.delete_at(index) self end |
#replace(target_name, middleware, name: nil) ⇒ self
Replace a named middleware with a new one.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/philiprehberger/middleware/stack.rb', line 116 def replace(target_name, middleware, name: nil) validate_middleware!(middleware) index = find_index!(target_name) old_entry = @entries[index] @entries[index] = Entry.new( middleware: middleware, name: name || old_entry.name, if_guard: old_entry.if_guard, unless_guard: old_entry.unless_guard, on_error: old_entry.on_error, timeout: old_entry.timeout ) self end |
#size ⇒ Integer Also known as: length
Number of middleware entries in the stack.
390 391 392 |
# File 'lib/philiprehberger/middleware/stack.rb', line 390 def size @entries.length end |
#stats ⇒ Hash
Return metadata hash about the stack.
344 345 346 347 348 349 350 351 352 353 354 355 |
# File 'lib/philiprehberger/middleware/stack.rb', line 344 def stats { count: @entries.length, named: @entries.count(&:name), groups: @groups.keys, hooks: { before: @before_hooks.keys, after: @after_hooks.keys, around: @around_hooks.keys } } end |
#swap(name1, name2) ⇒ self
Swap positions of two named entries.
316 317 318 319 320 321 |
# File 'lib/philiprehberger/middleware/stack.rb', line 316 def swap(name1, name2) idx1 = find_index!(name1) idx2 = find_index!(name2) @entries[idx1], @entries[idx2] = @entries[idx2], @entries[idx1] self end |
#to_a ⇒ Array<String, Symbol, nil>
Return the list of middleware names/entries.
383 384 385 |
# File 'lib/philiprehberger/middleware/stack.rb', line 383 def to_a @entries.map(&:name) end |
#use(middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) ⇒ self
Append a middleware to the end of the stack.
35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/philiprehberger/middleware/stack.rb', line 35 def use(middleware, name: nil, if: nil, unless: nil, on_error: nil, timeout: nil) validate_middleware!(middleware) @entries << Entry.new( middleware: middleware, name: name, if_guard: binding.local_variable_get(:if), unless_guard: binding.local_variable_get(:unless), on_error: on_error, timeout: timeout ) self end |