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.
-
#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.
-
#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.
-
#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.
191 192 193 194 |
# File 'lib/philiprehberger/middleware/stack.rb', line 191 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).
205 206 207 208 |
# File 'lib/philiprehberger/middleware/stack.rb', line 205 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.
181 182 183 184 |
# File 'lib/philiprehberger/middleware/stack.rb', line 181 def before(middleware_name, &block) @before_hooks[middleware_name] << block self end |
#call(env) ⇒ Object
Execute the middleware stack with the given environment.
214 215 216 217 218 219 220 221 222 |
# File 'lib/philiprehberger/middleware/stack.rb', line 214 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.
280 281 282 283 284 285 286 287 288 |
# File 'lib/philiprehberger/middleware/stack.rb', line 280 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.
340 341 342 343 344 345 346 347 348 349 350 |
# File 'lib/philiprehberger/middleware/stack.rb', line 340 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.
163 164 165 166 |
# File 'lib/philiprehberger/middleware/stack.rb', line 163 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.
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 |
# File 'lib/philiprehberger/middleware/stack.rb', line 245 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 |
#enable_group(group_name) ⇒ self
Enable a middleware group.
154 155 156 157 |
# File 'lib/philiprehberger/middleware/stack.rb', line 154 def enable_group(group_name) @disabled_groups.delete(group_name) self end |
#frozen_copy ⇒ FrozenStack
Return an immutable copy of the stack.
355 356 357 358 |
# File 'lib/philiprehberger/middleware/stack.rb', line 355 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.
145 146 147 148 |
# File 'lib/philiprehberger/middleware/stack.rb', line 145 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.
172 173 174 |
# File 'lib/philiprehberger/middleware/stack.rb', line 172 def group_enabled?(group_name) !@disabled_groups.include?(group_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.
270 271 272 273 274 275 |
# File 'lib/philiprehberger/middleware/stack.rb', line 270 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.
309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/philiprehberger/middleware/stack.rb', line 309 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.
228 229 230 231 232 233 234 235 |
# File 'lib/philiprehberger/middleware/stack.rb', line 228 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 |
#stats ⇒ Hash
Return metadata hash about the stack.
324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/philiprehberger/middleware/stack.rb', line 324 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.
296 297 298 299 300 301 |
# File 'lib/philiprehberger/middleware/stack.rb', line 296 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.
363 364 365 |
# File 'lib/philiprehberger/middleware/stack.rb', line 363 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 |