philiprehberger-middleware 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +20 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/lib/philiprehberger/middleware/stack.rb +106 -0
- data/lib/philiprehberger/middleware/version.rb +7 -0
- data/lib/philiprehberger/middleware.rb +10 -0
- metadata +56 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 29bf5f1bc723e0258d69143b9fe27f6518ed7df927364c41c43ff9ae513feb50
|
|
4
|
+
data.tar.gz: 90803aef2b5619ff376fad5b0c1033e6649311652b4692cd51ddd7aef0c66aa9
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 7cb953d12ccd0a5a9f8125c8c1c045bd3a571cbc9394b202d762e59b31e984fbc2e84b88eabfb9cbf1a86507abb6ac0110a5792b9da79d5550fd812d74f3cb79
|
|
7
|
+
data.tar.gz: bbfd92e64de7c312eef8198da722b3b6e5c89473641454e4653a23258f20b99fc458652c427dcc70500a49b7deee3ba27af19ad4898a5566637f642997692a47
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this gem will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-03-22
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Initial release
|
|
14
|
+
- Middleware stack with sequential execution
|
|
15
|
+
- Lambda and class-based middleware support
|
|
16
|
+
- Named middleware entries for targeted manipulation
|
|
17
|
+
- Insert before and after named entries
|
|
18
|
+
- Remove middleware by name
|
|
19
|
+
- Short-circuit capability for early termination
|
|
20
|
+
- Stack introspection via `to_a`
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 philiprehberger
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# philiprehberger-middleware
|
|
2
|
+
|
|
3
|
+
[](https://github.com/philiprehberger/rb-middleware/actions/workflows/ci.yml)
|
|
4
|
+
[](https://rubygems.org/gems/philiprehberger-middleware)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
Generic middleware stack for composing processing pipelines
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Ruby >= 3.1
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
gem "philiprehberger-middleware"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install philiprehberger-middleware
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "philiprehberger/middleware"
|
|
31
|
+
|
|
32
|
+
stack = Philiprehberger::Middleware::Stack.new
|
|
33
|
+
stack.use(->(env, next_mw) { next_mw.call(env.merge(logged: true)) }, name: :logger)
|
|
34
|
+
stack.use(->(env, next_mw) { next_mw.call(env.merge(authed: true)) }, name: :auth)
|
|
35
|
+
|
|
36
|
+
result = stack.call({})
|
|
37
|
+
# => { logged: true, authed: true }
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Class-Based Middleware
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
class TimingMiddleware
|
|
44
|
+
def call(env, next_mw)
|
|
45
|
+
start = Time.now
|
|
46
|
+
result = next_mw.call(env)
|
|
47
|
+
result.merge(elapsed: Time.now - start)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
stack = Philiprehberger::Middleware::Stack.new
|
|
52
|
+
stack.use(TimingMiddleware.new, name: :timing)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Insert and Remove
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
stack = Philiprehberger::Middleware::Stack.new
|
|
59
|
+
stack.use(->(env, next_mw) { next_mw.call(env) }, name: :first)
|
|
60
|
+
stack.use(->(env, next_mw) { next_mw.call(env) }, name: :last)
|
|
61
|
+
|
|
62
|
+
stack.insert_before(:last, ->(env, next_mw) { next_mw.call(env) }, name: :middle)
|
|
63
|
+
stack.to_a # => [:first, :middle, :last]
|
|
64
|
+
|
|
65
|
+
stack.remove(:middle)
|
|
66
|
+
stack.to_a # => [:first, :last]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Short-Circuit
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
stack = Philiprehberger::Middleware::Stack.new
|
|
73
|
+
stack.use(lambda { |env, _next_mw|
|
|
74
|
+
return { error: 'unauthorized' } unless env[:token]
|
|
75
|
+
|
|
76
|
+
_next_mw.call(env)
|
|
77
|
+
}, name: :auth)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `Stack`
|
|
83
|
+
|
|
84
|
+
| Method | Description |
|
|
85
|
+
|--------|-------------|
|
|
86
|
+
| `.new` | Create an empty middleware stack |
|
|
87
|
+
| `#use(mw, name:)` | Append middleware to the stack |
|
|
88
|
+
| `#insert_before(name, mw, name:)` | Insert middleware before a named entry |
|
|
89
|
+
| `#insert_after(name, mw, name:)` | Insert middleware after a named entry |
|
|
90
|
+
| `#remove(name)` | Remove middleware by name |
|
|
91
|
+
| `#call(env)` | Execute the stack with the given environment |
|
|
92
|
+
| `#to_a` | List middleware names in order |
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bundle install
|
|
98
|
+
bundle exec rspec # Run tests
|
|
99
|
+
bundle exec rubocop # Check code style
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## License
|
|
103
|
+
|
|
104
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Philiprehberger
|
|
4
|
+
module Middleware
|
|
5
|
+
# A composable middleware stack for processing pipelines.
|
|
6
|
+
#
|
|
7
|
+
# Middleware can be a lambda `(env, next_mw)` or an object responding to `#call(env, next_mw)`.
|
|
8
|
+
class Stack
|
|
9
|
+
# Create a new empty middleware stack.
|
|
10
|
+
def initialize
|
|
11
|
+
@entries = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Append a middleware to the end of the stack.
|
|
15
|
+
#
|
|
16
|
+
# @param middleware [#call, Proc] middleware callable accepting (env, next_mw)
|
|
17
|
+
# @param name [String, Symbol, nil] optional name for insertion/removal
|
|
18
|
+
# @return [self]
|
|
19
|
+
def use(middleware, name: nil)
|
|
20
|
+
validate_middleware!(middleware)
|
|
21
|
+
@entries << Entry.new(middleware: middleware, name: name)
|
|
22
|
+
self
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Insert a middleware before the named entry.
|
|
26
|
+
#
|
|
27
|
+
# @param target_name [String, Symbol] name of the entry to insert before
|
|
28
|
+
# @param middleware [#call, Proc] middleware callable
|
|
29
|
+
# @param name [String, Symbol, nil] optional name for the new entry
|
|
30
|
+
# @return [self]
|
|
31
|
+
# @raise [Error] if the target name is not found
|
|
32
|
+
def insert_before(target_name, middleware, name: nil)
|
|
33
|
+
validate_middleware!(middleware)
|
|
34
|
+
index = find_index!(target_name)
|
|
35
|
+
@entries.insert(index, Entry.new(middleware: middleware, name: name))
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Insert a middleware after the named entry.
|
|
40
|
+
#
|
|
41
|
+
# @param target_name [String, Symbol] name of the entry to insert after
|
|
42
|
+
# @param middleware [#call, Proc] middleware callable
|
|
43
|
+
# @param name [String, Symbol, nil] optional name for the new entry
|
|
44
|
+
# @return [self]
|
|
45
|
+
# @raise [Error] if the target name is not found
|
|
46
|
+
def insert_after(target_name, middleware, name: nil)
|
|
47
|
+
validate_middleware!(middleware)
|
|
48
|
+
index = find_index!(target_name)
|
|
49
|
+
@entries.insert(index + 1, Entry.new(middleware: middleware, name: name))
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Remove a middleware by name.
|
|
54
|
+
#
|
|
55
|
+
# @param target_name [String, Symbol] name of the entry to remove
|
|
56
|
+
# @return [self]
|
|
57
|
+
# @raise [Error] if the target name is not found
|
|
58
|
+
def remove(target_name)
|
|
59
|
+
index = find_index!(target_name)
|
|
60
|
+
@entries.delete_at(index)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Execute the middleware stack with the given environment.
|
|
65
|
+
#
|
|
66
|
+
# @param env [Object] the environment/context passed through the stack
|
|
67
|
+
# @return [Object] the final environment after all middleware have run
|
|
68
|
+
def call(env)
|
|
69
|
+
chain = build_chain
|
|
70
|
+
chain.call(env)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Return the list of middleware names/entries.
|
|
74
|
+
#
|
|
75
|
+
# @return [Array<String, Symbol, nil>] names of middleware in order
|
|
76
|
+
def to_a
|
|
77
|
+
@entries.map(&:name)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
Entry = Struct.new(:middleware, :name, keyword_init: true)
|
|
83
|
+
|
|
84
|
+
def validate_middleware!(middleware)
|
|
85
|
+
raise Error, 'middleware must respond to #call' unless middleware.respond_to?(:call)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def find_index!(target_name)
|
|
89
|
+
index = @entries.index { |e| e.name == target_name }
|
|
90
|
+
raise Error, "middleware '#{target_name}' not found" unless index
|
|
91
|
+
|
|
92
|
+
index
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_chain
|
|
96
|
+
# Build from right to left: last middleware calls the terminal,
|
|
97
|
+
# each preceding middleware calls the next one.
|
|
98
|
+
terminal = ->(env) { env }
|
|
99
|
+
|
|
100
|
+
@entries.reverse.reduce(terminal) do |next_mw, entry|
|
|
101
|
+
->(env) { entry.middleware.call(env, next_mw) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: philiprehberger-middleware
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Philip Rehberger
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-22 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A composable middleware stack that supports lambda and class-based middleware,
|
|
14
|
+
named entries with insert-before/after and removal, and sequential execution with
|
|
15
|
+
short-circuit capability.
|
|
16
|
+
email:
|
|
17
|
+
- me@philiprehberger.com
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- CHANGELOG.md
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/philiprehberger/middleware.rb
|
|
26
|
+
- lib/philiprehberger/middleware/stack.rb
|
|
27
|
+
- lib/philiprehberger/middleware/version.rb
|
|
28
|
+
homepage: https://github.com/philiprehberger/rb-middleware
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://github.com/philiprehberger/rb-middleware
|
|
33
|
+
source_code_uri: https://github.com/philiprehberger/rb-middleware
|
|
34
|
+
changelog_uri: https://github.com/philiprehberger/rb-middleware/blob/main/CHANGELOG.md
|
|
35
|
+
bug_tracker_uri: https://github.com/philiprehberger/rb-middleware/issues
|
|
36
|
+
rubygems_mfa_required: 'true'
|
|
37
|
+
post_install_message:
|
|
38
|
+
rdoc_options: []
|
|
39
|
+
require_paths:
|
|
40
|
+
- lib
|
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: 3.1.0
|
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
47
|
+
requirements:
|
|
48
|
+
- - ">="
|
|
49
|
+
- !ruby/object:Gem::Version
|
|
50
|
+
version: '0'
|
|
51
|
+
requirements: []
|
|
52
|
+
rubygems_version: 3.5.22
|
|
53
|
+
signing_key:
|
|
54
|
+
specification_version: 4
|
|
55
|
+
summary: Generic middleware stack for composing processing pipelines
|
|
56
|
+
test_files: []
|